Same semantics, three completely different wire protocols — each one built to fix the last one’s bottleneck.
Prerequisites: HTTP, HTTPS, TLS, TCP, UDP, IP Time to read: ~14 minutes
HTTP/1.1 was designed in 1997 for pages with a handful of resources. A modern page loads 70–150 separate assets. The protocol’s core limitation — one request at a time per connection — turned from a non-issue into the dominant bottleneck.
Everything about HTTP/2 and HTTP/3 follows from trying to fix that, and then from discovering that the fix exposed a deeper problem one layer down.
A connection handles one request, waits for the full response, then handles the next.
Connection 1: [── req A ──][── req B ──][── req C ──]
↑ B cannot start until A finishes
Head-of-line blocking at the application layer. A slow response blocks everything queued behind it on that connection.
The workaround browsers adopted: open 6 connections per hostname and round-robin across them. Which means 6 TCP handshakes, 6 TLS handshakes, 6 congestion windows all starting from slow-start, and 6× the server memory per user.
That in turn spawned a whole culture of workarounds you’ll still see in old codebases:
| Hack | What it did | Why it’s obsolete now |
|---|---|---|
| Domain sharding | img1.site.com, img2.site.com to get 6 connections each |
Multiplies DNS lookups and connections; actively harmful on H2/H3 |
| Sprite sheets | Combine 30 icons into one image | Downloads all 30 even if you need 1 |
| Concatenating JS/CSS | One giant bundle | One byte changes → the whole bundle’s cache is invalidated |
| Inlining assets | Base64 images inside CSS | Uncacheable, ~33% size penalty |
🚨 If a candidate proposes domain sharding in an interview today, that’s a dated answer. Under HTTP/2 it makes things worse, because you lose multiplexing and header compression across the split connections.
Pipelining was HTTP/1.1’s attempted fix — send multiple requests without waiting. It failed: responses still had to come back in order, so one slow response blocked the rest anyway, and buggy proxies mangled it. Effectively dead.
HTTP/2 (2015, from Google’s SPDY) keeps HTTP semantics identical — same methods, headers, status codes — and replaces the wire format.
1. Binary framing. Messages become binary frames instead of text. Faster and less ambiguous to parse.
2. Multiplexing. Many concurrent streams over a single TCP connection, interleaved:
Connection: [A1][B1][C1][A2][C2][B2][A3][C3]
all three requests in flight simultaneously
One connection. One handshake. One congestion window that gets properly warm. This is the big win.
3. Header compression (HPACK). HTTP headers are enormously repetitive — the same cookies, user-agent, and accept headers on every request, often 800+ bytes each. HPACK keeps a shared table of previously-sent headers so repeats cost a byte or two.
📐 On a page with 100 requests and 800 bytes of headers each, that’s 80 KB of headers reduced to a few KB. On mobile uplinks, this is substantial.
4. Stream prioritization. Clients can declare that CSS matters more than a below-the-fold image. (In practice, implementations were inconsistent and this was redesigned in HTTP/3.)
5. Server push. The server proactively sends resources it knows you’ll want. Sounded great,
worked badly (it pushed things clients already had cached), and has been removed from Chrome.
Don’t propose it. 103 Early Hints is the modern replacement.
Multiplexing moved head-of-line blocking from the application layer down to the transport layer.
TCP delivers bytes strictly in order. If one packet is lost, TCP holds back everything that arrived after it — including frames belonging to completely unrelated streams:
TCP stream: [A1][B1][✗ LOST ][C1][A2][B2]
↑ C1, A2, B2 arrived fine but sit in the kernel buffer
waiting for the lost packet to be retransmitted
⚖️ So HTTP/2 is strictly better than HTTP/1.1 on a good network, and can be worse on a lossy one — because HTTP/1.1’s 6 connections mean a loss only stalls 1/6 of your traffic, while HTTP/2’s single connection means a loss stalls everything. This matters a lot on mobile and in regions with poor connectivity.
And TCP is implemented in operating system kernels and in middleboxes worldwide. It cannot be changed on any useful timescale. So the fix had to bypass TCP entirely.
HTTP/3 (standardized 2022) runs over QUIC, a reliable transport built on UDP, implemented in userspace.
Why UDP? Not because UDP is better — but because it’s the only transport that gets through the world’s existing routers, NATs, and firewalls unmolested. QUIC then reimplements everything TCP gave up: reliability, ordering (per stream), and congestion control.
1. No cross-stream head-of-line blocking. QUIC knows about streams, so a lost packet only stalls the stream it belonged to. Everything else is delivered immediately. This is the headline feature.
2. Faster handshakes. QUIC merges the transport and TLS 1.3 handshakes:
| Connection setup | |
|---|---|
| HTTP/1.1 or /2 over TCP + TLS 1.2 | 3 RTT |
| HTTP/1.1 or /2 over TCP + TLS 1.3 | 2 RTT |
| HTTP/3 (QUIC) | 1 RTT — and 0 RTT on resume |
📐 On a 200 ms connection to a distant server, that’s 600 ms → 200 ms before the first byte moves.
3. Connection migration. The connection is identified by a connection ID, not the (source IP, source port, dest IP, dest port) tuple. Walk out of the café and switch from Wi-Fi to 5G, and your download continues uninterrupted. Under TCP the connection dies and everything restarts. This is genuinely transformative for mobile.
4. Always encrypted. TLS 1.3 isn’t optional in QUIC; it’s part of the protocol. Even most transport metadata is encrypted, which also stops middleboxes from “helpfully” interfering.
tcpdump shows you encrypted UDP. (qlog and
Wireshark with keys help.)| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Year | 1997 | 2015 | 2022 |
| Transport | TCP | TCP | QUIC / UDP |
| Format | Text | Binary | Binary |
| Requests per connection | 1 at a time | Many (multiplexed) | Many (multiplexed) |
| Head-of-line blocking | App layer ✗ | Transport layer ✗ | None across streams ✅ |
| Header compression | None | HPACK | QPACK |
| Encryption | Optional | Optional in spec, mandatory in browsers | Built in, mandatory |
| Handshake RTTs | 2–3 | 2–3 | 1 (0 on resume) |
| Connection migration | ✗ | ✗ | ✅ |
| Best for | Legacy, simple internal tools | Most traffic today | Mobile, lossy networks, global users |
For public web and mobile clients: enable HTTP/2 and HTTP/3 at your CDN or load balancer. It’s
usually a config flag, and the client negotiates the best version automatically (via ALPN and the
Alt-Svc header). This is close to free performance.
Undo the HTTP/1.1 hacks. With multiplexing, many small cacheable files beat one giant bundle — change one file and only that file’s cache is invalidated. Domain sharding actively hurts.
For internal service-to-service traffic: you’re on a fast, reliable network inside a datacenter, so head-of-line blocking is rarely your problem. gRPC uses HTTP/2 and that’s the right default. HTTP/3 internally is mostly unnecessary complexity.
Long-lived connections change your capacity math. With HTTP/2, one client holds one connection open. A million concurrent users = a million connections spread across your edge, each costing memory. Factor this into capacity planning.
103 Early Hints instead.# Which version does a site negotiate?
curl -sI --http2 https://www.cloudflare.com | head -1 # HTTP/2 200
curl -sI --http3 https://www.cloudflare.com | head -1 # HTTP/3 200 (needs curl built with HTTP/3)
# Look for the Alt-Svc header advertising h3
curl -sI https://www.google.com | grep -i alt-svc
Then open Chrome DevTools → Network → right-click the column headers → enable Protocol. Reload a
few major sites and watch h2 and h3 appear. Compare against a small self-hosted site (probably
http/1.1).