TCP vs UDP: Choosing the Right Transport
TCP and UDP are the two transport-layer protocols that carry almost all internet traffic, and the choice between them shapes the latency, reliability, and complexity of everything you build on top. This guide walks through the concrete trade-offs so you can pick deliberately instead of reaching for TCP by reflex.
Connection-oriented vs connectionless
TCP is connection-oriented. Before any application data flows, the two endpoints establish a connection and agree on shared state: sequence numbers, window sizes, and options like selective acknowledgment. Both sides maintain a state machine (think SYN_SENT, ESTABLISHED, TIME_WAIT) for the life of the connection, and the kernel tracks every byte until it is acknowledged.
UDP is connectionless. There is no setup and no shared state in the protocol itself. You hand the kernel a datagram with a destination address and port, and it sends it. Each datagram is independent; the protocol does not know or care whether the previous one arrived. A UDP header is just 8 bytes (source port, destination port, length, checksum) versus TCP's 20-byte minimum, and that minimalism is the whole point.
Reliability, ordering, and retransmission
TCP gives you a reliable, ordered byte stream. It achieves this with three mechanisms working together:
- Sequence numbers label every byte so the receiver can reassemble data in the order it was sent, even if packets arrive out of order.
- Acknowledgments tell the sender what has been received. Unacknowledged data is retransmitted after a timeout (RTO) or when duplicate ACKs signal a likely loss (fast retransmit).
- Flow and congestion control adapt the sending rate to both the receiver's buffer and the network's capacity, backing off when loss is detected.
UDP provides none of this. There is no acknowledgment, no retransmission, no ordering, and no congestion control. Datagrams can be lost, duplicated, or reordered, and the application will never be told. The only built-in integrity feature is an optional checksum that lets the receiver discard a corrupted datagram, not repair it.
The key insight: UDP does not mean "unreliable in practice, use only when you don't care." It means the protocol delegates reliability decisions to you. If your application needs ordering or retransmission, you implement exactly the parts you need, with the latency and semantics you choose, rather than accepting TCP's one-size-fits-all behavior.
Head-of-line blocking
This is the most underappreciated reason to avoid TCP for certain workloads. Because TCP delivers a single ordered stream, a lost segment blocks delivery of everything sent after it until the retransmission arrives. The later bytes may already be sitting in the receiver's kernel buffer, fully intact, but the application cannot read them because doing so would violate ordering. This is head-of-line (HOL) blocking.
Consider HTTP/2, which multiplexes many independent requests over one TCP connection. At the HTTP layer the streams are independent, but they all share one ordered TCP stream underneath. A single dropped packet stalls every multiplexed stream, not just the request whose data was lost. The application-layer multiplexing cannot escape the transport-layer ordering guarantee.
UDP has no HOL blocking because there is no cross-datagram ordering. A lost datagram affects only the message it carried. Anything built on UDP that wants partial ordering, multiple independent streams, or "deliver whatever you have now" semantics is free to do so.
Handshakes and overhead
TCP opens with a three-way handshake before a single byte of application data moves:
Client Server
| --- SYN -------> |
| <-- SYN-ACK ---- |
| --- ACK -------> |
| --- data ------> | (data may piggyback on the final ACK)
That is one full round trip of latency before you can send a request. Add TLS on top and a classic handshake costs another one to two round trips. On a connection with 100 ms RTT, you can spend 200-300 ms just shaking hands before the first request leaves. Closing is similarly chatty (FIN/ACK in both directions), and the TIME_WAIT state holds resources afterward.
UDP has zero handshake. The first datagram is the data. For short, latency-sensitive exchanges this difference dominates. A DNS query and its answer are typically one datagram each; running that over TCP would mean a handshake costing more round trips than the actual lookup.
Concrete use cases
Where TCP wins
- HTTP/1.1 and HTTP/2: Web pages, APIs, and file transfers need every byte intact and in order. Latency from a handshake is amortized over many requests on a kept-alive connection.
- File transfer, SSH, database protocols: Anything where a missing or reordered byte corrupts the result. You want TCP's guarantees and you want them maintained by the kernel.
- Email (SMTP), most messaging backends: Long-lived, correctness-critical, not microsecond-sensitive.
Where UDP wins
- DNS: Small request/response that fits in one datagram. Retrying a lost query is cheaper than maintaining a connection. It falls back to TCP only for large responses (e.g. zone transfers or big DNSSEC records).
- VoIP and real-time audio: A 20 ms audio frame that arrives late is useless. Retransmitting it would only add jitter. Better to drop it and let the codec conceal the gap. UDP plus RTP is the standard stack.
- Online gaming: Player position updates are sent many times per second. A lost update is obsolete by the time it could be resent, so games send fresh state rather than retransmit stale state. Many use UDP with a thin custom reliability layer for the few messages that must arrive (e.g. "player fired weapon").
- Live and real-time video: Similar to audio, low-latency streaming tolerates loss with concealment. (Note that buffered on-demand video like typical adaptive HTTP streaming runs over TCP, because there latency budget is generous and integrity matters more.)
- Service discovery, telemetry, NTP: Broadcast/multicast and high-frequency small messages where occasional loss is acceptable.
A useful heuristic: if a late packet is still valuable, lean toward TCP; if a late packet is worthless, lean toward UDP.
Where QUIC fits
QUIC is a transport protocol built on top of UDP that reclaims TCP's guarantees while fixing its structural weaknesses. It is the basis of HTTP/3. The design goals address exactly the pain points above:
- No transport-level HOL blocking: QUIC carries multiple independent streams, and loss on one stream does not stall the others. This is the fix HTTP/2-over-TCP could never have.
- Faster handshakes: QUIC integrates the cryptographic handshake with the transport handshake, typically establishing a secure connection in one round trip, and supporting 0-RTT resumption for repeat connections.
- Connection migration: A QUIC connection is identified by a connection ID, not the four-tuple of IPs and ports, so it can survive a client changing networks (Wi-Fi to cellular) without a full re-handshake.
QUIC lives in user space rather than the kernel, which lets it evolve far faster than TCP, whose behavior is frozen into operating systems and middleboxes. The trade-off is higher CPU cost per packet and reliance on UDP not being throttled or blocked by intermediate networks, which still happens in some environments.
Practical takeaway
Default to TCP for correctness-critical, request/response, and bulk-transfer workloads; the kernel does the hard reliability work and a kept-alive connection amortizes the handshake. Reach for UDP when latency beats completeness and stale data is worthless, accepting that you will build the slice of reliability you actually need. And when you want TCP-grade guarantees without HOL blocking or slow handshakes, especially for web traffic, QUIC (HTTP/3) is now the production-ready answer. Choose based on whether a late packet still has value, not on which protocol feels safer.
← All articles