UDPUDPdatagramconnectionlessportfragmentation

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.

ConceptualIPv4
▶ InteractiveInterview question
Progress

The problem

A voice call sends fifty packets a second. If packet 37 is lost, a retransmitted copy arriving 200 ms later is worthless — the moment it belonged to has passed, and holding up packets 38–47 to wait for it would be worse than the gap. What transport lets the application make that call itself?

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.

A message becomes a datagram becomes a packet
  1. Application: sendto(buf, 412 bytes)one call, one message
  2. UDP: 8-byte headersrc port, dst port, length = 420, checksum
  3. IP: 20-byte header (IPv4)440-byte packet; if larger than the MTU, fragmented
  4. Link: frameEthernet, Wi-Fi; the packet leaves
  5. Receiver: recvfrom() returns 412 bytesor nothing at all; never 200 now and 212 later
Boundaries preserved: three sends are three receives
1import socket
2s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # no connect() needed
3for 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 returns
6# 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.

What each UDP user adds on top
ApplicationReliabilityOrderingCongestion controlWhy UDP
DNSretry by IDnone needednone (tiny, request/response)one round trip, no handshake
RTP / VoIP / videonone (conceal loss)jitter buffer, drop lateRTCP feedback, rate adaptationlateness is worse than loss
Game statesuperseded by next tickdiscard older ticksfixed tick ratenewest state only
QUIC (HTTP/3)full, per streamper stream, independentfull (CUBIC/BBR-class)no transport HoL blocking, evolvable in user space
syslog / statsd / metricsnonenonenonefire and forget; losing a metric beats blocking the app

Size, fragmentation and the MTU

IPv4

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

Conceptual

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

UDP: datagrams, not a stream
Each send() is one packet with its own boundary. Nothing is numbered, acknowledged or retried — unless the application does it.
Sender · sendto() ×0
#0 429B#1 967B#2 888B#3 520B#4 268B#5 991B#6 1096B#7 315B
Receiver · recvfrom() (as they arrive)
nothing yet
Delivered
0/8
Lost
4 (gone)
Boundaries
kept: 1 recv = 1 send
DNS: One question, one answer, 60 bytes. A retry from the client is cheaper than a handshake. Loss-tolerance beats lateness: a retransmit would arrive after the data stopped mattering.
1/16 · t=0Educational model

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.