Frontend Labs
⋯

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:

  1. Ports (application demultiplexing).
  2. A checksum over pseudo-header + UDP header + data (optional in IPv4 — 0 means "not computed"; mandatory in IPv6).
  3. 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

Generated from real-life-example.md · 8 Oct 2026 16:36