Lesson 1 of 9 · 20m
TCP vs UDP: What 'Reliable' Actually Buys You
Sequence numbers, ACKs, and datagrams: what TCP guarantees, what it doesn't, and why UDP drops are silent.
A metrics pipeline is silently losing data in production. Nothing crashes, nothing errors — batches just never arrive. The engineer who wrote it chose UDP "because it's faster," and didn't realize they'd also chosen to lose data with no signal when the buffers filled. Choosing between TCP and UDP is choosing which problems you want to debug later.
What "reliable" actually means
TCP gives you a byte stream with four guarantees: order, retransmission of lost segments, duplicate suppression, and flow control so a fast sender can't swamp a slow receiver. It does this with sequence numbers and ACKs — every byte is numbered, and the receiver acknowledges what it got. What it does not guarantee is that the remote application processed your bytes. TCP only promises delivery to the peer kernel's receive buffer. Your app can write() 10 MB, get success back, and still have the remote process crash before reading it. The bug that ships: treating a successful send() as "the server got it."
UDP gives you datagrams — discrete packets delivered whole, or not at all. No ordering, no dedup, no ACKs. Your reads are message-aligned: recvfrom() returns exactly what one sendto() put on the wire, assuming it arrived at all.
# UDP: each recvfrom() is one datagram; some just never show up.
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9999))
data, addr = s.recvfrom(65536) # whole datagram, or timeout you set# TCP: recv() returns a chunk of a stream; message boundaries are yours.
s = socket.create_connection(("host", 9999))
data = s.recv(65536) # may be half a message or three messagesThe cost of reliability
TCP's ordering creates head-of-line blocking: one lost segment stalls everything behind it until retransmission lands. This is why HTTP/3 moved to QUIC — streams over UDP, so one lost packet only stalls the stream it belongs to, not the whole connection. UDP's cost is different: kernel socket buffers are finite, and when they're full, packets are dropped silently. No error, no log line. That's the metrics pipeline.
Security notes
- TCP's handshake is a state commitment, so SYN floods exhaust listener queues; Linux counters this with SYN cookies (
net.ipv4.tcp_syncookies, on by default). - UDP can't be source-verified by handshake, so it's the reflection/amplification vector of choice: DNS (~50x), NTP, and the infamous memcached UDP amplification (~51,000x, 2018). If you run a UDP service on the public internet, rate-limit responses by source or someone will weaponize you.
Try this
# Terminal 1 — UDP listener
nc -lu 9999
# Terminal 2 — blast 10000 datagrams
for i in $(seq 1 10000); do echo "msg $i"; done | nc -u -w1 127.0.0.1 9999Count what arrived. Then watch it get worse: increase the sender's rate, or shrink the receiver's buffer (net.core.rmem_max caps it). Drops climb silently — no error on either side. That silence is the point.
Field notes
- "Unreliable" doesn't mean broken — DNS, RTP, WireGuard, and game state pick UDP deliberately because a late answer is worse than a missing one.
- If you need reliability and no head-of-line blocking, QUIC is the current answer; if you just need "fast and lossy," UDP is fine — instrument the drops.