3 min readhttpnetworking

Notes on HTTP/2 multiplexing and connection reuse

HTTP/1.1 can only have one request in flight per connection (pipelining exists on paper, but nobody ships it). Browsers worked around that by opening up to six connections per host, and sites worked around that with domain sharding and sprite sheets. HTTP/2 removes the reason for all of it: one connection, many concurrent requests.

Streams and frames

An HTTP/2 connection carries streams. Each request/response pair is one stream with its own ID: odd numbers are opened by the client, even numbers by the server. Everything on the wire is a frame tagged with a stream ID, so frames from different streams can be interleaved freely:

HEADERS  stream=1   GET /index.html
HEADERS  stream=3   GET /app.css
DATA     stream=1   (8 KB)
HEADERS  stream=5   GET /logo.svg
DATA     stream=3   (4 KB, END_STREAM)
DATA     stream=1   (8 KB, END_STREAM)

A slow response on stream 1 does not block stream 3 at the HTTP layer. Headers are compressed with HPACK, which keeps a shared table per connection — another reason to keep requests on the same connection rather than spreading them out.

How many streams at once?

Each side advertises SETTINGS_MAX_CONCURRENT_STREAMS. The spec only recommends that it be at least 100; in practice servers pick something between 100 and a few hundred. When a client hits the limit it simply queues new requests until a stream closes. If you see requests stalling in a waterfall even though bandwidth is idle, this setting is one of the first things to check.

Flow control is per stream and per connection. The default window is 64 KB, which is far too small for a fast, high-latency link; good clients and servers raise it early with a WINDOW_UPDATE frame. A server that never raises its window caps a single download at roughly window / RTT.

Connection coalescing

Browsers will reuse one HTTP/2 connection for different hostnames when two conditions hold: the hostname resolves to the same IP address, and the certificate already presented on that connection is valid for the new name. That is why a wildcard or multi-name certificate on a shared front end can collapse static.example.com and www.example.com into a single connection. The ORIGIN frame (RFC 8336) lets a server state explicitly which origins it is willing to serve.

The flip side: if a server serves a name on that connection that it should not, it must answer 421 Misdirected Request so the client retries on a fresh connection.

Where head-of-line blocking still exists

HTTP/2 fixes head-of-line blocking at the HTTP layer, but all streams still share one TCP byte stream. One lost packet stalls every stream until it is retransmitted. On a clean network you never notice; on lossy mobile or long-distance links, a single HTTP/2 connection can perform worse than several HTTP/1.1 connections. HTTP/3 moves streams onto QUIC, where loss on one stream does not block the others.

Shutting down gracefully

A server that wants to close a connection sends GOAWAY with the last stream ID it will process. The client finishes those streams and opens a new connection for anything newer. Long-lived connections behind load balancers often get a GOAWAY after a fixed number of requests or a maximum age, which spreads clients across backends without dropping requests.

Looking at it yourself

$ curl -sv --http2 -o /dev/null https://example.com/ 2>&1 | grep -iE 'ALPN|HTTP/2'
* ALPN: curl offers h2,http/1.1
* ALPN: server accepted h2
* using HTTP/2
> GET / HTTP/2

Protocol negotiation happens in the TLS handshake through ALPN, so no extra round trip is needed. In browser devtools, add the "Connection ID" column to the network panel — with HTTP/2 most requests to one origin share the same ID.

Takeaways

  • Stop sharding domains; it defeats coalescing and HPACK.
  • Check MAX_CONCURRENT_STREAMS and the flow-control window when a single connection feels slow.
  • On lossy links, HTTP/3 helps more than any HTTP/2 tuning.