Skip to main content

HTTP/1.1 to HTTP/2 to HTTP/3 (QUIC)

Introduction

Look how far you've come in three lessons. A name became an address (DNS), a packet learned to cross the planet (IP), and a connection learned to carry your bytes reliably or fast (TCP/UDP). You've arrived at the server's front door with an open connection — and now you finally get to say something. The language you speak is HTTP, the protocol behind essentially the entire web.

But here's the thing: this lesson is not "what is HTTP." HTTP's basic shape — you ask, the server answers — has barely changed in thirty years. What has changed, three times, is something subtler and far more interesting. Because it turns out the whole history of HTTP is a single villain, fought twice:

Head-of-line blocking — when one stuck thing blocks everything waiting behind it. HTTP/2 was an escape from one version of it. HTTP/3 was an escape from another.

That's the frame that makes "HTTP/1.1 vs 2 vs 3" click. It's not three random sets of features to memorize; it's an arc. Each version loads a web page — dozens or hundreds of files — a little faster than the last, by killing a head-of-line bottleneck the previous one couldn't. HTTP/2 killed it at the application layer. HTTP/3 killed it at the transport layer. By the end of this lesson you'll understand exactly what those two bottlenecks are, why each escape works, and — the part that trips up even senior engineers — why "just upgrade to the newest HTTP" can occasionally make your site slower. One enemy, two escapes. Let's meet it.

Illustration of the evolution of HTTP as one enemy fought twice: head-of-line blocking, and its two escapes. Three lanes each load the same web page of many resources. HTTP/1.1 uses about six parallel TCP connections, but within each connection requests are served one at a time, so a slow response blocks everything behind it on that lane, which is application-layer head-of-line blocking. HTTP/2 uses a single TCP connection and multiplexes many streams over it, interleaving their frames so all requests progress together, which escapes the application-layer blocking and removes the six-connection limit; but because it still runs on TCP, which delivers bytes strictly in order, a single lost packet makes TCP hold back every later packet and stalls all of the multiplexed streams at once, which is transport-layer head-of-line blocking. HTTP/3 runs on QUIC over UDP, which implements independent streams natively in the transport with separate loss recovery, so a lost packet stalls only its own stream and the others keep flowing, which escapes the transport-layer blocking; QUIC also folds in TLS 1.3 for a one-round-trip or zero-round-trip handshake and supports connection migration so a download survives switching from Wi-Fi to cellular. A central spine states the theme that HTTP/2 escaped head-of-line blocking at the application layer with multiplexing and HTTP/3 escaped it at the transport layer with QUIC. A note explains that the version is negotiated between client and server through ALPN and Alt-Svc with fallback, and that on a lossy mobile network HTTP/2 can actually be slower than HTTP/1.1 because one lost packet freezes all of its streams.

HTTP Itself: You Ask, the Server Answers

Strip away the version numbers and HTTP is disarmingly simple. Over the connection you just opened, your browser sends a request — a method (GET, POST…), a path (/index.html), some headers (who you are, what you accept) — and the server sends back a response: a status code (200 OK, 404 Not Found), some headers, and usually a body (the HTML, the image, the JSON). Ask, answer, done. That part is the same in HTTP/1.1, /2, and /3 — the semantics never changed.

So if the request-and-response is identical, what on earth is there to have three versions of? This one fact: a modern web page is not one request. It's dozens to hundreds of them. Open a typical site and the browser has to fetch the HTML, then a pile of CSS files, a heap of JavaScript, fonts, and a swarm of images — often 50, 100, 200+ separate resources, all needed to render the page you see. The HTML alone tells the browser about the others, so they cascade in waves.

And that is where the whole drama lives. The question every HTTP version is really answering is not "how do I make one request?" — it's "how do all these hundreds of requests share the connection?" Do they line up single file? Do they go all at once? What happens when one of them gets stuck? Get that sharing wrong and a page that's 200 small files feels sluggish no matter how fast your network is. So keep this picture in mind for the rest of the lesson: not one request on a quiet wire, but a crowd of them all trying to get through at once. How they queue is the entire story.

