22. UDP: the header, and when "unreliable" is the right choice
22.1 The entire header (8 bytes)
0 1 2 3
+--------+--------+--------+--------+--------+--------+--------+--------+
| Source Port (16) | Destination Port (16) |
+--------+--------+--------+--------+--------+--------+--------+--------+
| Length (16, header+data) | Checksum (16) |
+--------+--------+--------+--------+--------+--------+--------+--------+
That's it. No sequence numbers, no ACKs, no connection, no flow/congestion control, no ordering. UDP adds exactly three things to raw IP:
- Ports (application demultiplexing).
- A checksum over pseudo-header + UDP header + data (optional in IPv4 —
0means "not computed"; mandatory in IPv6). - A length field.
sendto() of N bytes → exactly one UDP datagram → (usually) one IP packet. No
merging, no splitting (unless IP fragments it). If it's lost, it's gone — the app
finds out only if it built its own detection.
22.2 "Connected" UDP sockets
You can connect() a UDP socket. It doesn't send anything; it just fixes the
default peer so you can use send()/recv() and — importantly — the kernel will
deliver ICMP port-unreachable errors back to that socket (as ECONNREFUSED
on the next syscall). An unconnected UDP socket silently swallows those.
22.3 When UDP is the right call
| Use case | Why UDP wins |
|---|---|
| DNS queries | one tiny request/response; a lost query is just re-asked; connection setup would triple the cost |
| Real-time audio/video (RTP, WebRTC) | a 200 ms-late retransmitted voice packet is useless — better to skip it; app does its own loss concealment and jitter buffer (Ch. 47) |
| Live game state | only the latest position matters; TCP's in-order guarantee would stall on an old lost packet (head-of-line blocking) |
| QUIC / HTTP/3 | wants reliability + encryption + streams but implemented in userspace so it can evolve without kernel/middlebox upgrades; UDP is just the substrate (Ch. 39) |
| DHCP, TFTP, SNMP, NTP, syslog | simple, often broadcast/one-shot, on constrained devices |
| Multicast / broadcast | TCP is strictly point-to-point; UDP can go one-to-many (IPTV, service discovery, mDNS) |
| VPN transports (WireGuard, OpenVPN-UDP) | avoid TCP-over-TCP meltdown: two nested reliable transports fight each other's retransmit timers and collapse under loss |
22.4 What your UDP app must handle itself (if it needs them)
- Loss detection & retransmission (sequence numbers + ACKs, or FEC).
- Ordering / reassembly of multi-datagram messages.
- Congestion control — ethically mandatory. Uncontrolled UDP floods are how you cause collateral damage and get null-routed. QUIC implements NewReno/CUBIC/ BBR; WebRTC uses GCC (Google Congestion Control); use a library, don't wing it.
- Path MTU — keep datagrams small (≤1200) or do PLPMTUD; you won't get the friendly "connects then hangs" — you'll just get silent drops.
- NAT keepalives — UDP NAT mappings expire in as little as 30 s; send a keepalive every ~15–25 s or inbound packets stop (Ch. 46).
- Amplification hygiene — never answer a small spoofed request with a big
response to an unverified source (Ch. 54). DNS, NTP
monlist, memcached, and SSDP became infamous DDoS amplifiers this way.
22.5 UDP framing: one datagram = one message
Unlike TCP (a byte stream where you must delimit messages yourself), each UDP
recvfrom() returns exactly one datagram. If your buffer is smaller than the
datagram, the rest is discarded (MSG_TRUNC). A zero-length UDP datagram is
valid and is delivered (useful as a ping/keepalive).
22.6 War story
A metrics agent shipped counters over UDP to a collector "for low overhead."
During incidents — exactly when metrics matter most — the collector's socket
receive buffer (net.core.rmem_max) overflowed under the surge and the kernel
dropped ~40% of datagrams silently (netstat -su → "receive buffer errors").
Dashboards under-reported the outage. Fixes: raise SO_RCVBUF, batch with
recvmmsg(), sample, and add sequence numbers to measure loss.
Lesson: UDP loss is invisible unless you instrument it; check
nstat/netstat -su for InErrors/RcvbufErrors.
Try it yourself
netstat -su # UDP stats: InErrors, RcvbufErrors, NoPorts
nstat -az | grep -i udp
ss -uap # UDP sockets + Recv-Q backlog
# Quick UDP echo test:
nc -u -l 9999 # terminal 1
nc -u 127.0.0.1 9999 # terminal 2, type lines
sysctl net.core.rmem_max net.core.rmem_default