pickuma.
Dev Knowledge

TCP vs UDP, Explained Through What Breaks When You Pick Wrong

TCP and UDP aren't interchangeable. We walk through the exact failure modes — head-of-line blocking, silent packet loss, Nagle delays — that show up when you pick the wrong transport.

7 min read

Most “TCP vs UDP” explanations stop at a feature table: TCP is reliable and ordered, UDP is fast and connectionless. True, and useless. You don’t feel the difference until a wrong choice ships and something behaves in a way the table never warned you about — a multiplayer game that stutters precisely when the network is busiest, a metrics agent that reports numbers from 90 seconds ago, a file transfer that arrives corrupted with no error logged anywhere.

The useful way to learn the two protocols is backwards: pick each one for the wrong job and watch what breaks. The failure modes are specific, repeatable, and they map directly onto the guarantees each protocol makes.

What TCP guarantees, and what those guarantees cost

TCP gives you a byte stream that arrives in order, with no gaps and no duplicates, or the connection dies trying. To deliver that, it opens with a three-way handshake (SYN, SYN-ACK, ACK), assigns every byte a sequence number, acknowledges what it receives, retransmits what it doesn’t, and slows itself down when the network signals congestion. You write send(), the bytes come out the other end in the right order. That contract is why HTTP, SSH, and database wire protocols all sit on top of it.

The cost is hidden in the word ordered. TCP will not hand your application byte 5,000 until bytes 1 through 4,999 have arrived. If a single packet in the middle is lost, every packet that arrived after it sits in the kernel’s receive buffer, complete and useless, until the retransmission of the missing one lands. This is head-of-line blocking, and it is the single most important TCP behavior nobody mentions in the feature table.

Now pick TCP for a 60-tick multiplayer shooter. Each tick you send a position update. A packet drops — normal on any real network. TCP detects the loss and retransmits, which on a typical link takes at least one round-trip time, often more once the retransmission timer is involved. For that entire window, every newer position update is stuck behind the lost one. The player freezes, then snaps forward when the backlog flushes. The cruel part: this gets worse under load, exactly when players notice. You picked the protocol that prioritizes delivering stale data over delivering fresh data, in a domain where stale data is worthless.

There’s a quieter TCP trap too: Nagle’s algorithm. To avoid flooding the network with tiny packets, TCP may hold a small write, waiting to coalesce it with the next one. Combined with delayed ACKs on the receiver, this can stall a small request-response exchange for up to roughly 40 ms while each side waits for the other. For a chatty protocol sending many small messages, that latency is invisible in a LAN test and brutal in production. The fix is TCP_NODELAY, but you only reach for it once you know the behavior exists.

Where UDP wins, and the bill it hands you

UDP is almost nothing: a 8-byte header, source and destination ports, length, checksum. No handshake, no sequence numbers, no acknowledgments, no retransmission, no ordering, no congestion control. You hand the kernel a datagram and it tries once. The datagram arrives intact, arrives corrupted-and-discarded, arrives out of order relative to its siblings, arrives duplicated, or never arrives — and UDP tells you nothing about which happened.

That sounds worse, until you remember the game. With UDP, a lost position update is simply skipped; the next datagram carries a newer position anyway, so there’s nothing worth retransmitting. No head-of-line blocking, because there is no line. This is why real-time voice, video, and games live on UDP, and why QUIC — the transport under HTTP/3 — was built on UDP specifically to escape TCP’s head-of-line blocking while rebuilding reliability per-stream.

But UDP hands you a bill, and developers underpay it constantly. Pick UDP for a job that actually needs reliability — say, shipping log lines to a collector — and you will reinvent TCP, badly. First you notice lines go missing under load, so you add acknowledgments. Then duplicates appear, so you add sequence numbers to dedupe. Then you discover messages arrive out of order, so you add a reordering buffer. Then the receiver gets overwhelmed because nothing throttles the sender, so you add flow control. You have now written a worse TCP, with more bugs, and you still don’t have congestion control, so your agent contributes to network collapse during an incident.

There’s also a size trap. A UDP datagram larger than the path MTU (commonly around 1500 bytes on Ethernet) gets fragmented at the IP layer. If any single fragment is lost, the entire datagram is discarded — and many middleboxes drop IP fragments outright. So a 4 KB UDP message can vanish on networks where a 1 KB one always works, with nothing in your logs. Keeping datagrams under the MTU is a constraint you have to enforce yourself.

The decision, framed by failure mode

Skip the feature checklist. Ask one question: when a packet is lost, what does your application want to happen?

If the answer is “wait for it, I need every byte in order” — file transfer, an API call, a database query, anything where a gap corrupts meaning — use TCP and accept the latency variance. If the answer is “skip it, the next one supersedes it” — live telemetry, game state, voice, anything where freshness beats completeness — use UDP and budget engineering time for the reliability you do need.

Failure modeTCPUDP
Packet lost mid-streamRetransmits; later data blocks until it arrivesSkipped; next datagram proceeds independently
Out-of-order arrivalReassembled in order for youDelivered as-is; you reorder if you care
Receiver overwhelmedFlow + congestion control throttle the senderNothing throttles; you build it or melt the link
Message exceeds MTUSegmented transparentlyIP-fragmented; one lost fragment drops the whole message

The trap on both sides is the same shape: each protocol’s strength is the other’s failure mode. TCP’s ordering becomes head-of-line blocking. UDP’s leanness becomes a pile of reliability code you have to write and test yourself. Picking well means knowing which failure your application can tolerate, not which feature list looks longer.

Cursor

Socket bugs hide in the gap between what the protocol guarantees and what you assumed. An AI-native editor that reads your whole networking layer helps catch the mismatches — a UDP path that assumes ordering, a TCP write that needs TCP_NODELAY — before they ship.

Free tier; Pro at $20/mo

Try Cursor

Affiliate link · We earn a commission at no cost to you.

FAQ

Is UDP always faster than TCP?
Not in throughput — a healthy TCP connection on a clean link can saturate bandwidth fine. UDP wins on tail latency and jitter, because it never blocks fresh data behind a retransmission. If you measure average throughput on a lossless LAN, the two look similar; the gap appears under loss and load.
Why does HTTP/3 use UDP if UDP is unreliable?
HTTP/3 runs on QUIC, which is built on UDP but rebuilds reliability, ordering, and congestion control per-stream in userspace. The point was to escape TCP's connection-wide head-of-line blocking: in QUIC, a lost packet on one stream doesn't stall the others. It's UDP as a foundation, not raw UDP.
Can I just use TCP everywhere and add TCP_NODELAY to fix latency?
TCP_NODELAY disables Nagle's algorithm, which removes the small-write coalescing delay — useful for chatty request-response traffic. It does not touch head-of-line blocking, which is inherent to the ordered byte stream. For real-time data where late packets are worthless, no TCP socket option helps.

The protocols haven’t changed in decades. What changes is whether you chose the one whose failure mode your application can actually absorb.

Related reading

See all Dev Knowledge articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.