HTTP/1.1 ran on TCP. HTTP/2 ran on TCP. HTTP/3 does not — it runs on QUIC, which runs on UDP. The shift off TCP is not arbitrary; it solves four production problems that have dogged the web for two decades, and trades new complexity for the user-visible wins.
This topic is what changes — and what stays the same — when HTTP moves to UDP.
Why HTTP Left TCP
TCP has served HTTP well for thirty years. Four problems compound as the network gets faster and the wire gets more hostile:
- Head-of-line blocking — HTTP/2 multiplexes multiple streams on one TCP connection. A single dropped packet stalls all streams, because TCP reassembles in order. The gain of multiplexing at the HTTP layer is undone by losses at the TCP layer.
- Connection setup latency — HTTP/2 over TLS over TCP needs
1 × RTTfor the TCP handshake plus1 × RTTfor the TLS handshake. On a satellite or mobile latency of 200ms, that’s 800ms before the first byte. - No connection migration — A phone switching from Wi-Fi to cellular drops its TCP connection, because a TCP connection is identified by a 4-tuple including IPs; the moment the IP changes, the connection is dead. Every TCP-aware application must reconnect.
- Stuck TLS — TLS lives between the application and TCP. Many higher-layer features (encryption of headers, reprioritisation) are limited by where TLS sits in the stack.
QUIC fixes each by moving the transport logic to UDP and rebuilding the features on top — with TLS built in, not bolted on.
QUIC’s Stream Model
A QUIC connection carries multiple streams, each independently sequenced and reliably delivered. A packet loss on one stream does not block the others — the receiver can hand the unaffected streams to the application immediately.
| Property | TCP + HTTP/2 | QUIC + HTTP/3 |
|---|---|---|
| Multiplexing | HTTP/2 multiplexes over a single in-order TCP stream | QUIC multiplexes over independent streams from the start |
| Head-of-line blocking | One loss stalls everything | One loss stalls only the affected stream |
| Packet numbering | Monotonic; lost packets inferred only via gaps | Monotonic AND explicit ack ranges; faster loss detection |
| Acknowledgements | Cumulative acks; retransmit ambiguity with delays | Explicit ack ranges; no ambiguity |
| Congestion control | Implementable at the kernel, mostly Cubic | Implementable at the application; per-connection state |
The “no head-of-line blocking” is the user-visible win — under packet loss, both HTTP/2 and HTTP/3 send the same data; only HTTP/3 lets the unaffected streams through.
TLS 1.3 Baked In
QUIC does not run over TLS — QUIC integrates TLS. The TLS 1.3 handshake messages are carried in QUIC’s own frames, and the QUIC packet protection keys come from the TLS handshake directly.
| Property | TCP + TLS | QUIC + TLS 1.3 |
|---|---|---|
| Layer separation | TCP below; TLS above; HTTP above TLS | All integrated |
| Header encryption | TCP headers in the clear; middleboxes can read them | Almost all QUIC header fields encrypted |
| Middlebox ossification | TCP extension space is frozen by middleboxes that reject unknown options | QUIC’s UDP framing leaves nothing for middleboxes to ossify |
The encryption of headers is a feature, not a side-effect: it stops middleboxes (NB: by liberal definition, including corporate proxies and bad firewalls) from ossifying on undocumented fields. TCP’s option space was largely frozen twenty years ago because inserting an unknown option goes unrecognised by middleboxes — QUIC dodges this by encrypting everything except the bits the wire needs.
0-RTT and Resumption
TLS 1.3 introduced 0-RTT resumption: a client that has previously talked to a server can carry a resumption ticket, and on a new connection send application data in its very first flight — no handshake round-trip at all.
QUIC goes further: the 0-RTT data is multiplexed as a stream, so the application’s first request is on the wire in the first packet. The user-visible effect is that a returning client to a familiar site feels instant: zero round-trips before the page starts.
| Mode | Handshake | First byte of app data |
|---|---|---|
| TCP + TLS 1.2 | 3 RTT (1 × TCP + 2 × TLS 1.2) | ~3 × RTT |
| TCP + TLS 1.3 | 1-RTT (TCP handshake) + 1 × RTT (TLS handshake) | ~2 × RTT |
| QUIC + TLS 1.3 (cold start) | 1 × RTT (combined QUIC + TLS handshake) | ~1 × RTT |
| QUIC + TLS 1.3 (0-RTT resumption) | 0 RTT — data in first flight | 0 × RTT |
The risk of 0-RTT: replay attacks. A 0-RTT request is a captured and replayable artefact, because it was sent without a fresh handshake. The discipline: only allow 0-RTT for idempotent operations (GETs, configured safe POSTs); reject 0-RTT for state-changing operations.
Connection Migration
A QUIC connection identified by a connection ID, not by the 4-tuple of addresses. The connection ID is carried in every QUIC packet; the IP can change mid-connection and the connection survives.
phone on Wi-Fi phone on cellular
src 192.168.1.30:5000 src 100.64.10.20:5000
cid 0x1234 cid 0x1234 ← same connection id
↓ ↓
Server sees packets with cid=0x1234 from two source IPs;
it confirms they are the same connection.
The user-visible effect: a phone switching networks mid-stream keeps the connection alive. The application effect: a single download resumes at the offset without re-handshaking, page loads continue without dropping, gRPC streams stay up.
HTTP/3 Framing
HTTP/3 is essentially the HTTP/2 frame set adapted to run over QUIC streams. Request/Response is a pair of QUIC streams (or one bidirectional stream); server push uses a separate stream; the priority tree from HTTP/2 is simplified — PRIORITY_UPDATE frames instead of complex precedence-tree maintenance.
| HTTP/2 over TCP | HTTP/3 over QUIC |
|---|---|
| One TCP connection, many HTTP streams inside | One QUIC connection, many QUIC streams, HTTP frames inside each |
| Header compression with HPACK | Header compression with QPACK (similar but adapted for stream independence) |
| Server push supported | Server push deprecated in many implementations (real-world mis-use) |
The change in header compression (HPACK → QPACK) is the “small print” — QPACK had to be redesigned because HPACK’s ordering constraints assumed the TCP-style in-order delivery that QUIC explicitly avoids for separate streams.
Practice Trajectory
- Use Chrome’s
chrome://net-exportor Wireshark to capture a QUIC handshake (Chrome-to-Google, most sites). Identify the combined QUIC + TLS handshake in a single round trip. - Replay the same request over HTTP/2 (force it via
curl --http2) and HTTP/3 (curl --http3). Measure time-to-first-byte on a fresh connection and on a resumed connection. - Switch your phone from Wi-Fi to cellular mid-stream on a YouTube video. Verify the stream survives; that is connection migration.
- In an application where 0-RTT is enabled, identify which requests are 0-RTT-eligible. Argue which are idempotent and which must be excluded.
- Inspect the
Alt-Svcheader on a response from a major CDN; that is how the server advertises HTTP/3 availability on a different port.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Latency-sensitive user-facing traffic on lossy networks | HTTP/3’s no-head-of-line-blocking matters most on mobile |
| Returning users, instant first byte | 0-RTT resumption; gate state-changing requests |
| Mobile applications that switch networks | Connection migration keeps streams alive |
| Internal service mesh | TCP + HTTP/2 is still fine; QUIC’s wins are at the end-user edge |
| Middleboxes that break QUIC | Some corporate firewalls block UDP entirely; advertise HTTP/3 as Alt-Svc, fall back to HTTP/2 |