12

HTTP/1.1 vs HTTP/2 vs HTTP/3

How the web's core protocol evolved to kill head-of-line blocking — and when each version actually matters for your design.

The problem every HTTP version is trying to solve

HTTP (HyperText Transfer Protocol) is the request/response language your browser and servers speak: the client sends a request (GET /page), the server sends back a response. That much has not changed since 1991. What has changed across HTTP/1.1, HTTP/2, and HTTP/3 is the plumbing underneath — specifically, how many requests can share a connection and what happens when one of them is slow.

The villain of this whole story is head-of-line (HOL) blocking: when a queue processes items in order and the item at the front is stuck, everything behind it waits, even if those later items are ready. A modern web page pulls dozens or hundreds of resources — scripts, stylesheets, images, fonts — and HOL blocking is what turns "one slow resource" into "the whole page stalls." Each HTTP version is largely a different, deeper attack on head-of-line blocking.

Keep two distinct layers in mind, because the versions attack HOL blocking at different ones. There is HOL blocking at the HTTP layer (requests waiting on one connection) and HOL blocking at the TCP layer (one lost packet stalling every stream on that connection). HTTP/2 fixes the first; HTTP/3 fixes the second. That is the entire arc of the day.

HTTP/1.1: persistent connections and the pipelining dead end

HTTP/1.1 (1997) is the version that made the modern web usable, and huge amounts of traffic still run on it. Its core model is one request, then one response, per connection at a time. Its two big ideas were persistent connections and pipelining — one succeeded, one failed.

A persistent connection (keep-alive) reuses a single TCP connection for many requests instead of opening a fresh one per request. Opening a TCP connection costs a round trip (and TLS adds more), so reusing it is a large win. Before keep-alive, every image on a page paid a full connection setup.

Pipelining was the attempt to send several requests back-to-back without waiting for each response. It failed in practice because the server still had to return responses in request order — so a slow first response blocked all the ready ones behind it. That is HTTP-layer HOL blocking in its purest form, and pipelining is disabled in effectively every browser.

Because one HTTP/1.1 connection carries one request at a time, browsers worked around it by opening ~6 parallel connections per origin. This helps but is a blunt instrument: connections are expensive (each is its own TCP + TLS handshake and its own congestion-control state), six is an arbitrary cap, and it spawned ugly hacks like domain sharding (splitting assets across img1., img2. subdomains to unlock more parallel connections) and spriting (mashing many small images into one file to cut request count).

HTTP/1.1 traitConsequence
One request at a time per connectionNeeds ~6 connections per origin for parallelism
Pipelining blocked by in-order responsesEffectively unused
Headers sent in full, plaintext, every requestRepeated Cookie/User-Agent bytes on every call
Workarounds: sharding, spriting, inliningComplexity pushed into the app and build

HTTP/2: one connection, many multiplexed streams

HTTP/2 (2015) keeps HTTP's semantics — same methods, same headers, same status codes — but completely rebuilds how bytes move over the wire. The headline is multiplexing: many concurrent requests and responses interleaved over a single TCP connection, with no per-request ordering constraint. The six-connection hack and domain sharding become unnecessary and even counterproductive.

Four mechanisms make this work, and each is worth naming:

  • Binary framing. HTTP/2 stops sending human-readable text lines and instead sends compact binary frames — small typed chunks (HEADERS, DATA, etc.). Frames are the unit that gets interleaved.
  • Streams. Each request/response is a stream with its own id. Frames from many streams are interleaved on the one connection and reassembled by stream id at the far end, so slow stream 1 no longer blocks ready stream 3 at the HTTP layer.
  • HPACK header compression. HTTP headers are repetitive — the same cookies and user-agent on every request. HPACK compresses them with a shared dynamic table so a header sent once is later referenced by a small index instead of resent in full. On header-heavy APIs this is a real bandwidth saving.
  • Stream prioritization. The client can hint that the CSS matters more than a below-the-fold image, so the server allocates bandwidth accordingly.

Multiplexing solves HTTP-layer HOL blocking, but a sharp limit remains, and it motivates HTTP/3 entirely. HTTP/2 still runs over one TCP connection, and TCP guarantees in-order delivery of its own byte stream. If a single TCP packet is lost, TCP holds back all the data behind it until the retransmission arrives — even data belonging to other, unrelated streams. So one dropped packet stalls every multiplexed stream at once. This is TCP-level head-of-line blocking, and no amount of HTTP-layer cleverness can fix it, because the block happens below HTTP.

What happened to server push

HTTP/2 also shipped server push — the server proactively sending resources the client hadn't yet requested (pushing style.css alongside index.html, guessing the client will need it). It sounded great and is now deprecated and removed from Chrome. It was hard to use well: servers pushed resources the browser already had cached (wasting bandwidth), cache interactions were subtle, and the win was marginal versus simpler techniques. The lesson: <link rel="preload"> hints let the client decide what to fetch early, which turned out to beat the server guessing. Do not design around server push.

HTTP/3: HTTP over QUIC, over UDP