HTTP/1.1: One Lane, Used Single-File

HTTP/1.1 (from 1997, and still everywhere) made one genuinely important improvement over its ancestor: persistent connections, or keep-alive. Instead of paying for a fresh TCP handshake on every single request, a connection is kept open and reused for many requests in a row. Good — no more handshake tax per file.

But it has a crippling limitation: on any one connection, it handles exactly one request at a time. Request #2 cannot begin until request #1's response has completely arrived. Picture a single supermarket checkout lane: the cashier serves one customer start-to-finish before the next steps up. Now put a customer with a giant, slow cart at the front — a big image, a slow API call — and everyone behind them just waits, even though their items would take two seconds. That waiting is application-layer head-of-line blocking: the thing at the head of the line blocks the whole line behind it. Meet the villain.

The web's workaround was blunt: open more lanes. Browsers learned to open about six parallel TCP connections per origin, so up to six requests could be in flight at once. Six is better than one — but it's still a hard cap, and each of those six is its own handshake and its own slow, cautious start-up. Developers even hacked around that with domain sharding — splitting assets across extra hostnames (img1.site.com, img2.site.com…) to trick the browser into opening more than six connections. It worked, but it was a pile of connections, each single-file, all to dodge one fundamental flaw: HTTP/1.1 can't do real concurrency on a connection. That flaw is exactly what the next version set out to kill.

HTTP/2: One Connection, Everyone at Once

