HTTP/3 and QUIC: routing around TCP

HTTP/3 is the first HTTP that does not run on TCP. This covers where TCP head-of-line blocking actually hurts, what QUIC moved into user space, how connection migration works, and what the switch costs.

From HTTP/1.0 in 1996 to HTTP/2 in 2015, the protocol changed twice in twenty years while the layer underneath stayed TCP. HTTP/3 is the first version to break that: it runs on QUIC, and QUIC runs on UDP.

Head-of-line blocking: the problem is ordering, not loss

TCP provides one reliable, ordered byte stream. That second word is the whole story: if byte 3 is lost, the kernel cannot hand bytes 4 through 100 to the application even though they arrived. It waits for the retransmission.

The TCP header: sequence numbers, acknowledgements and windows all exist to serve ordered reliability

Image: Daniele Giacomini, CC BY-SA 2.5, via Wikimedia Commons.

HTTP/2 added multiplexing: many streams over one TCP connection, fixing the HTTP/1.1 problem of one request per connection. But it did not touch the ordering constraint — every stream shares one byte stream, so a single lost packet stalls all of them. That is TCP-level head-of-line blocking.

Browsers work around it by opening more connections, but each connection runs its own congestion control, which tends to make things worse on a shared link.

What QUIC does: reliability in user space

QUIC starts from a blunt premise: if TCP semantics cannot be changed, implement a new transport on top of UDP. UDP only offers best-effort datagrams; everything else is built in user space.

Capability On TCP With QUIC
Reliability kernel retransmission user-space retransmission, per-stream numbering
Ordering global; one loss blocks every stream ordered within a stream, streams independent
Encryption TLS on top, two handshakes TLS 1.3 built in, merged into one
Connection identity 4-tuple (src/dst IP + port) Connection ID, decoupled from addresses
Handshake TCP 1-RTT + TLS 1-2 RTT 1-RTT, 0-RTT on resumption

The UDP header is 8 bytes; QUIC implements reliability and encryption inside its payload

Image: Jigs35, CC BY-SA 2.5, via Wikimedia Commons.

Three changes matter most:

Streams are genuinely independent. A lost packet only affects its own stream; the rest keep delivering. This is the most concrete win over HTTP/2, and it shows up hardest on lossy mobile networks.

The handshakes merge. QUIC folds the transport handshake into TLS 1.3, so one round trip is enough to start sending data. On resumption, 0-RTT puts application data in the first packet — at the cost of that packet being replayable, so it is only safe for idempotent requests.

The TLS 1.3 handshake: QUIC embeds this directly in the transport layer instead of stacking two handshakes

Image: Tortue rapide, CC BY-SA 4.0, via Wikimedia Commons.

Connections follow the device, not the IP. My favourite part. A TCP connection is identified by its 4-tuple, so when a phone moves from Wi-Fi to 5G the connection dies. QUIC identifies it with a Connection ID; changing networks means sending a migration frame, and the connection slides to the new address — an in-flight download survives.

Why route around TCP instead of fixing it

The obvious question: why not put these improvements into TCP?

Because TCP lives in the kernel and in middleboxes. Kernel changes wait on OS upgrade cycles, and the harder problem is middleboxes: NATs, firewalls, load balancers and carrier policy boxes have made assumptions about what TCP looks like and how ports are used for decades. A new TCP option is likely to be dropped by some box along the path.

UDP gets completely different treatment: it is forwarded as opaque datagrams and middleboxes mostly leave the payload alone. Putting a new protocol inside UDP is therefore the only path that both iterates fast and traverses the existing network. It is also why QUIC is encrypted so heavily that middleboxes cannot inspect it — that is a design goal, not a side effect.

The costs

QUIC is not free:

  • UDP is often rate-limited or blocked. UDP 443 being blocked on enterprise networks is normal, so browsers all implement “fall back to HTTP/2 when HTTP/3 fails” and remember the failure;
  • Higher CPU cost. A user-space stack plus blanket encryption costs more CPU per unit of throughput than kernel TCP. Hence the push to offload QUIC to the NIC;
  • 0-RTT can be replayed. Idempotent requests only;
  • New operational tooling. Packet captures show encrypted UDP, and debugging looks nothing like before.

My take

The benefit of HTTP/3 depends heavily on network quality. On a clean internal network the difference from HTTP/2 is unmeasurable; on a mobile network with more than 1% loss it is obvious.

So my policy for my own sites: enable HTTP/3, keep HTTP/2 fallback, and do not optimise specifically for it. On a CDN it is usually one config line and the win is free; hand-tuning QUIC parameters and UDP stacks is a much worse trade.

What is worth remembering is the underlying move: when a protocol layer cannot be changed, reimplement it a layer up, where you can iterate freely. That idea is not specific to transports.

← Back to all posts

Comments

…