system-design

HTTP/1.1 vs HTTP/2 vs HTTP/3

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


The problem

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.


HTTP/1.1: one at a time

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: multiplexing

HTTP/2 (2015, from Google’s SPDY) keeps HTTP semantics identical — same methods, headers, status codes — and replaces the wire format.

What changed

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.

The problem HTTP/2 couldn’t solve

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: QUIC over UDP

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.

What you get

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.

The costs


The comparison

  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

What this means for your designs

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.


In the real world


🚨 Interview traps


🎙️ Soundbites


🛠️ Try it

# 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).


Check yourself

1. Explain head-of-line blocking at both the HTTP/1.1 and TCP layers. HTTP/1.1: a connection serves one request at a time, so a slow response blocks all queued requests behind it on that connection. TCP: bytes must be delivered in order, so a single lost packet stalls delivery of everything received after it — and under HTTP/2, that includes frames from unrelated streams sharing the connection. HTTP/2 fixed the first and inherited the second; QUIC fixes both by making streams independent at the transport layer.
2. Why is domain sharding harmful under HTTP/2? Sharding was designed to escape HTTP/1.1's 6-connections-per-host limit. Under HTTP/2 you want *one* connection carrying everything: shared header compression state, one warm congestion window, one handshake. Sharding splits that into several connections, each with cold congestion windows, its own handshake, extra DNS lookups, and no shared HPACK table. You pay all the costs and get none of the original benefit.
3. Your users are on unreliable mobile networks in a region far from your servers. Which protocol and why? HTTP/3. Three reasons stack up for exactly this user: 1-RTT (or 0-RTT) handshake matters most when RTT is high; no cross-stream head-of-line blocking matters most when loss is high; and connection migration means switching between cell towers or to Wi-Fi doesn't kill in-flight requests. This is the profile HTTP/3 was designed for.
4. Why couldn't the head-of-line problem be fixed in TCP itself? TCP lives in OS kernels and in middleboxes (NATs, firewalls, load balancers) everywhere on the internet. Changing it requires the whole world to upgrade, which takes 10+ years, and middleboxes actively drop traffic they don't recognize. Building on UDP put the new logic in userspace — it ships with the browser and can be updated in weeks.
5. Is HTTP/3 worth it for internal service-to-service calls in one datacenter? Usually not. Inside a datacenter, packet loss is very low and RTT is ~0.5 ms, so the two main HTTP/3 wins (head-of-line blocking under loss, handshake RTT) are near-zero benefits. Meanwhile you'd pay higher CPU and worse tooling. HTTP/2 — which is what gRPC uses — is the right default internally.

Further reading