Hermes Wiki
Developer/Networking/TransportLayer/Fundamentals/tcp-vs-udp-and-why-quic-exists

TCP vs. UDP, and Why QUIC Exists

Concept

OSI layer 4 (transport) decides how bytes actually move between two hosts once the network layer (IP routing) has gotten a packet to the right machine. The two classic choices make opposite bets on the same question: should the network guarantee delivery, or should the application handle that itself in exchange for speed?

TCP is connection-oriented and reliable: it opens a connection with a three-way handshake, numbers every byte sent, retransmits anything the receiver doesn't acknowledge, reorders packets that arrive out of sequence back into the original order, and applies flow control (don't overwhelm the receiver) and congestion control (don't overwhelm the network path) automatically. The application just writes bytes to a socket and TCP guarantees they arrive, in order, exactly once, or the connection reports an error. That guarantee is exactly what almost every application protocol assumes — HTTP, WebSockets over their initial handshake, database wire protocols — because building reliable delivery yourself on top of an unreliable transport is real, easy-to-get-wrong work.

UDP is connectionless and makes no such guarantees: a datagram either arrives or it doesn't, there's no handshake before sending, no retransmission, no ordering guarantee across multiple datagrams, and no built-in congestion control. What UDP buys back for giving up all of that is the absence of TCP's specific cost: no handshake round-trip before data flows, and critically, no head-of-line blocking — since TCP guarantees strict in-order delivery to the application, a single lost packet stalls delivery of every packet sent after it until the retransmission arrives, even if those later packets themselves arrived fine. For real-time media (video calls, live game state) a dropped frame that arrives late is worse than one dropped entirely, so UDP's willingness to just lose data outright is a feature, not a gap — DNS queries and VoIP/gaming protocols build directly on UDP for this reason.

QUIC, which underlies HTTP/3, is the modern answer to a problem neither classic transport solves cleanly: HTTP/2 multiplexes many logical streams over one TCP connection to avoid one slow request blocking others at the application level, but because it's still one TCP connection underneath, a single lost TCP packet still head-of-line-blocks every HTTP/2 stream sharing that connection — the multiplexing gain evaporates exactly when the network is lossy, which is when it matters most. QUIC runs over UDP but reimplements TCP's reliability and congestion control itself, stream-by-stream, entirely in user space: each HTTP/3 stream gets independent ordering and loss recovery, so one lost packet only stalls the one stream it belonged to, not every concurrent request on the connection. QUIC also folds the transport and TLS 1.3 handshakes into a single round-trip (versus TCP+TLS's separate sequential round-trips), and because it runs over UDP rather than TCP, it can be implemented and updated in application/library code without waiting on OS kernel TCP-stack changes — part of why it shipped and iterated as fast as it did.

Tradeoffs

Protocol Reliability Ordering Head-of-line blocking Typical use
TCP Guaranteed (retransmits lost packets) Strict, in-order Yes — one lost packet stalls everything behind it on that connection HTTP/1.1, HTTP/2, database connections, most request/response protocols
UDP None — lost datagrams are just lost None across datagrams No — datagrams are independent DNS, VoIP, live video/gaming, anything where late data is worse than lost data
QUIC (over UDP) Guaranteed, but per-stream rather than per-connection Per-stream ordering, not connection-wide No — loss on one stream doesn't stall other streams sharing the connection HTTP/3, and increasingly any latency-sensitive multiplexed protocol

The trade isn't really "reliable vs. unreliable" as a binary — QUIC shows reliability and freedom from head-of-line blocking aren't mutually exclusive, they're just expensive to get both at once, which is why it took a from-scratch transport design (rather than a TCP tweak) to achieve it.

When to use / when not to

  • Use TCP (the default for virtually all application-level code) whenever the application logic assumes ordered, complete delivery — which is true of essentially all request/response and RPC-style traffic, including most internal service-to-service calls.
  • Reach for raw UDP only when the application can tolerate loss and actively prefers freshness over completeness — real-time audio/video, live multiplayer game state, or protocols like DNS where a lost query is cheaply retried rather than recovered mid-stream.
  • Adopt QUIC/HTTP-3 primarily by choosing infrastructure that already supports it (most CDNs and modern browsers negotiate HTTP/3 automatically) rather than implementing the transport yourself — the win (no cross-stream head-of-line blocking, faster connection setup) matters most on lossy or high-latency networks (mobile, satellite, congested Wi-Fi), less on a stable low-latency internal network where TCP's head-of-line blocking rarely triggers in practice.
  • Don't choose UDP for anything expecting TCP's guarantees "for performance" without re-implementing reliability yourself — that reimplementation is exactly the nontrivial work QUIC did, done properly; a half-built version of it is usually worse than just using TCP.

Common pitfall

Assuming HTTP/2's multiplexing solves head-of-line blocking outright, when it only solves it at the application layer — a single dropped TCP packet still blocks every multiplexed HTTP/2 stream on that connection until it's retransmitted, because there's still exactly one TCP connection underneath. This is precisely the gap QUIC was built to close, and teams debugging "why does our HTTP/2 API get slow on flaky mobile networks" often don't realize the multiplexing they adopted for exactly that scenario doesn't protect them from packet loss the way they assumed.

Engineering Lens

TCP vs. UDP vs. QUIC is a clean instance of a pattern that recurs across the whole stack: a general-purpose guarantee (TCP's total ordering and reliability) is expensive precisely because it's general, and a purpose-built alternative (QUIC's per-stream reliability, or UDP's no-guarantee-at-all) wins by matching the guarantee to what the actual workload needs instead of paying for guarantees the application doesn't use. The same reasoning shows up in database isolation levels and in choosing eventual vs. strong consistency for a distributed system — the strongest guarantee is rarely free, and the right design question is which guarantee this specific workload actually needs, not which one sounds safest by default.

Sources

Hermes Wiki