UDP socket buffer sizes are a major performance bottleneck, and tuning them is often the difference between a sluggish connection and one that saturates your network link.
Let’s watch a simple UDP sender and receiver interact, and see how buffer sizes change things.
Imagine a producer sending UDP packets as fast as it can, and a consumer reading them. If the producer is faster than the consumer, packets will start to pile up. Where do they pile up? In the operating system’s socket buffers.
// sender.go
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
panic(err)
}
defer conn.Close()
fmt.Println("Sending packets...")
for i := 0; ; i++ {
_, err := conn.Write([]byte(fmt.Sprintf("Packet %d", i)))
if err != nil {
fmt.Fprintf(os.Stderr, "Write error: %v\n", err)
// In a real scenario, you might want to back off or retry
time.Sleep(100 * time.Millisecond) // Simple backoff
continue
}
if i%10000 == 0 {
fmt.Printf("Sent %d packets\n", i)
}
}
}
// receiver.go
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
panic(err)
}
defer conn.Close()
buffer := make([]byte, 1024) // A small buffer for reading
fmt.Println("Listening for packets...")
// Get initial buffer sizes
rcvBufSize, _ := conn.GetSockOptInt(1, 2) // SOL_SOCKET, SO_RCVBUF
fmt.Printf("Initial SO_RCVBUF: %d bytes\n", rcvBufSize)
for {
n, _, err := conn.ReadFromUDP(buffer)
if err != nil {
fmt.Fprintf(os.Stderr, "Read error: %v\n", err)
continue
}
if n > 0 {
// fmt.Printf("Received: %s\n", buffer[:n]) // Too chatty
}
if len(buffer) < n { // This should not happen with proper buffer sizing
fmt.Fprintf(os.Stderr, "Buffer overflow! Received %d bytes, buffer size %d\n", n, len(buffer))
}
if n%10000 == 0 {
fmt.Printf("Received %d packets\n", n)
}
}
}
If you run these two, the receiver will likely start dropping packets very quickly. The sender is just blasting data, and the receiver’s kernel-level buffer (SO_RCVBUF) fills up. Once full, the kernel drops incoming packets. The sender might not even know this is happening because UDP is connectionless and doesn’t provide explicit acknowledgments or retransmissions.
The two key parameters are SO_SNDBUF (send buffer) and SO_RCVBUF (receive buffer). These are kernel-level buffers associated with each socket.
SO_RCVBUF: This is the buffer where the kernel stores incoming UDP datagrams after they arrive on the network interface but before your application reads them viaReadFromUDP. If this buffer fills up, incoming datagrams are dropped.SO_SNDBUF: This is the buffer where your application’s data is placed before the kernel sends it out over the network interface. If this buffer fills up, yourWritecalls will block or return an error (EAGAIN/EWOULDBLOCKif non-blocking, or a fullEWOULDBLOCKon UDP).
The default values for these buffers are often quite small, typically a few tens of kilobytes. For high-throughput UDP, this is almost always insufficient.
Tuning SO_RCVBUF
This is usually the more critical one for high-speed UDP reception.
-
Diagnosis: Run the receiver program. Observe if it reports
Read error: read udp 127.0.0.1:8080: i/o timeoutor if theninReadFromUDPdoesn’t increment as rapidly as the sender’sWritecalls. You can also check the current buffer size usingss -nu -w 60 | grep 8080(look forrcv-qandsnd-qwhich are queue lengths, not buffer sizes directly, but high values indicate a problem). A more direct way is to usegetsockoptin your application as shown in the receiver example. -
Cause: The default
SO_RCVBUFis too small for the rate of incoming data. -
Fix (Application Level):
// In receiver.go, before conn.ReadFromUDP // Set SO_RCVBUF to 4MB (4 * 1024 * 1024) err = conn.SetSockOptInt(1, 2, 4*1024*1024) // SOL_SOCKET, SO_RCVBUF if err != nil { fmt.Fprintf(os.Stderr, "Error setting SO_RCVBUF: %v\n", err) // Handle error appropriately } rcvBufSize, _ := conn.GetSockOptInt(1, 2) // SOL_SOCKET, SO_RCVBUF fmt.Printf("Set SO_RCVBUF to: %d bytes\n", rcvBufSize)Why it works: This tells the kernel to allocate a larger ring buffer for this specific UDP socket. Incoming datagrams can queue up in this larger buffer, giving your application more time to process them before they are dropped.
-
Cause: The system-wide maximum for socket buffer sizes is too low. The kernel often has a
net.core.rmem_maxandnet.core.wmem_maxsysctl value that caps the actual buffer size, even if you request more. -
Fix (System Level):
# Check current limits sysctl net.core.rmem_max sysctl net.core.wmem_max # Set limits to 16MB (example) sudo sysctl -w net.core.rmem_max=16777216 sudo sysctl -w net.core.wmem_max=16777216 # Make persistent (e.g., in /etc/sysctl.conf or a file in /etc/sysctl.d/) # echo "net.core.rmem_max=16777216" | sudo tee -a /etc/sysctl.conf # echo "net.core.wmem_max=16777216" | sudo tee -a /etc/sysctl.confWhy it works: These sysctls define the absolute maximum size the kernel will allow for receive and send buffers across all sockets. By increasing them, you permit larger individual socket buffers to be set.
-
Cause: The default initial send/receive buffer sizes are too small, and the kernel’s automatic tuning mechanism doesn’t grow them large enough.
-
Fix (System Level):
# Check default initial sizes sysctl net.ipv4.udp_rmem_min sysctl net.ipv4.udp_wmem_min # Increase to 4MB (example) sudo sysctl -w net.ipv4.udp_rmem_min=4194304 sudo sysctl -w net.ipv4.udp_wmem_min=4194304 # Make persistent # echo "net.ipv4.udp_rmem_min=4194304" | sudo tee -a /etc/sysctl.conf # echo "net.ipv4.udp_wmem_min=4194304" | sudo tee -a /etc/sysctl.confWhy it works: These parameters set the minimum size for UDP socket buffers. The kernel will attempt to grow them up to
net.core.rmem_max(orwmem_max) as needed, but starting with a larger minimum gives them a better chance to reach sufficient size.
Tuning SO_SNDBUF
This is important if your sender is experiencing Write calls blocking or returning errors, or if you want to ensure the sender can keep up with its processing rate.
-
Diagnosis: Run the sender program. Observe if
Writecalls return errors likewrite udp 127.0.0.1:8080: sendto: Resource temporarily unavailableor if the program slows down significantly. You can also checkss -nu -w 60 | grep 8080forsnd-q. -
Cause: The default
SO_SNDBUFis too small for the rate of outgoing data. -
Fix (Application Level):
// In sender.go, after conn := net.DialUDP(...) // Set SO_SNDBUF to 4MB (4 * 1024 * 1024) err = conn.SetSockOptInt(1, 2, 4*1024*1024) // SOL_SOCKET, SO_SNDBUF if err != nil { fmt.Fprintf(os.Stderr, "Error setting SO_SNDBUF: %v\n", err) // Handle error appropriately } // Note: Go's net.DialUDP does not directly expose SetSockOpt for send buffer size. // You'd typically need to use a lower-level API or a library that does. // For demonstration, if you were using net.ListenUDP for sending (less common for pure sender) // or a custom socket implementation, you would apply it there. // The principle is the same: use getsockopt/setsockopt.Why it works: Allocates a larger kernel buffer for outgoing data, allowing the sender application to queue up more data to be sent without blocking.
-
Cause: System-wide
net.core.wmem_maxis too low (covered inSO_RCVBUFsection). -
Fix (System Level): Increase
net.core.wmem_maxas described above. -
Cause: The default initial send buffer size (
net.ipv4.udp_wmem_min) is too small. -
Fix (System Level): Increase
net.ipv4.udp_wmem_minas described above.
The actual buffer sizes you set are often a trade-off. Too small, and you drop packets or block. Too large, and you consume excessive memory, and packets can experience higher latency within the kernel buffers before being processed. A common starting point for high-performance UDP is 4MB to 16MB for both SO_RCVBUF and SO_SNDBUF, but this should be tuned based on your specific network conditions, packet size, and application’s processing speed.
After tuning these buffers, you might encounter the next common issue: the application itself is too slow to process the incoming data, leading to Read calls blocking or the sender’s Write calls blocking.