HTTP/2 (2015, born from Google's SPDY) attacked that flaw head-on with one brilliant idea: multiplexing. Instead of one-request-at-a-time, it breaks every request and response into small frames, tags each with a stream ID, and interleaves them all over a single TCP connection. A frame from the CSS, then a frame from an image, then a frame from the JavaScript, then back to the CSS — all woven together on one wire, all making progress at the same time.

Go back to the checkout analogy: HTTP/2 replaces the single-file cashier with one super-cashier who scans one item from every cart in rotation. Nobody waits for the giant cart to finish anymore — everyone's items advance together. In one stroke this kills application-layer head-of-line blocking (the slow response no longer blocks the others) and makes the six-connection workaround obsolete — you can have a hundred streams flowing over one connection. (This is also why domain sharding flipped from clever optimization to anti-pattern: splitting your assets across hostnames now sabotages the single-connection multiplexing and adds needless DNS and TLS handshakes.)

HTTP/2 brought friends, too: HPACK, which compresses the repetitive headers that otherwise bloat hundreds of tiny requests, and stream prioritization so the browser can say "the CSS matters more than that footer image." (There was also server push, letting the server send resources you didn't ask for yet — but it turned out to cause more problems than it solved and has since been removed from browsers, so you can safely forget it exists.)

This was a massive leap, and for a while it looked like the end of the story. The head-of-line monster was dead. Except… it wasn't. It had just moved somewhere the multiplexing couldn't reach.

The Catch: HTTP/2 Still Rides on TCP

Here's the twist that makes this whole lesson worth learning. HTTP/2 multiplexes beautifully — but all those streams still ride on a single TCP connection. And remember what TCP promised, way back in the transport lesson? Strictly in-order, reliable bytes. TCP will not hand the application byte #500 until it has delivered byte #499, no exceptions. That guarantee, so useful for a single file, becomes a trap the moment you multiplex.

Watch what happens when a single packet is lost. That packet carried frames for, say, one image. But TCP doesn't know or care about your streams — it only sees one ordered byte stream. So when a packet goes missing, TCP holds back every packet that arrived after it — packets belonging to the CSS, the JavaScript, ten other images — and refuses to deliver any of them to HTTP/2 until the one lost packet is retransmitted. One dropped packet freezes all the streams at once.

This is transport-layer head-of-line blocking, and it's devious: HTTP/2 evicted the villain from the application layer, and the villain simply took up residence one floor down, in the transport. Back to the checkout: the super-cashier is happy to interleave everyone's items — but they're all riding out on one shared conveyor belt, and if that belt jams, everybody stops, no matter whose item caused it.

And this isn't academic. On a clean network you'll rarely notice. But on a lossy mobile connection — 1 to 5% packet loss, totally normal on cellular — this bites hard, with a genuinely shocking result: HTTP/2 can be slower than HTTP/1.1. Why? Because HTTP/1.1's six separate connections each freeze independently — a loss on one stalls only that lane — while HTTP/2's single connection means one loss stalls everything. The upgrade made things worse. To truly kill head-of-line blocking, you couldn't fix HTTP. You had to replace the transport underneath it.

HTTP/3: Put the Streams in the Transport (QUIC)

HTTP/3 (standardized 2022) makes a move that sounds radical until you see the logic: it throws out TCP. Instead it runs on a new transport protocol called QUIC, which is built on top of UDP — yes, the fast, no-guarantees pipe from the transport lesson. That seems backwards (didn't we want reliability?), but it's exactly right, and here's the trick.

QUIC takes the fast, unopinionated UDP pipe and rebuilds reliability, ordering, and congestion control on top of it — but per stream, not per connection. Each HTTP/3 stream gets its own independent loss recovery inside QUIC. So when a packet is lost, QUIC knows precisely which stream it belonged to, and stalls only that one stream. Every other stream keeps flowing, completely unaffected. The lost image waits; the CSS, the JavaScript, and the other images sail right past it. The conveyor belt isn't shared anymore — every customer gets their own independent belt, so one jam can't spread. That is the escape from transport-layer head-of-line blocking, and it's the whole reason HTTP/3 exists.

Because QUIC was designed fresh, it folds in two more gifts:

  • A faster handshake. QUIC has TLS 1.3 encryption built directly in, so setting up a secure connection takes just one round trip — or zero if you've connected before (0-RTT resumption). Compare that to TCP's handshake plus a separate TLS handshake stacked on top.
  • Connection migration. A QUIC connection is identified by a connection ID, not by your IP address and port. So when you walk out of the coffee shop and your phone switches from Wi-Fi to cellular — a change that instantly kills a TCP connection — your QUIC download just keeps going. The connection survives the network change.

Two escapes, two layers. Let's put the whole arc together, because seeing it as one story is the point.

See It: The Page Load Race

You've heard the story; now watch it, because head-of-line blocking is one of those things that's abstract in words and blindingly obvious the instant you see it stall. Below, the same web page — a batch of resources — loads down three lanes at once: HTTP/1.1, HTTP/2, and HTTP/3.

First, just hit go on a clean network. Watch HTTP/1.1 dribble the files out in serial batches of six, while HTTP/2 and HTTP/3 start everything together — that's multiplexing, killing the app-layer blocking, visible at a glance. Then do the thing that reveals the deeper lesson: raise the packet loss. Now watch closely. HTTP/2 freezes every stream at once the moment a packet drops — there's transport-layer head-of-line blocking, live — while HTTP/3 freezes only the single stream that lost a packet and lets the rest flow. Push it further: flip to a lossy mobile network and watch HTTP/2 actually finish behind HTTP/1.1 — the real-world regression that shocks teams. Same page, three transports, one enemy fought twice. Go make it stall.

The Page Load Race. Fire the same page — a bunch of resources — down three lanes at once: HTTP/1.1, HTTP/2, and HTTP/3. Watch HTTP/1.1 dribble them out in serial batches of six connections, while HTTP/2 and HTTP/3 start everything together (multiplexing). Then do the thing that reveals the whole lesson: raise the packet loss. HTTP/2 FREEZES every stream at once — that's TCP head-of-line blocking, because it's all one ordered connection — while HTTP/3 freezes only the one stream that lost a packet and the rest keep flowing. Flip to a lossy mobile network and watch HTTP/2 finish behind even HTTP/1.1. Same page, three transports, one enemy fought twice.

Two Enemies, Two Escapes

Now zoom out, because everything you just learned collapses into one clean, memorable arc. There was never really "HTTP/1.1 vs 2 vs 3" as three separate things. There was one enemy — head-of-line blocking — appearing at two different layers, and each new HTTP version was the escape from one of them:

The blockingThe escape
HTTP/1.1application-layer HoL — one request at a time per connection; a slow response blocks the rest(workaround: ~6 connections)
HTTP/2✅ escapes app-layer HoL via multiplexing — but inherits transport-layer HoL from TCP: one lost packet stalls every streammultiplex many streams over 1 TCP connection
HTTP/3✅ escapes transport-layer HoL via QUIC — independent per-stream loss recovery over UDPput the streams in the transport

Read it top to bottom and the logic is airtight. HTTP/2 looked at HTTP/1.1's single-file lane and said "let everyone share one lane at once" — killing the blocking at the application layer. But that shared lane was still one ordered TCP connection, so the blocking reappeared one layer down. HTTP/3 looked at that and said "then give every stream its own independent lane inside the transport" — killing it at the transport layer, where TCP couldn't. Each version didn't add random features; it chased the same monster to the next floor down and slammed the door.

Hold onto this frame. "HTTP/2 = multiplexing, HTTP/3 = QUIC" is easy to memorize but easy to forget. "One enemy — head-of-line blocking — escaped first at the app layer, then at the transport layer" is a story, and stories stick. That single sentence is the whole lesson.

HTTP's evolution is one enemy — head-of-line blocking — fought twice. Escape one, HTTP/2 at the application layer: it multiplexes many streams over one TCP connection, so app-layer HoL is gone, but because it still rides TCP the transport-layer blocking remains. Escape two, HTTP/3 at the transport layer: independent streams on QUIC over UDP, so transport HoL is gone too and one loss affects only one stream. QUIC also folds in TLS 1.3 for a 0-RTT handshake plus connection migration (Wi-Fi to cellular). You don't pick a version — it's negotiated via ALPN/Alt-Svc with fallback if UDP is blocked. The asterisk on 'newer is faster': on a lossy mobile link, HTTP/2 can finish slower than HTTP/1.1 because its single connection freezes all streams at once.

The Trade-off

If each version is strictly better, why does HTTP/1.1 still run, and why isn't HTTP/3 an automatic win everywhere? Because underneath the arc is a real trade-off between two things you can't fully maximize at once:

Concurrency (many requests progressing together) vs isolation (one stuck request not stalling the others).

  • HTTP/1.1 gives you isolation but almost no concurrency. Its six separate connections are wonderfully independent — a jam on one lane doesn't touch the others — but each lane is strictly single-file, so you're capped and slow.
  • HTTP/2 flips it: maximum concurrency, at the cost of isolation. Unlimited streams share one connection with tiny overhead — glorious on a clean network — but they now share a single fate: one lost packet freezes them all.
  • HTTP/3 refuses the compromise and buys back both — concurrency and isolation — by moving streams into QUIC. But nothing is free: it pays in complexity and a rockier path. UDP is sometimes blocked or throttled by firewalls (so HTTP/3 must be able to fall back to HTTP/2 or /1.1), the QUIC stack historically costs more CPU than battle-hardened kernel TCP, and it's a younger technology.

This is why the honest answer to "which HTTP should I use?" is "offer all three and let the client negotiate." You don't pick a version in your code — the client and server agree on the best one they both support (via ALPN during the TLS handshake, and an Alt-Svc header advertising "I also speak HTTP/3"), and if UDP is blocked, HTTP/3 quietly falls back. In practice you flip all three on (usually at your CDN) and each visitor automatically gets the fastest their network allows. The clean "newest is best" story has an asterisk — and now you know exactly what it says.

Mental-Model Corrections

"HTTP/2 eliminated head-of-line blocking." Only the application-layer one, via multiplexing. It inherited a transport-layer one from TCP — one lost packet stalls every stream. That leftover blocking is the entire reason HTTP/3 exists.

"HTTP/3 is just HTTP/2 with a small tweak." No — it replaced the transport, swapping TCP for QUIC over UDP. That's a foundational change, not a tune-up.

"HTTP/2 opens lots of connections like HTTP/1.1." The opposite — HTTP/2 uses one connection and multiplexes over it. The six-connections trick was HTTP/1.1's workaround, made obsolete.

"Newer HTTP is always faster." Not on every network. On a lossy mobile link, HTTP/2 can be slower than HTTP/1.1 (TCP HoL freezes all its streams). Faster is conditional — and knowing why is the skill.

"Domain sharding is a good optimization." It was — for HTTP/1.1. Under HTTP/2 it's an anti-pattern that breaks single-connection multiplexing. New protocol, inverted advice.

"You choose the HTTP version in your application code." No — it's negotiated between client and server (ALPN / Alt-Svc), with automatic fallback. You enable versions (usually at the CDN); the client uses the best it can.

"HTTP/3 replaces TCP everywhere." No — QUIC/UDP is for HTTP and a few peers, can be blocked (then it falls back to TCP), and TCP still carries most of the internet. It's a targeted escape hatch, not a wholesale replacement.

Try It Yourself: Watch the Protocols Live

Five minutes in your browser and you'll see multiplexing and the version negotiation with your own eyes. Predict first: on a busy news or shopping site, how many of the requests do you think are HTTP/2 or HTTP/3 versus old HTTP/1.1 — and will they start in staggered batches or all together?

  1. Reveal the protocol. Open your browser's DevTools → Network tab and reload a big site. Right-click the column headers and switch on the Protocol column: you'll see h2 and h3 everywhere (and the odd http/1.1). You're watching the negotiated version, per request, live.
  2. See multiplexing in the waterfall. In that same Network waterfall, notice how on h2/h3 many requests start at the same moment and download in parallel — no staggered batches of six. That simultaneous start is multiplexing; the old six-at-a-time stair-step is what HTTP/1.1 looked like.
  3. Feel the loss (the real test). Turn on DevTools network throttling (a slow/lossy profile) and reload. Watching a page struggle on a bad connection is the closest you'll get to feeling head-of-line blocking — the very thing HTTP/3 was built to smooth out. (Bonus: curl --http3 -sI https://cloudflare.com shows an HTTP/3 negotiation from the command line.)

Do step 2 once and the abstract word "multiplexing" turns into a picture you'll never lose: a column of requests all launching together instead of trickling out six at a time. That picture is the leap from HTTP/1.1 to the modern web.

Recap & What’s Next

One enemy, fought twice:

  • HTTP's request → response never changed; what changed is how a page's hundreds of requests share the connection.
  • HTTP/1.1: one request at a time per connection → application-layer head-of-line blocking; browsers open ~6 connections to cope.
  • HTTP/2: multiplexes many streams over one TCP connection → escapes app-layer HoL and the 6-connection limit. But it still rides TCP, so one lost packet stalls every streamtransport-layer HoL — and on lossy mobile it can be slower than HTTP/1.1.
  • HTTP/3: runs on QUIC over UDP with independent per-stream loss recoveryescapes transport-layer HoL (a lost packet stalls only its own stream), plus a 0–1 RTT handshake and connection migration.
  • The trade-off is concurrency vs isolation; you don't pick a version, you offer all three and the client negotiates (ALPN / Alt-Svc) with fallback.

There's one thread we've quietly left dangling. Both HTTP/2 and HTTP/3 assume encryption — the browser won't even speak them in the clear, and QUIC has TLS baked right in. That encryption isn't free: it's a handshake, and a handshake is round trips, and round trips are latency. So what exactly does security cost, where does it hurt, and how do modern protocols shrink the bill? That's the next lesson: TLS — security's latency bill.