HTTP/3 (2022) attacks the one problem HTTP/2 could not: it replaces TCP entirely. Instead of running over TCP, HTTP/3 runs over QUIC — a new transport protocol built on UDP (User Datagram Protocol, the internet's connectionless, no-guarantees packet service). QUIC re-implements the good parts of TCP (reliability, congestion control, ordering) inside itself, but with streams as first-class citizens, so it can do what TCP structurally cannot: keep streams independent.

The concrete wins, each solving something the prior versions could not:

  • No transport-level HOL blocking. Because QUIC understands streams itself, a lost packet only stalls the one stream it belonged to; every other stream keeps flowing. This is the definitive fix for the problem HTTP/2 exposed.
  • TLS 1.3 built in. In HTTP/2-over-TCP you pay a TCP handshake then a TLS handshake — two round trips before any data. QUIC folds encryption into the transport handshake, so a new connection is established and secured in a single round trip. There is no such thing as unencrypted HTTP/3; security is not optional.
  • 0-RTT resumption. When reconnecting to a server seen before, the client can send its first request with the handshake — zero round trips of setup before data flows. (It carries a small replay risk, so it is used only for safe, idempotent requests.)
  • Connection migration. A QUIC connection is identified by a connection ID, not by the IP/port four-tuple TCP uses. So when your phone moves from Wi-Fi to cellular and its IP changes, the connection survives instead of breaking. TCP connections die on an IP change; QUIC ones migrate seamlessly.
Round trips to first byte (new connection)HTTP/2 over TCPHTTP/3 over QUIC
TCP handshake1 RTT
TLS handshake1–2 RTTfolded into transport
Total new connection2–3 RTT1 RTT (0 on resume)

The cost side: QUIC lives in user space (not the OS kernel like TCP), so it uses more CPU per byte, and because it rides UDP, some corporate networks and middleboxes that throttle or block UDP will fall back to HTTP/2. Clients therefore try HTTP/3 and gracefully downgrade.

When each version matters for system design

Version choice is mostly a matter of turning on what your CDN and load balancer already support — but knowing which workloads benefit tells you where to spend attention. The gains concentrate on high-latency, lossy, or many-resource paths.

Practical guidance:

  • Public, global, mobile-heavy traffic benefits most from HTTP/3 — poor networks with packet loss and IP changes are exactly where independent streams, single-RTT setup, and connection migration pay off. Enable it at your CDN/edge.
  • Many small assets or chatty header-heavy APIs are the sweet spot for HTTP/2 multiplexing and HPACK. If you are still doing domain sharding or spriting, moving to HTTP/2 lets you delete those workarounds.
  • A few large downloads (video, big files) are bandwidth-bound, not connection-bound, so the version barely moves the needle — the bottleneck is throughput, not HOL blocking.
  • Internal service-to-service RPC often uses gRPC, which is built on HTTP/2 specifically for its multiplexed streams; that is HTTP/2's other major home.
  • Backward compatibility is automatic. Version is negotiated per connection (via ALPN in the TLS handshake, or an Alt-Svc header advertising HTTP/3). You do not rewrite your app; you enable versions at the edge and clients pick the best they can reach. Set the advertisement header once:
Alt-Svc: h3=":443"; ma=86400

The through-line: HTTP semantics never changed, so your application code is version-agnostic. What you are choosing is a transport, and the right choice is dictated by how bad the network is and how many things the page fetches.

Key takeaways

  • Every HTTP version is an attack on head-of-line blocking: HTTP/2 fixes it at the HTTP layer, HTTP/3 fixes it at the transport layer.
  • HTTP/1.1 handles one request at a time per connection; pipelining failed (in-order responses), so browsers open ~6 connections and hack around it with sharding and spriting.
  • HTTP/2 multiplexes many streams over one TCP connection via binary framing, stream ids, and HPACK header compression — but a single lost TCP packet still stalls all streams (TCP-level HOL blocking). Server push is deprecated; use client preload hints.
  • HTTP/3 runs over QUIC-on-UDP: independent streams (no transport HOL blocking), TLS 1.3 built in (1-RTT setup, 0-RTT on resume), and connection migration that survives an IP change.
  • Version is a transport choice negotiated per connection; HTTP semantics are unchanged, so app code doesn't change — you enable versions at the CDN/edge.
  • HTTP/3 wins on lossy/mobile/global paths; HTTP/2 wins on many-small-resources and header-heavy APIs; for a few large files the version barely matters.

Checklist

  • [ ] I can explain HOL blocking at both the HTTP layer and the TCP layer, and which version fixes which.
  • [ ] I can say why HTTP/1.1 pipelining failed and why browsers open multiple connections.
  • [ ] I can name HTTP/2's four mechanisms: binary framing, streams, HPACK, prioritization.
  • [ ] I understand why HTTP/2's single TCP connection still suffers HOL blocking on packet loss.
  • [ ] I know why server push was deprecated and what replaced it.
  • [ ] I can list HTTP/3's wins: no transport HOL blocking, 1-RTT/0-RTT, connection migration, built-in TLS 1.3.
  • [ ] I can pick a version for a given workload and enable it at the edge without touching app code.