HTTPhttp/3quicudptls 1.30-rtt

HTTP/3 and QUIC

HTTP/3 runs over QUIC, a UDP-based transport that folds TLS 1.3 into its handshake, gives each stream independent loss recovery, and survives a change of IP address — reducing, not eliminating, head-of-line blocking, at the price of userspace CPU and UDP-hostile networks.

ConceptualBrowser
Interview question
Progress

The problem

HTTP/2 multiplexed streams onto one TCP connection and then discovered TCP does not know about streams: one lost packet stalls all of them, and TCP plus TLS still costs two round trips to set up. TCP lives in every kernel and every middlebox on Earth and cannot be changed quickly. How do you fix the transport without waiting for the world’s kernels to upgrade?

The ladder: HTTP/3 → QUIC → UDP → IP

QUIC (RFC 9000, 2021) is a new transport built in userspace on top of UDP. UDP provides nothing but ports and a checksum (UDP: Datagrams and the Contract You Choose), and that is the point: every kernel and middlebox already forwards UDP, so a transport implemented inside the browser or the server library can be shipped and changed with an application update instead of an OS release. QUIC re-implements what TCP does — reliability, ordering, congestion control, flow control — but per stream, and with TLS 1.3 built into the handshake instead of layered on top.

HTTP/3 (RFC 9114) is HTTP semantics mapped onto QUIC streams: each request/response is one bidirectional QUIC stream, and QUIC, not HTTP, owns multiplexing and reliability. Header compression is QPACK rather than HPACK, redesigned so that streams do not have to wait on each other for dynamic-table updates. From the application’s point of view nothing changed: the same GET /users/42, the same status codes, negotiated by the same fetch().

HTTP/3 keeps HTTP; QUIC replaces TCP + TLS as one layer
  1. HTTP/3Same semantics as HTTP/1.1 and /2; QPACK header compression; one request per QUIC stream
  2. QUICStreams, per-stream loss recovery, congestion control, flow control, connection IDs, TLS 1.3 handshake inside
  3. UDPPorts and a checksum; no state in the kernel or in middleboxes
  4. IPUnchanged; routers see UDP/443 packets with encrypted payload

Handshake: 1 RTT, or 0

A cold TCP + TLS 1.3 connection costs two round trips before the request: one for SYN/SYN-ACK, one for the TLS exchange. QUIC carries the TLS 1.3 handshake messages in its own CRYPTO frames inside the first packets, so transport and cryptographic setup complete together in one round trip. With a session ticket from a previous connection, a returning client can send 0-RTT data — the request in the very first packet — under the same rules and the same replay caveat as TLS 1.3 early data: only for idempotent requests, and the server may reject it (The TLS Handshake).

Because the handshake is TLS 1.3, QUIC connections are always encrypted, including almost all of the transport header. Packet numbers, ACK frames and stream ids are invisible to the network. That is deliberate: TCP’s plaintext headers let middleboxes make assumptions (rewriting options, dropping unknown flags), which ossified TCP so badly that extensions like multipath or larger initial windows took a decade to deploy. QUIC exposes only what routers need (connection ID and a version) so that the protocol can keep changing.

Round trips to first request byte on a cold connection
StackTransport handshakeCrypto handshakeTotal before requestReturning client
HTTP/1.1 or /2 over TCP + TLS 1.21 RTT2 RTT3 RTT2 RTT (resumption)
HTTP/1.1 or /2 over TCP + TLS 1.31 RTT1 RTT2 RTT2 RTT, or request in TLS 0-RTT data
HTTP/3 over QUICcombinedcombined1 RTT0 RTT (replayable early data)

Streams with independent loss recovery

Conceptual

In QUIC, reliability is tracked per stream, and the unit of loss detection is the QUIC packet, not the stream’s byte position. When a packet carrying a piece of stream 7 is lost, stream 7 waits for the retransmission — but packets carrying streams 3 and 5 are delivered to the application immediately, because nothing about their ordering depends on stream 7. Packet numbers are never reused (a retransmission gets a new number), which removes TCP’s retransmission ambiguity and gives cleaner RTT estimates; ACK frames carry ranges, so a receiver can describe complex loss patterns in one frame.

This is why the accurate claim is that QUIC reduces head-of-line blocking to the stream that actually lost data — not that it eliminates it. Within a single stream, ordering is still enforced and a lost packet still stalls the bytes behind it, exactly as in TCP. A single large response on one stream gains nothing. And shared state can reintroduce cross-stream waits: QPACK’s dynamic table is updated on a separate encoder stream, and a header block that references an entry the decoder has not yet received blocks until it arrives — the design bounds this (encoders can choose not to reference in-flight entries) but does not make it impossible. Congestion control is also per connection: a loss on any stream shrinks the congestion window for all of them (Congestion Control: Protecting the Network, Head-of-Line Blocking).

QUIC also has per-stream and per-connection flow control like HTTP/2, plus a limit on the number of concurrently open streams, all advanced with dedicated frames — the same window-management bugs apply.

  • Lost packet → only streams with data in that packet wait; others continue.
  • Within one stream, delivery is still in order: HoL blocking inside a stream is unchanged.
  • Congestion window is shared by all streams on the connection.
  • QPACK avoids most cross-stream header dependencies; it cannot forbid all of them.

