Skip to content

Send Policy EN

b23r0 edited this page Oct 12, 2026 · 6 revisions

English · 简体中文

Optional send policy

Availability: included in 1.1.0, published on October 3, 2026. The measured implementation originated in 8c06cf4, now part of the public release. Older releases do not provide this feature.

Enable it explicitly

Enable the feature on the released crate:

[dependencies]
rust-raknet = { version = "1.4.0", features = ["send-policy"] }

The feature exposes SendOptions, SendOptionsError, set_send_options and send_options; it does not activate the policy on every socket automatically.

use rust_raknet::{RaknetSocket, SendOptions};

async fn configure(socket: &RaknetSocket) -> Result<(), Box<dyn std::error::Error>> {
    let options = SendOptions::default()
        .with_in_flight_limits(64, 256 * 1024)?
        .with_queue_budgets(256 * 1024, 64 * 1024)?
        .with_flush_budget(256 * 1024)?;
    socket.set_send_options(options).await?;
    assert_eq!(socket.send_options().await, Some(options));
    Ok(())
}

Apply to connected client sockets and accepted server sockets before bulk sending. Options are shared by clones of the same connection. A feature-enabled socket returns None until configured. Default builds do not compile policy state or checks and do not expose these methods.

Where to set options

The configure function above can be applied immediately after connecting:

let address: std::net::SocketAddr = "127.0.0.1:19132".parse()?;
let client = RaknetSocket::connect(&address).await?;
configure(&client).await?;

On the server, configure each socket returned by accept, before starting its sender task. This is a socket API, not a listener option:

let client = listener.accept().await?;
configure(&client).await?;

For a reverse proxy, the accepted frontend and the connected upstream are separate connections. Configure both if both sending directions need the policy. Configuring one leg does not configure the other, and no policy is negotiated with the remote peer.

let frontend = listener.accept().await?;
let upstream = RaknetSocket::connect(&upstream_address).await?;
configure(&frontend).await?;
configure(&upstream).await?;

These fragments assume a listening RaknetListener, an upstream SocketAddr where needed, and an async function that propagates errors. The setting changes future admission and scheduling, not message reliability: continue selecting Reliability with each send.

Changing an existing connection

Preserve other configured values when changing a limit:

let previous = socket.send_options().await.unwrap_or_default();
let updated = previous.with_in_flight_limits(32, 64 * 1024)?;
socket.set_send_options(updated).await?;

If the socket was unconfigured, this activates the policy. If it was already configured, unchanged queue and flush budgets remain in place. Lowering limits preserves accepted messages and outstanding frames; stricter admission takes effect as old work drains. The peer can receive a short remaining burst from earlier accepted work, so this is not an instantaneous network-rate limiter.

There is no disable/reset-to-unconfigured API. set_send_options(SendOptions::default()) applies a preset; it does not turn off the scheduler. A new unconfigured socket starts with the original strategy, and a build without the feature excludes the policy code.

Choosing limits

Goal Start with What to watch
Reliable-only throughput Default build; no policy setter No need to add policy costs when you do not need its controls
Mixed reliable and realtime updates Explicit SendOptions::default() and separate sender tasks Delivery rate and reliable throughput as well as latency of received updates
Less outstanding reliable data Reduce frame or byte limit one at a time Healthy high-RTT throughput can fall; packet loss alone does not establish queue congestion
Smaller bursts Reduce flush_bytes cautiously Remaining frames may wait for another send, ACK or maintenance tick
More queued work Adjust the reliable soft budget or unreliable reserve Queueing delay, memory and application backpressure; these are not bandwidth allocations

Frame and byte limits apply together: whichever is reached first stops new reliable flight. For a rough starting estimate, bandwidth × RTT gives a byte budget for data in transit (100 Mbps × 30 ms ≈ 375,000 B). This is not an automatic controller: application windows, ACK behavior, encoded-frame overhead and shared bottlenecks still matter. Queue budgets and the in-flight limits are different constraints.

The 256 KiB flight-byte cap in the earlier example is a custom configuration. The benchmark policy columns use SendOptions::default() with 64 MiB; do not attach those measurements to the custom cap.

Limits and defaults

Limit SendOptions::default() Meaning
Reliable flight frames 64 Outstanding reliable frames, including fragments and individual packed members
Reliable flight bytes 64 MiB Encoded frame bytes, including frame-set headers
Reliable queue soft budget 256 KiB Pending and unacknowledged reliable payload plus estimated metadata
Unreliable queue reserve 64 KiB Independent pending budget; newly accepted reliable data must leave it free
Flush budget 256 KiB Encoded frame-byte burst returned by one scheduled flush

