Head-of-Line Blocking Explained: HTTP/1.1, HTTP/2, QUIC
Head-of-line blocking is when one stalled item holds up everything queued behind it. How it affects HTTP/1.1, HTTP/2 over TCP, and how QUIC fixes it.
Head-of-line blocking (HOL blocking) happens when the first item in a queue cannot be processed, and everything behind it has to wait even though it is ready to go. The name comes from a checkout line where one customer’s slow transaction holds up everyone else. In networking it shows up at several layers, and the history of HTTP from version 1.1 to version 3 is largely a story of moving the blocking point lower and lower until it disappears.
The general pattern
Any system that enforces strict ordering on a shared channel is exposed to HOL blocking. Items B, C, and D could be delivered now, but the rules say they must follow A, and A is stuck. The cost is that one slow or lost item delays unrelated work.
It is worth separating two ideas:
- Ordering requirement: whether items must be delivered in the order they were sent.
- Independence: whether the items actually depend on each other.
HOL blocking is painful precisely when a channel enforces ordering across items that are independent.
HTTP/1.1: blocking at the application layer
HTTP/1.1 sends one request at a time on a connection and expects responses in the same order. Pipelining technically allowed several requests to be sent without waiting, but responses still had to come back in order, so a slow first response held up the rest. Pipelining was never broadly enabled in browsers because of this and because of buggy intermediaries.
The practical workaround was to open several parallel TCP connections per origin (browsers typically allow around six), plus tricks like domain sharding, image sprites, and concatenated bundles. These reduced the number of requests competing for a connection, but each connection still carried one request at a time.
HTTP/2: fixing it above TCP
HTTP/2 introduced multiplexing. Many requests and responses share a single connection as independent streams, and their data is chopped into frames that can be interleaved. A slow response on stream 1 no longer blocks stream 3; the server can send stream 3’s frames whenever they are ready.
That solved HOL blocking at the HTTP layer. It did not solve it at the transport layer, because HTTP/2 still runs over TCP.
TCP: the blocking that remained
TCP guarantees an ordered byte stream. If a packet is lost, the receiving operating system holds every later packet in its buffer until the missing one is retransmitted and arrives. Only then does it hand the bytes to the application.
TCP has no idea that HTTP/2 frames belong to separate streams. So if a packet carrying part of stream 1 is lost, frames for streams 3, 5, and 7 that arrived fine are stuck in the kernel buffer too. With everything multiplexed onto one connection, a single lost packet stalls every in-flight request.
This creates a counter-intuitive result: on lossy networks, such as congested mobile links, HTTP/2 over one connection can perform worse than HTTP/1.1 over six connections, because a loss in HTTP/1.1 only stalls one of the six.
QUIC and HTTP/3: removing it at transport
QUIC is a transport protocol that runs over UDP and builds streams into the transport itself. Each stream has its own ordering. When a packet is lost, only the streams whose data was in that packet wait for the retransmission; other streams keep delivering data to the application.
HTTP/3 is HTTP mapped onto QUIC. It keeps HTTP/2’s multiplexing but finally removes transport-level HOL blocking between streams. Our HTTP/2 vs HTTP/3 comparison covers the other differences, including faster handshakes and connection migration.
Where each version blocks
| HTTP/1.1 | HTTP/2 over TCP | HTTP/3 over QUIC | |
|---|---|---|---|
| Requests per connection | One at a time | Many, multiplexed | Many, multiplexed |
| HTTP-layer HOL blocking | Yes | No | No |
| Transport-layer HOL blocking | Per connection | Across all streams | Only within a stream |
| Effect of one lost packet | Stalls one connection | Stalls every stream | Stalls affected streams only |
| Common workaround | Multiple connections | Accept it, tune TCP | Not needed |
A subtle leftover: header compression
HTTP/2 compresses headers with HPACK, which keeps a shared dynamic table across all streams. Headers must be decoded in order, so a lost packet carrying header updates could block other streams’ headers. HTTP/3 replaced HPACK with QPACK, which is designed so that streams can avoid depending on table updates that have not arrived yet, at a small cost in compression efficiency. It is a good example of how HOL blocking can sneak back in through any shared, ordered state.
HOL blocking outside HTTP
The pattern appears well beyond web protocols:
- TLS records over TCP inherit TCP’s ordering, which is one reason QUIC integrates encryption into the transport rather than layering TLS on top of TCP.
- Network switches with a single input queue per port can stall packets destined for an idle output behind a packet waiting for a busy one. Virtual output queues fix this.
- Message queues and job queues with a single ordered consumer block on one poison message. Dead-letter queues and per-key partitioning limit the damage.
- Single-threaded servers and event loops block every request behind one slow, synchronous handler, closely related to the ideas in what backpressure is.
The fix is always some version of the same move: only enforce ordering among items that genuinely depend on each other, and let independent items proceed in parallel.
What this means for web developers
- Do not rely on old HTTP/1.1 hacks. Domain sharding actively hurts on HTTP/2 and HTTP/3 because it splits traffic across connections that cannot share prioritisation or congestion state.
- Enable HTTP/3 where you can. Most CDNs support it, and it helps most on mobile and lossy networks.
- Keep critical resources small and early. Even without HOL blocking, bandwidth is shared, so prioritising render-critical CSS and scripts still matters.
The takeaway
Head-of-line blocking is what happens when strict ordering forces independent work to wait behind a stalled item. HTTP/1.1 suffered from it at the request level, HTTP/2 fixed that with multiplexing but inherited it from TCP at the packet level, and HTTP/3 over QUIC finally gives each stream its own ordering. The general lesson applies to any queue: enforce order only where there is a real dependency.
Keep reading
Takina · · 4 min read The Server-Timing Header Explained
The Server-Timing HTTP header exposes backend performance metrics — database time, cache hits — directly in the browser's DevTools network panel.
Takina · · 4 min read What Is WebTransport? A Guide to the API
WebTransport is a browser API for low-latency, bidirectional client-server messaging over HTTP/3. How it works and when to reach for it.
Takina · · 4 min read HTTP Range Requests and Partial Content, Explained
HTTP range requests let a client ask for just part of a resource, enabling video seeking, resumable downloads, and partial file fetches over HTTP.