Frontend Labs
⋯

39. HTTP/1.1 vs HTTP/2 vs HTTP/3 (QUIC)

39.1 The problem each version solves

HTTP/1.1 HTTP/2 HTTP/3
Year 1997 2015 (RFC 7540 / 9113) 2022 (RFC 9114)
Transport TCP (usually + TLS) one TCP + TLS connection QUIC over UDP (+ TLS 1.3 built in)
Concurrency 1 request per connection at a time; browsers open ~6 per origin; pipelining exists but is broken by intermediaries streams multiplexed on one connection; interleaved frames streams multiplexed without TCP HOL blocking
Head-of-line blocking at HTTP layer (request N blocks N+1 on that connection) at TCP layer (one lost segment stalls all streams) eliminated — each QUIC stream is independently reliable; a loss only stalls its own stream
Header compression none (verbose, repeated) HPACK QPACK
Server push no yes (largely deprecated in practice) yes (rare)
Connection setup TCP + TLS = 2–3 RTT same 1-RTT, or 0-RTT resumption; combines transport+crypto handshake
Connection migration no (new IP = new connection) no yes — a Connection ID survives IP/port change (Wi-Fi↔cellular)
Prioritization via connection count stream priority tree (H2) / scheme (RFC 9218) RFC 9218

39.2 HTTP/1.1 mechanics you still deal with

  • Persistent connections (Connection: keep-alive, default in 1.1). Reuse avoids repeated TCP+TLS handshakes — huge.
  • Chunked transfer encoding (Transfer-Encoding: chunked) streams a body of unknown length; each chunk is <hex size>\r\n<data>\r\n, terminated by a 0\r\n\r\n.
  • Content-Length vs chunked mismatch, or two Content-Length headers, or CL + TE together → request smuggling when a front proxy and back server disagree on where the request ends (Ch. 52 relevance). Normalize at the edge.
  • Pipelining (send request 2 before response 1 returns) is allowed but disabled in every major browser because buggy proxies mis-associate responses.
  • Connection limits: ~6 per origin per browser → domain sharding was a common (now counterproductive with H2) hack.

39.3 HTTP/2 mechanics

  • Everything is a frame (HEADERS, DATA, SETTINGS, WINDOW_UPDATE, RST_STREAM, PING, GOAWAY) tagged with a stream ID (odd = client- initiated). Frames from many streams interleave on the one TCP connection.
  • Flow control exists again at the HTTP/2 layer (per-stream and per-connection WINDOW_UPDATE) — independent of TCP's window. A stuck reader on one stream can stall via HTTP/2 flow control even if TCP is fine.
  • HPACK keeps a synchronized header table both sides; repeated headers cost ~1 byte. Dynamic-table desync or a hostile peer → the HPACK bomb DoS.
  • The TCP HOL problem: all streams ride one TCP connection, so a single lost TCP segment blocks delivery of every stream's bytes behind it. On a clean network H2 crushes H1; on a lossy network (mobile) H2 can be worse than H1's 6 connections (which fail independently). This is the entire motivation for HTTP/3.
  • GOAWAY: graceful connection shutdown telling the client the highest stream it will process — clients replay higher streams on a new connection. Load balancers use this for draining.

39.4 HTTP/3 / QUIC mechanics

QUIC (RFC 9000) is a full reliable, multiplexed, encrypted transport in userspace, over UDP:

  • Streams are independent: loss on stream 3 doesn't delay stream 7. Solves TCP HOL blocking.
  • Crypto is integral: the transport and TLS 1.3 handshakes are one; 1-RTT setup, 0-RTT resumption.
  • Connection ID: the connection is identified by a CID in the QUIC header, not the 4-tuple. Change networks (new IP/port) and the connection migrates seamlessly — great for phones. Load balancers must route by CID, not 5-tuple.
  • Always encrypted, including most of the transport header — middleboxes can't inspect/mangle sequence numbers, so QUIC can evolve ("ossification" resistance). Only a few bits are visible.
  • Its own congestion control (NewReno/CUBIC/BBR reimplemented in userspace), ACK ranges (like SACK but richer, no separate "cumulative" limitation), PLPMTUD (no ICMP dependency).
  • Costs: UDP is more expensive per-packet in many kernels/NICs (less offload — improving with UDP GSO/GRO and hardware QUIC offload); some networks rate-limit or block UDP/443 → clients fall back to HTTP/2. QUIC amplification limits: a server may send at most ~3× the bytes it received from an unvalidated client address.

39.5 How a browser picks a version

  1. Connects via TCP+TLS, ALPN offers h2, http/1.1 → gets HTTP/2 or 1.1.
  2. Server response includes Alt-Svc: h3=":443"; ma=86400 or the DNS HTTPS record advertises alpn=h3 → browser tries QUIC/HTTP/3 on the next navigation and caches that.
  3. If UDP/443 is blocked/broken → falls back to h2 (Happy-Eyeballs-style racing in modern browsers).

39.6 War story

A news site enabled HTTP/2 and saw p75 page-load improve but p95 (mobile users) regress by 20%. On lossy cellular, all 30 sub-resources shared one TCP connection, and each packet loss stalled the whole pipe (TCP HOL). Rolling out HTTP/3 (via Alt-Svc) recovered p95 because QUIC streams fail independently, and connection migration cut reconnects during cell handovers. Lesson: HTTP/2 trades many-connection resilience for single-connection efficiency; on lossy links that trade can lose. HTTP/3 is the fix.

Try it yourself

curl -sI --http2 https://example.com | grep -i '^HTTP\|alt-svc'
curl -sI --http3 https://cloudflare.com | head -1        # needs curl built with HTTP/3
dig example.com HTTPS +short                               # alpn=h3 advertised?
nghttp -nv https://example.com                             # HTTP/2 frame-level trace
# See which protocol Chrome used: DevTools > Network > Protocol column (h2 / h3 / http/1.1)

Generated from real-life-example.md · 8 Oct 2026 16:36