Connection migration and the real-world costs

Browser

A TCP connection is identified by the 4-tuple (source IP, source port, destination IP, destination port); if the client’s IP changes — a phone walking out of Wi-Fi coverage onto LTE, a laptop switching networks, a NAT rebinding a port after an idle period (NAT: Many Private Hosts Behind One Public Address) — every TCP connection dies and HTTP/2 loses all its streams. A QUIC connection is identified by connection IDs chosen by each endpoint and carried in the packet header. When a client’s address changes it keeps sending with the same connection ID from the new address; the server validates the new path with a challenge and continues the connection — streams, keys and congestion state intact (congestion state is reset for the new path). The download continues; no reconnect, no re-handshake, no lost request.

The costs are concrete. CPU: QUIC runs in userspace, so every packet crosses the syscall boundary and is encrypted, acknowledged and paced by application code; without kernel offloads (UDP GSO/GRO, segmentation offload) operators have measured roughly two to three times the CPU per byte of TCP + kTLS, and even with them QUIC generally costs more. Networks: some corporate and institutional firewalls block or rate-limit UDP/443, some NATs expire UDP mappings after 30 s so idle connections need keep-alive PINGs, and a few middleboxes mangle UDP packets over ~1200 bytes — which is why QUIC’s initial packets are padded to exactly 1200 and path MTU discovery is per connection.

Discovery and fallback handle the hostile cases. A browser’s first connection to a site is TCP; the HTTP/2 response carries Alt-Svc: h3=":443"; ma=86400, and subsequent connections try QUIC, often racing it against TCP and falling back if the UDP path fails. The HTTPS DNS record can advertise h3 up front (DNS Record Types and What They Are For). Servers that terminate HTTP/3 at a CDN or load balancer usually speak HTTP/1.1 or /2 to the origin, so an application may never see a QUIC packet and still benefit from it on the last mile.

How a client learns HTTP/3 is available (response to the first, TCP-based request)
HTTP/2 200
alt-svc: h3=":443"; ma=86400
server: cloudflare

# and/or, before any connection, in DNS:
example.com.  300  IN  HTTPS  1 . alpn="h3,h2" ipv4hint=203.0.113.10

Key points

  • HTTP/3 = HTTP semantics over QUIC; QUIC = a userspace transport over UDP that includes TLS 1.3, streams, reliability, congestion and flow control.
  • QUIC’s handshake completes transport and crypto setup in one round trip, and 0-RTT for returning clients — with the same replay caveat as TLS 1.3 early data.
  • Loss recovery is per stream: a lost packet stalls only the streams whose data it carried. HoL blocking is reduced, not eliminated — in-stream ordering, shared congestion control and QPACK dependencies remain.
  • Connection IDs, not the IP 4-tuple, identify a connection, so it survives Wi-Fi → cellular handoffs and NAT rebinding.
  • Almost the whole header is encrypted to prevent middlebox ossification; routers see only connection ID and version.
  • Costs: more CPU per byte in userspace, UDP blocked or throttled on some networks, NAT timeouts, MTU quirks; clients discover h3 via Alt-Svc or the HTTPS DNS record and fall back to TCP.
  • CDNs and load balancers typically terminate HTTP/3 and speak HTTP/1.1 or /2 to origins.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why build on UDP instead of a new IP protocol number?

A new transport protocol (SCTP tried) is dropped by NATs and firewalls that only understand TCP and UDP, and needs kernel support on every device. UDP passes everywhere today and userspace can implement anything on top of it.

Why encrypt the transport headers, not just the payload?

Middleboxes that could read TCP headers began depending on them and dropping anything unfamiliar, which froze TCP’s evolution. Hiding QUIC’s headers means the network cannot grow dependencies on them, so the protocol can keep changing.

Why does QUIC not eliminate head-of-line blocking?

Because the application still wants each stream in order, and the streams share one congestion window and one QPACK table. QUIC scopes the wait to the stream that lost data; it cannot deliver stream bytes out of order without breaking the stream abstraction.

Why is a userspace transport acceptable when it costs more CPU?

For latency-sensitive traffic across lossy last-mile links the round trips saved and the stalls avoided are worth the cycles, and the cycles are spent at CDN edges built for it. For a bulk transfer inside a data centre they are not, which is why HTTP/3 is not universal.

How it fails

What the failure looks like from inside real software.

  • HTTP/3 works at home and fails on the office network: UDP/443 is blocked; the browser silently falls back to h2 and only the latency changes — unless Alt-Svc caching causes a stall before fallback.
  • Idle QUIC connections through a NAT die after ~30 s without keep-alive; the next request pays a full handshake or times out on a dead path.
  • A QUIC-terminating edge speaking one HTTP/1.1 connection to the origin serialises what the client multiplexed; the origin hop becomes the bottleneck.
  • CPU on an HTTP/3 server two to three times higher than the same traffic on h2 because UDP GSO/GRO offloads are unavailable or disabled.
  • 0-RTT POST accepted by an application unaware of replay: duplicated side effects.
  • MTU: a path that cannot carry 1200-byte UDP packets blocks the handshake entirely; tcpdump shows client Initials with no reply.