Blog

How HTTP/2 Multiplexing Killed Head-of-Line Blocking at the App Layer

HTTP/2 replaced HTTP/1.1's serialized request queue with interleaved streams over one connection, removing a bottleneck that years of hacks only partially hid.

March 25, 20266 min readMuhammad Shehzaib
PERFORMANCENETWORKINGHTTP2HTTP

For most of the web's life, HTTP/1.1 forced a awkward bargain: one TCP connection could carry only one request and response at a time. Developers worked around it by opening six connections per origin, concatenating files, inlining assets, and sharding domains, all to dodge a fundamental limitation. HTTP/2 changed the model. Instead of treating the connection as a single lane that requests queue up in, it introduced a binary framing layer that slices every message into small frames, tags each with a stream identifier, and interleaves them over one connection. Many requests and responses now travel concurrently on a single TCP socket. Understanding how multiplexing works, and where it still falls short, helps you reason about real production performance, why server push largely failed, and why HTTP/3 moved off TCP entirely to finish the job.

HTTP/1.1 Keep-Alive Explained

HTTP/1.0 opened a fresh TCP connection for each request and closed it after the response, paying the handshake cost every single time. HTTP/1.1 introduced persistent connections, also called keep-alive, where the connection stays open after a response so the next request reuses it. This avoided repeated handshakes and the slow-start ramp-up, dramatically improving throughput. The connection header and timeouts govern how long a connection lingers idle before either side closes it. Keep-alive is genuinely valuable and remains important, but it solved only part of the problem. The connection could be reused sequentially, yet it still carried one transaction at a time. A second request could not begin until the first response fully arrived, so a slow response blocked everything queued behind it. Keep-alive removed setup overhead but left the serialization bottleneck firmly in place.

The Pipelining Dead End

HTTP/1.1 tried to address serialization with pipelining, which let a client send multiple requests without waiting for each response, expecting the server to answer them in order. In theory this filled the connection. In practice it failed badly. The server had to return responses in the exact order requests arrived, so a single slow response still stalled every response behind it, a problem called head-of-line blocking at the application layer. Worse, many proxies and servers implemented pipelining incorrectly or not at all, sometimes corrupting responses. Browsers ultimately disabled it by default because it was too unreliable to ship. The lesson shaped HTTP/2: ordered responses on a single channel are inherently fragile. To truly run requests concurrently, the protocol needed a way to interleave responses out of order and reassemble them correctly, which is exactly what the binary framing layer provides.

The Six-Connection Workaround

Because one HTTP/1.1 connection handled one transaction at a time, browsers compensated by opening multiple parallel connections to each origin, conventionally capped around six. With six connections a page could fetch six resources simultaneously, but a typical page references dozens of assets, so most still queued and waited. Developers stacked further hacks on top: domain sharding spread assets across multiple hostnames to unlock more parallel connections, while concatenation bundled many scripts into one file and spriting merged many images into one to reduce request count. These tricks helped but carried real costs. Each extra connection repeats the TCP handshake and slow start, consumes server memory, and competes for bandwidth without coordinating congestion control. Bundling hurt caching because changing one small file invalidated an entire bundle. The whole ecosystem was contorting itself around a transport limitation rather than fixing it.

The Binary Framing Layer

HTTP/2's central innovation is replacing the text-based, line-delimited message format with a binary framing layer. Every message is split into frames, each a small typed unit with a header that includes a length, a type such as HEADERS or DATA, flags, and crucially a stream identifier. A request and its response form a logical stream identified by a unique number, and frames from many streams can be interleaved on the wire in any order. The receiver uses the stream identifier on each frame to reassemble the correct message. Because frames are fixed-format binary rather than free-form text, parsing is faster, less ambiguous, and less error-prone than the old approach of reading until a blank line. This framing layer is the foundation that makes everything else, multiplexing, prioritization, flow control per stream, and header compression, possible over a single connection.

How Multiplexing Works

Multiplexing means many requests and responses share one TCP connection concurrently by interleaving their frames. The client can send a HEADERS frame for stream one, then HEADERS for stream three, then more frames for stream one, all without waiting for any response. The server likewise sends response frames as data becomes ready, tagging each with its stream identifier, so a fast small response can be delivered fully while a large slow one is still streaming in the background. The receiver demultiplexes by stream identifier, buffering and reassembling each stream independently. This eliminates the application-layer head-of-line blocking that doomed pipelining, because responses no longer must arrive in request order. One connection now does the work that previously required six, with a single congestion-controlled flow that behaves better on the network and uses far less memory on both ends.

Header Compression With HPACK

HTTP requests carry verbose headers, and on a typical page most requests to the same origin repeat nearly identical headers: the same user agent, accept values, and cookies sent over and over. In HTTP/1.1 this redundant text was sent in full every time, wasting bandwidth, especially on the upstream link. HTTP/2 introduced HPACK, a compression scheme designed specifically for headers. It maintains a dynamic table of previously seen header fields on both ends, so a repeated header can be sent as a small index reference rather than the full string. Static common headers have predefined indices. HPACK was carefully designed to resist the compression-based attacks that plagued earlier attempts to compress HTTPS data, by avoiding a naive shared-context approach. The result is that the per-request header overhead, which used to be significant on many small requests, shrinks dramatically across a busy connection.

Stream Priority And Push

Because many streams share one connection, HTTP/2 added a way to express which responses matter most. A client could mark a stream as dependent on another and assign weights, hinting that the server should send critical CSS before a large image, for instance. In practice prioritization proved inconsistent across servers and was simplified in later revisions. Server push was a more ambitious feature: the server could proactively send resources the client had not yet requested but would obviously need, like the stylesheet referenced by an HTML page, saving a round trip. Push sounded compelling but failed in practice because servers often pushed resources already cached by the client, wasting bandwidth, and the logic was hard to get right. Most browsers eventually removed support. The enduring wins of HTTP/2 are multiplexing and header compression, not these two features.

TCP's Remaining Limitation

HTTP/2 eliminated head-of-line blocking at the application layer, but a deeper problem remained because all those multiplexed streams still ride on a single TCP connection. TCP guarantees in-order byte delivery for the whole connection, so if one packet is lost, TCP withholds every subsequent byte from the application until that packet is retransmitted, even bytes belonging to unrelated streams. The streams are independent to HTTP/2 but indistinguishable to TCP, which sees only one byte stream. On a lossy network this transport-level head-of-line blocking can make HTTP/2 perform worse than several separate HTTP/1.1 connections, because with separate connections a loss affects only one of them. This is precisely why HTTP/3 abandoned TCP for QUIC over UDP, where each stream is independently sequenced, so a lost packet blocks only its own stream and the others keep flowing unimpeded.