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:

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

Where UDP wins

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:

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.

networkingtcpudpquicprotocols
← All articles