HTTP/1.1 → 2 → 3: why the protocol keeps changing
The same request, three transports. Why HTTP/1.1 could only have one request in flight per connection and the hacks that bred, how HTTP/2 multiplexed everything over one TCP stream but inherited TCP's head-of-line blocking, and why HTTP/3 threw out TCP entirely for QUIC over UDP — per-stream loss recovery, a folded-in TLS handshake, and a connection that survives switching from wifi to 4G.
HTTP/1.1 → 2 → 3: why the protocol keeps changing
Open your network tab, right-click the column headers, and enable "Protocol." You'll see http/1.1, h2, and maybe h3 scattered across the same page. Three different protocols, all carrying the exact same thing — a method, a path, some headers, a body. The request the server sees is identical across all three. What changes is the transport underneath: how those bytes get framed, how many can be in flight at once, and what happens when a packet drops.
You already know the request format from what happens when you fetch(), and you know from TCP that a fresh connection costs a round trip to open and starts slow. This lesson is about why HTTP has been rewritten twice on top of that same TCP foundation — and then, in HTTP/3, why it abandoned TCP altogether.
HTTP/1.1: one lane, one car at a time
HTTP/1.1's model is brutally simple: a connection carries one request at a time. You write your request bytes, then you wait for the entire response before the connection is free to carry the next request. It's a single-lane road where the car ahead must clear before the next can enter.
The obvious fix was pipelining — send request 2 without waiting for response 1's bytes. HTTP/1.1 technically allowed it, and it mostly didn't work. The responses still had to come back in the exact order the requests went out (there's no request ID on the wire to match them up), so a slow first response blocked every response queued behind it — application-level head-of-line blocking. Worse, half the proxies and load balancers of the era mishandled pipelined requests, corrupting or reordering them. Browsers tried it, got burned, and shipped it disabled. Pipelining is the great "it's in the spec but nobody uses it" cautionary tale.
So how does a page load 60 resources over a protocol that does one-at-a-time? The browser opens multiple connections — capped at roughly 6 per host. Six parallel single-lane roads. That cap is the origin of a whole generation of front-end performance hacks:
- Spriting — glue 30 icons into one image so they cost one request instead of 30, then crop with CSS
background-position. - Concatenation — bundle all your JS into one file, all your CSS into another, to spend fewer of your 6 lanes.
- Domain sharding — serve assets from
img1.example.com,img2.example.com,img3.example.comto trick the browser into opening 6 connections per shard, multiplying your parallelism past the cap.
Every one of these exists to work around "6 connections, one request each." Hold that thought — HTTP/2 makes most of them not just unnecessary but actively counterproductive.
HTTP/2: many streams, one connection
HTTP/2 keeps the same requests and responses but changes how they're packaged. Two ideas do the heavy lifting.
Binary framing. HTTP/1.1 is text you can read with your eyes; a message ends when the headers' blank line and Content-Length say so. HTTP/2 chops everything into binary frames, each tagged with a stream ID. A stream is one request/response pair. Because every frame carries its stream ID, frames from different streams can be interleaved on the wire — a few frames of stream 1, then some of stream 3, then back to stream 1 — and the receiver reassembles each stream by its ID.
That interleaving is multiplexing, and it's the whole point: dozens of requests and responses share one TCP connection, in flight simultaneously, in any order. No head-of-line blocking at the application layer, because responses no longer have to come back in request order — each stream is independent. The 6-connection workaround evaporates. So does the reason for spriting, concatenation, and sharding — in fact domain sharding now hurts, because it splits your traffic across multiple connections and defeats the single-connection multiplexing you wanted.
HPACK header compression. Every HTTP request drags a pile of repetitive headers — the same User-Agent, Accept, Cookie, Authorization on request after request. In HTTP/1.1 you resend all of it, uncompressed, every time. HTTP/2's HPACK keeps a shared table of headers both sides have seen, so a repeated header collapses to a tiny index reference instead of hundreds of bytes. On a page firing 80 requests to the same host, that's real savings.
Sounds like the end of the story. It isn't — and the flaw is one you already met.
HTTP/3: throw out TCP
You cannot fix TCP's head-of-line blocking from inside TCP — the strict ordering is TCP. TCP is also implemented in the operating-system kernel, so evolving it means shipping OS updates to the entire planet, which takes a decade. So HTTP/3 does something radical: it stops using TCP.
HTTP/3 runs on QUIC, a new transport built on top of UDP. UDP is the internet's other basic transport — connectionless, unordered, unreliable, essentially "IP with port numbers." It offers none of TCP's guarantees. That's the point: QUIC gets a blank slate and rebuilds reliability, ordering, and congestion control itself, in userspace (in the browser and server application, not the kernel) — so it can be smarter about streams and can evolve without kernel upgrades.
Three consequences matter to you:
- Per-stream loss recovery. QUIC understands streams natively. A lost packet only stalls the stream whose data it carried; every other stream keeps flowing. This is the fix — the HTTP/2 problem simply cannot happen, because QUIC's streams have independent delivery, not one shared ordered buffer.
- A folded-in handshake. QUIC integrates TLS 1.3 directly into its connection setup instead of stacking TLS on top of a separate TCP handshake. A cold QUIC connection is up in one round trip; a resumed one can send request data in the first packet — 0-RTT. Compare that to HTTP/1.1 or HTTP/2, which pay a TCP handshake and then a TLS handshake — 2+ round trips before byte one.
- Connection migration. A TCP connection is welded to its 4-tuple (source IP, source port, dest IP, dest port) — change your IP and the connection is dead. QUIC instead tags each connection with a connection ID that lives above the IP address. Switch from wifi to 4G — new IP, new port — and the connection survives, identified by that ID, with no new handshake. For a phone that changes networks constantly, this is the headline feature.
The same request, three transports
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC over UDP |
| Concurrent requests / connection | 1 (pipelining failed) | many (multiplexed streams) | many (multiplexed streams) |
| Wire format | text | binary frames | binary frames |
| Header compression | none | HPACK | QPACK |
| Head-of-line blocking | app-layer + per-connection | TCP-level, blocks all streams | none (per-stream recovery) |
| Cold-connection setup | TCP + TLS (2+ RTT) | TCP + TLS (2+ RTT) | QUIC+TLS 1.3 folded (1 RTT, 0-RTT resumed) |
| Survives IP change | no | no | yes (connection ID) |
| Typical browser workaround | 6 conns/host, sprites, sharding | none needed (sharding hurts) | none needed |
Read the table as a chain of fixes: HTTP/2 fixed HTTP/1.1's concurrency, HTTP/3 fixed the transport-level blocking HTTP/2 couldn't touch. Each rewrite removes round trips and removes stalls — the two things that actually cost your users time.
Go deeper
- High Performance Browser Networking — "HTTP/2" — The rigorous chapter on streams, framing, and HPACK; it walks the binary layer in the depth this lesson only sketches, and explains exactly why the HTTP/1.1 hacks became anti-patterns.
- Cloudflare — "HTTP/3: the past, present, and future" — From a company that runs QUIC at global scale; the clearest explanation of why per-stream recovery and connection migration required leaving TCP behind.
- curl — HTTP/3 how-to (curl --http3 -v) — Run curl -v --http3 against a site yourself and watch the QUIC handshake in the verbose output — the fastest way to make h3 stop being a table row and start being real.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- HTTP/1.1 allowed pipelining but browsers shipped it disabled. Give the two independent reasons it failed, and explain why the 6-connections-per-host cap was the workaround that stuck.
- Domain sharding sped up HTTP/1.1 sites but slows down HTTP/2 sites. Explain the mechanism in each case — what sharding does for you under 1.1 and what it does *to* you under 2.
- One TCP packet is lost on an HTTP/2 connection carrying 20 streams. How many streams stall, and why? Contrast with what happens to those same 20 requests spread across 6 HTTP/1.1 connections.
- HTTP/3 runs on UDP, which guarantees nothing — no ordering, no reliability. Why is starting from that weaker transport the thing that lets QUIC avoid the head-of-line blocking that HTTP/2 could not?
- A phone switches from wifi to 4G mid-download. Explain why the TCP connection dies but a QUIC connection survives, in terms of what actually identifies each connection.
- A cold HTTP/2 request and a cold HTTP/3 request both need TLS. Why does HTTP/3 reach first-byte in fewer round trips, and what does a *resumed* QUIC connection do that no TCP-based HTTP can?