Skip to content
Docs

Size the UDP receive buffer

Turn an explicit burst tolerance into a verified Linux receive-buffer setting.

Before you start

  • A receiver packet-rate and size sample
  • Permission to change net.core.rmem_max

Start with explicit assumptions

For illustration, use 5,000 packets each second with mean payload bytes of 1,200. Replace both figures with receiver measurements. A receive queue must cover the period during which the application cannot drain its socket, called the stall budget. Include scheduler delay, stop-the-world pauses in linked runtimes, short CPU contention, and worker catch-up after a burst.

For 250 milliseconds at the assumed rate:

text

packets = 5,000 packets/s × 0.250 s = 1,250 packetspayload = 1,250 packets × 1,200 bytes = 1,500,000 bytesoverhead allowance = 1,250 packets × 512 bytes = 640,000 bytesburst multiplier = 4required queue = (1,500,000 + 640,000) × 4 = 8,560,000 bytesround up = 16 MiB requested SO_RCVBUF

The 512-byte allowance is a conservative planning value for socket-buffer accounting, packet metadata, alignment, and variance. It is not a Solana wire constant. Linux charges receive memory by internal allocation size rather than UDP payload length alone, and the exact charge depends on kernel, architecture, and driver path. Validate it with ss -u -m and live traffic.

A request of 16 MiB covers roughly 2.8 seconds of payload bytes if no overhead is counted, or about 2.0 seconds using 1,712 charged bytes per packet. After applying the four-times burst assumption, it represents about 490 milliseconds at that stressed packet rate. This exceeds the original 250 millisecond stall target with rounding headroom.

Set both the host ceiling and socket request

Linux applies net.core.rmem_max as the ceiling for an ordinary SO_RCVBUF request. Set it above the requested value. For internal bookkeeping, Linux doubles the request and returns that larger figure through getsockopt. A 16 MiB request therefore needs at least a 16 MiB ceiling and normally reads back near 32 MiB.

bash

sudo sysctl -w net.core.rmem_max=33554432sudo sysctl -w net.core.rmem_default=4194304sysctl net.core.rmem_max net.core.rmem_default

Persist the settings through the host's sysctl configuration system. rmem_default affects sockets that do not set their own buffer. The receiver should always set SO_RCVBUF, so rmem_max is the important ceiling. Avoid making the global default enormous because every UDP socket can consume receive memory.

Use socket2 and verify the effective value:

rust

use socket2::{Domain, Protocol, Socket, Type};use std::io; fn make_socket(requested: usize) -> io::Result<Socket> {    let sock = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;    sock.set_recv_buffer_size(requested)?;    let effective = sock.recv_buffer_size()?;    if effective < requested {        return Err(io::Error::other("receive buffer capped below request"));    }    let _verified_readback = effective;    Ok(sock)} fn main() -> io::Result<()> {    let _socket = make_socket(16 * 1024 * 1024)?;    Ok(())}

Because Linux usually doubles the value, checking effective < requested detects a severe cap but does not prove exact success. Record both values and alert when the readback changes across deployments.

Separate kernel and application buffers

SO_RCVBUF protects only the interval before the receive system call copies a datagram into user space. Once received, the datagram occupies an application object or queue. Size that queue independently in packets, not bytes alone.

At four times the example rate, 250 milliseconds is:

text

5,000 × 4 × 0.250 = 5,000 packet slots5,000 × 1,228 maximum bytes = 6,140,000 bytes of packet storage

Round to 8,192 slots if the queue implementation benefits from a power of two. Include object metadata and allocator overhead in the memory budget. A bounded queue gives a precise overload signal. An unbounded queue trades visible packet loss for growing latency and eventual memory failure.

Do not add kernel and application capacity and claim the sum as one clean stall budget. The receive thread must run to move packets from the kernel into the application queue. If that thread is descheduled, only SO_RCVBUF helps. If workers are slow while the receive thread runs, the application queue helps.

Measure the real burst distribution

Count datagrams in 1 millisecond, 10 millisecond, 100 millisecond, and 1 second windows. Retain the maximum and high percentiles. Feed production is not uniform. Coding shreds and data shreds can arrive close together, and network queues can compress inter-arrival gaps.

Increase the multiplier when the measured maximum exceeds four times the long-window mean. Decrease it only after observing busy periods and verifying zero RcvbufErrors. A large receive buffer does not repair a receiver that is continuously slower than the feed. It delays loss and increases the amount of stale data waiting in the kernel.

Bound latency as well as loss

Timestamp immediately after receive. Export the age of the oldest application-queued datagram. If that age exceeds the strategy's usefulness window, drop stale work intentionally and continue at the head. For raw shred consumers, processing seconds-old data while current packets are being discarded is usually the wrong failure mode.

Run a stall injection test. Under representative traffic, pause reception for four durations: 50, 100, 250, then 500 milliseconds. Observe SO_RXQ_OVFL, Udp:RcvbufErrors, and application sequence gaps. Accept the configured buffer only when it covers the stated target on the deployed kernel and host.

Parameters

NameTypeDefaultNotes
packets_per_secondf645000Worked-example assumption, replace with the receiver packet rate.
mean_packet_bytesusize1200Worked-example assumption, replace with the receiver mean payload size.
stall_tolerance_msu64250Longest receive-thread pause the socket should absorb.
burst_multiplierf644Multiplier applied to mean rate for short-window bursts.
socket_overhead_bytesusize512Planning allowance, validate against the deployed Linux kernel.
receive_buffer_bytesusize16777216Rounded SO_RCVBUF request from the worked calculation.

When it goes wrong

receive buffer capped: requested=16777216 effective=425984

Cause. net.core.rmem_max is below the socket request.

Fix. Raise net.core.rmem_max under host configuration and restart the receiver.

Udp: RcvbufErrors rises during 250 ms stall test

Cause. Actual per-packet memory charge or burst rate exceeds the sizing assumption.

Fix. Measure skmem and short-window rate, then increase SO_RCVBUF or reduce the stall budget.

receiver_queue_age_seconds keeps increasing

Cause. Downstream processing is slower than sustained input.

Fix. Add processing capacity or shed stale work; a larger socket buffer cannot fix sustained overload.

Questions

Why is the example request 16 MiB?
Its inputs are 5,000 packets each second, a 250 millisecond stall, 1,200 payload bytes, a 512-byte accounting allowance, and a four-times burst multiplier. The result is 8,560,000 bytes, rounded to 16 MiB for operational headroom and convenient configuration. Replace rate and payload size with receiver measurements.
Why does getsockopt report twice the requested value?
Linux reserves internal bookkeeping space by doubling SO_RCVBUF, then reports the larger figure. Other operating systems can differ. Log both requested and effective values, then judge success against observed queue drops instead of treating one readback convention as portable.
Will a larger buffer eliminate packet loss?
No. A larger queue absorbs finite stalls and bursts. It cannot compensate for a receiver whose sustained processing rate is below the input rate, a saturated link, NIC ring loss, or upstream network loss. Monitor each boundary and keep the user-space queue bounded.