Ch. 20 · Networking

HTTP/2 Multiplexing and Head-of-Line Blocking, vs HTTP/3

HTTP/2 removes HTTP-level head-of-line blocking, but TCP's remains. See both in Node and curl, then compare what HTTP/3 over QUIC changes.

~6 min readadvancedupdated Oct 11, 2026

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)]);
JavaScript
HTTP/1.1, one connection:
  /slow via HTTP/1.1 after 2031 ms
  /fast via HTTP/1.1 after 2045 ms
  connections: 1
Text

The 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}
Terminal
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)
Text

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 });
JavaScript
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
Text

/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)
Text

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/b
Text

ALPN 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-Svc header 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 Hints replaced it.
  • Cheap streams need limits. Node’s default maxConcurrentStreams is 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.

See also

More in Networking

read ✓Networking · hard

HTTP/3 and QUIC Transport

Why QUIC moves HTTP onto UDP, how browsers discover HTTP/3 through Alt-Svc and HTTPS records, and where its gains over HTTP/2 disappear.

~6 min readread →
read ✓Networking · mid

Networking: DNS Caching and TTL

See how resolvers count TTLs down, why NXDOMAIN answers are cached too, and how to schedule a DNS cutover so stale answers expire.

~7 min readread →
read ✓Networking · easy

Networking: DNS Record Types

Read real dig output for A, AAAA, CNAME, MX, TXT, NS, SOA and CAA records, and learn which combinations DNS rejects and why.

~6 min readread →
esc