Frame limits accept 1–4096. Flight and flush byte limits accept 2048 B–64 MiB. Each queue budget must be at least 2048 B, and their sum cannot exceed 64 MiB. Applying options also checks the negotiated MTU. Builders return SendOptionsError; applying incompatible limits returns a transport error.

These are accounting limits, not process RSS or bandwidth caps. A larger reliable send, including a batch, may exceed the reliable soft budget but remains subject to the 64 MiB hard accounting limit and unreliable reserve. Unreliable batches larger than their reserve stream through the existing send path rather than requiring the entire batch to fit at once.

Reliable and unreliable traffic

Configured connections keep independent pending queues. A full reliable flight window does not prevent admission of a small unreliable update while its own reserve is available. Flush scheduling rotates in byte-sized quanta between unreliable frames, reliable retransmissions and new reliable frames, subject to flight and burst limits.

Use separate application sender tasks if unreliable updates must proceed while another task is waiting for reliable capacity. The scheduler does not replace or silently evict accepted unreliable messages. The network can still lose them; UnreliableSequenced retains its existing sequence-filtering semantics.

Lowering limits preserves already accepted messages; they drain before further admission. A very small flush budget may defer remaining work to the next send, ACK or existing maintenance tick. There is no new timer to wait for a batch.

Compatibility and congestion

The wire encoding, five reliability modes, fragmentation, ACK/NACK handling and retransmission rules remain unchanged. Options require no new peer negotiation. The policy is not adaptive congestion control, does not guarantee a bandwidth split and does not expand or shrink windows automatically.

A smaller window can reduce bottleneck queue pressure while also limiting healthy high-RTT throughput. Choose it using your workload and network tests rather than assuming a smaller window is always better.

Performance and validation

The October 3 benchmark compares default and explicitly configured ordinary/batch builds against TCP, C KCP and quic-go. It contains 623 verified measurements with ranges and memory. All profiles use reliable ordered echoes; they do not measure mixed-traffic scheduling benefits.

The policy changes queue admission and burst timing, with extra accounting and scheduling work where applicable. It can affect throughput and slow-message latency while preserving the wire protocol. The benchmark does not isolate the cause of each change, and a feature-enabled but unconfigured build is not the default binary measured in the tables.

Reliable ordered profile Default build Configured preset Median change
One connection, 800 B, no injected loss, ordinary throughput 185.57 MiB/s 175.19 MiB/s −5.6%
1,024 connections, 800 B, no injected loss, ordinary throughput 464.75 MiB/s 486.67 MiB/s +4.7%
1,024 connections, 800 B, no injected loss, batch throughput 481.91 MiB/s 423.69 MiB/s −12.1%
1,024 connections, 64 B, no injected loss, ordinary loaded 99th-percentile RTT 54.03 ms 100.53 ms +86.1% (slower)

These are medians from three repeats; the full result tables show ranges and RSS. They are not proof of statistical significance or universal improvements.

A separate mixed-traffic experiment used eight one-connection processes sharing a link that dropped from 100 Mbps / 30 ms RTT to 2 Mbps / 800 ms RTT. Candidate senders used the default policy preset; each saturated a reliable 896 B stream while a separate task produced 64 B unreliable updates at 100 Hz. During congestion, slowest-1% latency among delivered unreliable messages improved from 1,894.73 to 743.49 ms, while reliable throughput fell from 0.072704 to 0.059392 Mbps. Whole-run unreliable delivery fell from 68.65% to 60.01%. Lower delivered-message latency must be considered alongside lost messages and reliable bandwidth.

Keep the feature optional, use separate mixed sender tasks, and measure your payloads, connection counts and network. Neither more buffering nor smaller windows guarantees higher throughput and lower latency at the same time.

Default and feature test builds passed 118 and 144 checks respectively, with overlapping baseline tests. Automated Bedrock 26.52 handshake/NetworkSettings and proxy checks passed 24/24; these were protocol checks, not a new manual gameplay test.

Configuration and concurrency · Benchmark methodology · Reproduction

For recovery timing and tail-loss options introduced in 1.2.0, see recovery policy. The parameter reference covers both independent features. Use the published 1.4.0 dependency for all current examples.

Current benchmark results · Congestion boundaries

Clone this wiki locally