UDP: Connectionless Transport

You are on a video call. Someone's face freezes for a fraction of a second, a syllable drops, and then the conversation carries right on. Nobody stops to re-request the lost frame — by the time it could be resent, the moment is gone; the next frame is what matters. This is the world UDP was built for: applications that would rather lose a little data than wait for it.

UDP — the User Datagram Protocol — is the transport layer stripped to the bone. Where TCP is an elaborate machine of handshakes, acknowledgements, timers and windows, UDP is barely more than multiplexing plus an optional error check. It takes your data, slaps on an eight-byte header, and throws it at the network. No promises. And that radical minimalism, far from being a weakness, is precisely why UDP underpins DNS, DHCP, live media, online games, and even the new HTTP/3.

The service UDP provides (and the four it doesn't)

UDP gives an application exactly two things over raw IP: multiplexing (ports, so the bytes reach the right program) and an optional checksum (so gross corruption can be detected). That is the entire feature list. Everything you might expect from a transport protocol, UDP deliberately omits:

Because there is no connection and no per-connection state, a UDP socket is a single mailbox that can exchange datagrams with the whole world at once — which is exactly why one socket on port 53 can serve every DNS query on the planet. The unit UDP sends is a self-contained datagram: unlike a TCP byte stream, message boundaries are preserved — one send becomes one recv. That message shape is another quiet reason applications choose it.

The eight-byte header

The whole UDP header is four 16-bit fields — eight bytes, the smallest of any mainstream transport protocol (TCP's is 20 bytes minimum). There is nothing to configure and nothing to negotiate:

FieldSizeMeaning
Source port16 bitsSender's port (may be 0 if no reply is expected)
Destination port16 bitsWhich socket on the destination host gets the datagram
Length16 bitsLength in bytes of header + data (minimum 8)
Checksum16 bitsError-detection over header, data, and a pseudo-header

Eight bytes of overhead per datagram versus TCP's twenty-plus is a real saving for tiny, frequent messages — a game sending 60 position updates a second, or a DNS query that fits in a single packet. The Length field is technically redundant (IP already knows the packet size), a small historical wart. The interesting field is the last one.

The checksum: one's-complement arithmetic

The UDP (and TCP, and IP) checksum uses a charming old trick: treat the data as a sequence of 16-bit integers, add them all up using one's-complement addition, and store the complement (bitwise NOT) of the sum. One's-complement addition means: add normally, but whenever the sum overflows past 16 bits, take the carry bit and add it back into the low 16 bits ("end-around carry"). The receiver adds up all the words including the checksum; if nothing was corrupted, the result is all-ones (0xFFFF).

It's cheap enough to compute in software on every packet, and it catches all single-bit errors and most burst errors — though, being only 16 bits, it is a weak check by modern standards, not a cryptographic guarantee. The demo computes it step by step over a handful of 16-bit words:

// One's-complement 16-bit checksum, as used by UDP/TCP/IP. // Sum the 16-bit words; fold any carry back into the low 16 bits; complement. function checksum16(words: number[]): number { let sum = 0; for (const w of words) { sum += w; // may exceed 16 bits sum = (sum & 0xffff) + (sum >>> 16); // end-around carry (fold once) } // A second fold in case the last add produced a fresh carry. sum = (sum & 0xffff) + (sum >>> 16); return (~sum) & 0xffff; // one's complement, kept to 16 bits } const hex = (n: number) => "0x" + n.toString(16).padStart(4, "0").toUpperCase(); // Pretend these are the 16-bit words of a small datagram. const data = [0x4500, 0x001c, 0x0000, 0x4011, 0xa8c0, 0x0001]; const cksum = checksum16(data); console.log("Words: " + data.map(hex).join(" ")); console.log("Checksum:", hex(cksum)); // The RECEIVER sums the same words PLUS the checksum. Result should be 0xFFFF. function verify(words: number[], cksum: number): boolean { let sum = cksum; for (const w of words) { sum += w; sum = (sum & 0xffff) + (sum >>> 16); } sum = (sum & 0xffff) + (sum >>> 16); return sum === 0xffff; // all-ones means "no error detected" } console.log("\nReceiver check (clean): ", verify(data, cksum) ? "OK (0xFFFF)" : "CORRUPT"); // Flip one bit in transit and watch the check fail. const corrupted = [...data]; corrupted[2] ^= 0x0001; // single-bit error console.log("Receiver check (1 bit flip):", verify(corrupted, cksum) ? "OK (0xFFFF)" : "CORRUPT — detected!");

One's-complement representation has two zeros (+0 = all-zeros, −0 = all-ones) and, crucially, the arithmetic is endianness-friendly: you get the same checksum whether you sum the words big-endian or little-endian, which let 1970s routers on different hardware agree without conversion. The end-around carry is what makes the modular arithmetic work in one's-complement — a carry out of the top is a "wrap around" that must be re-added at the bottom. The payoff is a checksum any machine can compute and verify identically, in a few instructions per word.

So why on earth choose UDP?

If TCP does so much more, why would a serious application pick the protocol that promises nothing? Four reasons, and every heavy UDP user is chasing at least one of them:

The classic UDP roll-call: DNS (one-shot query/response), DHCP (before you even have an IP), VoIP and video (latency > perfection), online games (freshest state wins), SNMP/NTP (tiny periodic messages), and — the plot twist — QUIC, the protocol under HTTP/3, which rebuilds reliability, ordering and congestion control on top of UDP in user space to escape TCP's limitations while keeping its guarantees. When TCP's fixed behaviour isn't the behaviour you want, you start from UDP and build your own.

"Unreliable" is a technical term here, and it trips people constantly. It does not mean UDP is flaky or that datagrams usually vanish. On a healthy local network, UDP delivers the vast majority of packets perfectly, often indistinguishably from TCP. "Unreliable" means only that UDP makes no promise: it will not detect loss, will not retransmit, will not guarantee order — so if the network drops something, UDP won't fix it for you. The correct reading is "best-effort, no guarantees," not "expect failure." And when an application does need reliability over UDP — as QUIC does — it simply adds acknowledgements, sequence numbers and timers itself, at the application layer, tuned exactly to its needs. Reliability isn't absent; it's relocated.