Interview questions about HTTP/2 tend to start with “what problem does it solve?” and quickly move to “so what problem does HTTP/3 solve?” Answering the second requires being precise about the first. HTTP/2 fixed head-of-line blocking in HTTP. It did not fix it in TCP, and on some networks the single connection it relies on makes that second kind of blocking worse.
This note shows the HTTP-level difference with a Node HTTP/2 server and curl, then explains what’s left for the transport to solve.
One exchange at a time per HTTP/1.1 connection
An HTTP/1.1 connection carries a request, then its response, then the next request. HTTP/1.1 did define pipelining (sending several requests without waiting), but responses still had to come back in order, so one slow response held up every one behind it. Proxies handled it badly and browsers never turned it on by default.
The workaround was more connections. Browsers open about six per origin, and sites used to spread assets across extra hostnames (“domain sharding”) to get more.
Here’s the blocking, measured. A Node server answers /slow after 2 seconds and /fast after 10 ms. A client limited to one HTTP/1.1 connection sends both at the same moment:
const agent = new https.Agent({ keepAlive: true, maxSockets: 1, ca }); // one connection, like a single HTTP/1.1 pipe
const results = await Promise.all([get('/slow', elapsed), get('/fast', elapsed)]);HTTP/1.1, one connection:
/slow via HTTP/1.1 after 2031 ms
/fast via HTTP/1.1 after 2045 ms
connections: 1The fast request took two seconds because it waited for the slow one to finish with the connection. Let curl fetch six URLs in parallel over HTTP/1.1, and it does what browsers do, opening a connection for each:
curl -s -Z --http1.1 --cacert cert.pem https://localhost:8446/{1,2,3,4,5,6}server: TLS connection #1 (ALPN http/1.1)
server: TLS connection #2 (ALPN http/1.1)
server: TLS connection #3 (ALPN http/1.1)
server: TLS connection #4 (ALPN http/1.1)
server: TLS connection #5 (ALPN http/1.1)
server: TLS connection #6 (ALPN http/1.1)Six TCP handshakes, six TLS handshakes, six congestion windows each starting small.
Streams over one connection
HTTP/2 keeps HTTP’s meaning (methods, headers, status codes) and changes how it travels. Each request and response becomes a stream with a numeric ID, and its headers and body are cut into binary frames tagged with that ID. Frames from different streams can be interleaved on one connection in any order, so a slow response no longer holds a place in line.
The same two requests, both sent on a single HTTP/2 connection:
const session = http2.connect('https://localhost:8445', { ca });
const stream = session.request({ ':path': path });HTTP/2, one connection:
/slow via HTTP/2.0 stream 1 after 2014 ms
/fast via HTTP/2.0 stream 3 after 24 ms
connections: 1/fast came back in 24 ms on stream 3 while stream 1 was still waiting. (Client-started streams use odd numbers.) And curl’s six parallel requests:
== HTTP/2
server: TLS connection #7 (ALPN h2)One connection for all six. curl’s verbose output shows how it got there:
* ALPN: curl offers h2,http/1.1
* ALPN: server accepted h2
* [HTTP/2] [1] OPENED stream for https://localhost:8446/a
* Multiplexed connection found
* Re-using existing connection with host localhost
* [HTTP/2] [3] OPENED stream for https://localhost:8446/bALPN is how the protocol is chosen: during the TLS handshake the client lists the protocols it speaks and the server picks one, so there’s no extra round trip to upgrade. Browsers only use HTTP/2 over TLS.
Two other HTTP/2 features come up in interviews. HPACK compresses headers using a table of previously sent values, which matters because cookies and user-agent strings repeat on every request. And each stream has its own flow-control window (Node’s default is the protocol’s initial 65,535 bytes) on top of one for the whole connection, so a client can stop a large download from starving the other streams.
What multiplexing doesn’t fix: TCP’s ordering
Every HTTP/2 stream on a connection travels inside one TCP byte stream, and TCP delivers bytes strictly in order. If one packet is lost, the receiving kernel holds everything that arrived after it until the retransmission comes in, roughly one round trip later at best. Those held-back bytes might belong to five other streams whose own data arrived intact. They wait anyway.
This is head-of-line blocking at the transport layer, and HTTP/2 can’t see it, because it happens below the socket API. (Reproducing it needs packet-loss emulation, which requires root on macOS, so this part is explained rather than measured.)
The single connection has a second cost. A lost packet makes TCP’s congestion control cut the sending rate for the whole connection. With six HTTP/1.1 connections, a loss slows one of them; with one HTTP/2 connection, it slows everything. On clean, low-loss networks HTTP/2 wins comfortably. On a congested mobile link with a percent or two of packet loss, six independent connections can beat one multiplexed connection.
HTTP/3 and QUIC
HTTP/3 keeps HTTP/2’s model of streams and moves it into the transport. QUIC runs over UDP and implements reliability, ordering, congestion control and encryption itself, but it orders data per stream. A lost packet carrying data for stream 5 delays only stream 5.
QUIC brings other changes that interviewers like to hear:
- Faster setup. The transport and TLS 1.3 handshakes are combined into one round trip, and returning clients can send data immediately with 0-RTT (which can be replayed, so it should only carry idempotent requests).
- Connection migration. Connections are identified by IDs rather than by IP address and port, so a phone moving from Wi-Fi to mobile data can keep its connection.
- Fallback. Some networks block or throttle UDP. Servers advertise HTTP/3 with an
Alt-Svcheader over HTTP/2, so a browser’s first visit typically uses HTTP/2 and later ones try HTTP/3, falling back if it fails.
The cost is CPU: QUIC runs in user space with encryption on every packet, and generally uses more server CPU per byte than TCP with kernel and hardware offloads.
Trade-offs worth knowing
- HTTP/1.1 workarounds hurt under HTTP/2. Domain sharding splits traffic across connections and defeats multiplexing. Aggressive bundling still helps compression but matters much less.
- Server push is gone in practice. Chrome removed it; preloading and
103 Early Hintsreplaced it. - Cheap streams need limits. Node’s default
maxConcurrentStreamsis 4,294,967,295, effectively unlimited. The 2023 “rapid reset” attack opened and cancelled streams at enormous rates against servers like this. Set a sensible limit. - Load balancing changes. One long-lived HTTP/2 connection carries all of a client’s requests, so a layer-4 balancer pins them all to one backend. gRPC, which runs on HTTP/2, usually needs a layer-7 balancer that spreads individual streams.
Interview practice
Your team switches the CDN to HTTP/2, expecting faster pages. Desktop users on office networks see a clear improvement. Mobile users on a busy cellular network see none, and sometimes pages get slower. Explain why, and say what you’d measure and change.
Answer
HTTP/2 removed HTTP-level head-of-line blocking and the cost of opening six connections, which is why desktop users on clean networks got faster pages. But all streams now share one TCP connection. On a lossy cellular network, each lost packet stalls every stream until it’s retransmitted, and the congestion window for the only connection shrinks, so the whole page slows down. Previously, with six HTTP/1.1 connections, a loss affected one connection’s resources and the others kept going.
I’d measure by network type: real-user metrics for time to first byte and largest contentful paint split by connection type, plus packet loss and retransmission rates if the CDN exposes them. Then I’d enable HTTP/3 on the CDN, since QUIC’s per-stream ordering is designed for exactly this, and check that Alt-Svc is being sent so repeat visitors upgrade. I’d also remove leftover HTTP/1.1 workarounds like domain sharding, cut the number of critical requests, and make sure the largest assets are compressed and cached so fewer bytes are exposed to loss.