UDP: Datagrams and the Contract You Choose
UDP wraps an application message in an 8-byte header and hands it to IP as one datagram — no connection, no ordering, no retransmission, no flow or congestion control — so the application gets message boundaries, minimal latency and total control, and inherits every responsibility TCP would otherwise have taken.
The problem
A datagram, not a stream
The application calls sendto() with a buffer and a destination. UDP prepends an 8-byte header — source port, destination port, length, checksum — and hands the result to IP as one datagram. IP delivers it, or not. The receiver’s recvfrom() returns exactly the bytes of one datagram, or nothing; it never returns half of one, and never two joined together. Message boundaries are preserved. That alone is a reason to choose UDP: the framing problem that bites every TCP application (TCP: A Reliable Ordered Byte Stream over an Unreliable Network) does not exist here.
There is no connection. There is no handshake, so the first datagram can leave immediately; there is no state in the kernel for a "connection", so a server can talk to a million clients through one socket; and there is nothing to tear down. A UDP "connection" is a convention in the application — DNS matches requests to responses by a 16-bit ID, QUIC by a connection ID it carries in its own header.
The header is 8 bytes against TCP’s minimum 20 and typical 32 (with options). More important than the bytes is what is *absent*: no sequence numbers, no acknowledgments, no windows, no timers in the kernel. Every one of those is machinery the application either does not need or will implement itself, with knowledge the kernel does not have — which packets are still worth delivering, and which are already too late.
- Application: sendto(buf, 412 bytes)one call, one message↓
- UDP: 8-byte headersrc port, dst port, length = 420, checksum↓
- IP: 20-byte header (IPv4)440-byte packet; if larger than the MTU, fragmented↓
- Link: frameEthernet, Wi-Fi; the packet leaves↓
- Receiver: recvfrom() returns 412 bytesor nothing at all; never 200 now and 212 later
1import socket2s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # no connect() needed3for msg in (b"hello", b"world", b"!"):4 s.sendto(msg, ("203.0.113.10", 9999))5# On the receiver, recvfrom(65535) is called three times and returns6# b"hello", b"world", b"!" — in some order, each possibly missing, never merged.What UDP does not do — and who does it instead
The list is TCP’s feature list: no delivery guarantee, no ordering, no duplicate suppression, no flow control, no congestion control. It is tempting to read that as "unreliable TCP" and it is the wrong reading. TCP offers one contract — a reliable, ordered byte stream, at whatever latency that takes. UDP offers a different one — deliver this message now if you can — and leaves the application to add exactly the guarantees it needs, no more.
Look at what real applications add. DNS (DNS: Why Names Need a Distributed Database) adds a retry with the same ID after a timeout; one question, one answer, nothing to order. RTP for voice and video adds a sequence number and a timestamp so the receiver can detect loss and reorder within a small jitter buffer, and then plays on; a lost frame is concealed, not retransmitted. Games send the full current state every tick, so a lost tick is superseded by the next one and retransmission would be actively harmful. QUIC (HTTP/3 and QUIC) builds a *complete* reliable, congestion-controlled, multiplexed transport in user space on top of UDP — with the difference that its streams are independent and it can change its algorithms by shipping a new library, not a new kernel.
The obligation that comes with the freedom: an application that sends UDP as fast as it likes with no congestion control will flood the path, cause loss for every TCP flow sharing it, and be throttled or blocked by operators. The IETF’s guidelines (RFC 8085) expect UDP applications to behave — QUIC, WebRTC and modern media stacks implement real congestion control; a hand-rolled protocol that does not is a problem for everyone else on the link.
| Application | Reliability | Ordering | Congestion control | Why UDP |
|---|---|---|---|---|
| DNS | retry by ID | none needed | none (tiny, request/response) | one round trip, no handshake |
| RTP / VoIP / video | none (conceal loss) | jitter buffer, drop late | RTCP feedback, rate adaptation | lateness is worse than loss |
| Game state | superseded by next tick | discard older ticks | fixed tick rate | newest state only |
| QUIC (HTTP/3) | full, per stream | per stream, independent | full (CUBIC/BBR-class) | no transport HoL blocking, evolvable in user space |
| syslog / statsd / metrics | none | none | none | fire and forget; losing a metric beats blocking the app |
Size, fragmentation and the MTU
A UDP datagram can carry up to 65,507 bytes over IPv4 (65,535 minus the two headers). Almost nobody should send that. A link’s MTU — 1500 bytes on Ethernet — bounds the *packet*; a datagram larger than MTU minus headers (1472 bytes on plain IPv4 Ethernet) is fragmented by IP into several packets and reassembled by the receiving host. Fragmentation is legal and terrible: if any one fragment is lost the whole datagram is lost, many firewalls and NATs drop fragments outright, and IPv6 routers do not fragment at all (the sender must, or must not exceed the path MTU).
The practical rule is to stay under the path MTU with margin. DNS resolvers advertise an EDNS buffer of 1232 bytes precisely to avoid fragmentation on any common path; QUIC requires a path to carry 1200-byte datagrams and probes upward from there. Sending 8 kB datagrams "because it works on the LAN" is the classic way to build something that fails only across the internet, and only sometimes — see the mtu-blackhole challenge.
- Ethernet MTU 1500 → 1472 bytes of UDP payload on IPv4, 1452 on IPv6, less inside tunnels and VPNs.
- Fragments are dropped by many middleboxes; one lost fragment loses the whole datagram.
- Design to ~1200 bytes if the path is unknown; probe if you must go larger.
UDP through NAT and firewalls
UDP has no connection, so a NAT (NAT: Many Private Hosts Behind One Public Address) or stateful firewall invents one: the first outbound datagram from (inside IP, port) creates a mapping, and replies to the mapped external port are forwarded back — for as long as the mapping lives. Because there is no FIN to say "done", the mapping expires on an idle timer, and UDP timers are short: 30 seconds is a common default, against hours for an established TCP connection. Long-lived UDP sessions — VoIP, VPNs like WireGuard, QUIC — therefore send keepalives every 15–25 seconds purely to keep the mapping alive. Forget them and the session goes silent after a pause, with no error at either end.
Two hosts both behind NATs can often still reach each other directly by hole punching: each learns its own external mapping via a STUN server and sends to the other’s, and the two mappings admit each other’s packets. WebRTC does this routinely. It works with the common "endpoint-independent" NAT behaviour and fails with symmetric NATs, which is why a relay (TURN) is always the fallback.
Firewalls are more hostile to UDP than to TCP: it is harder to inspect, easier to spoof (no handshake to prove the source is real), and the basis of most amplification attacks. Corporate networks routinely block all UDP except DNS. That is why every UDP-based protocol that wants to reach consumers ships with a TCP fallback — QUIC falls back to TCP+TLS, WebRTC to TURN over TCP, and DNS to TCP/53 — and why "it works at home and not at the office" is often "UDP is blocked at the office".
Key points
- UDP = 8-byte header + your message, handed to IP as one datagram; the receiver gets whole messages or nothing.
- No connection, no ordering, no retransmission, no flow or congestion control in the kernel — the application chooses what to add.
- It is not "unreliable TCP"; it is a different contract, and the right one when lateness is worse than loss or when boundaries matter.
- DNS, real-time media, games and QUIC all use it and all add exactly the reliability they need — QUIC adds all of it, in user space.
- Keep datagrams under the path MTU (~1200–1472 bytes); fragmentation fails silently across the internet.
- NATs and firewalls track UDP with short idle timers; long-lived sessions need keepalives, and everything needs a TCP fallback.
- A UDP sender with no congestion control is a hazard to every other flow on the path.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why would anyone want a transport that can lose data?
Because for real-time data the alternative is worse: a retransmitted packet arrives after the moment it belonged to, and waiting for it delays everything behind it. Losing 1% of audio frames is imperceptible; a 300 ms stall every time one is lost is not.
▸Why does QUIC build on UDP instead of on IP directly?
A new IP protocol number would be dropped by nearly every NAT and firewall on earth, and would need kernel support everywhere. UDP already passes through them and already reaches user space, so a new transport can ship as a library and evolve at application speed.
▸Why preserve message boundaries?
Because for request/response and for media frames the message is the natural unit; TCP’s byte stream forces every such application to re-invent framing. UDP simply does not merge or split what you send.
UDP: datagrams, not a stream
How it fails
What the failure looks like from inside real software.
- A metrics or logging pipeline over UDP silently losing 5% of events under load: the graphs are wrong, no error is ever raised, and it is discovered by reconciliation months later.
- A custom protocol sending 4 kB datagrams: perfect on the LAN, intermittent across any path with a smaller MTU or a firewall that drops fragments.
- A VPN or voice session that dies after ~30 seconds of silence: the NAT mapping timed out; the fix is a keepalive, not a bigger timeout on the server.
- A game server with no rate control saturating a home uplink: the household’s TCP traffic collapses while the game feels fine.
- HTTP/3 enabled but UDP/443 blocked at a corporate firewall: browsers fall back to TCP after a race, costing a delay on each new connection; users see "slower at work".
- A DNS client that reuses one source port and predictable IDs: trivially spoofable answers — the reason resolvers randomise both.