Articles

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.

The Lycoris Team The Lycoris Team · · 5 min read
Network switch with patch cables

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.1HTTP/2 over TCPHTTP/3 over QUIC
Requests per connectionOne at a timeMany, multiplexedMany, multiplexed
HTTP-layer HOL blockingYesNoNo
Transport-layer HOL blockingPer connectionAcross all streamsOnly within a stream
Effect of one lost packetStalls one connectionStalls every streamStalls affected streams only
Common workaroundMultiple connectionsAccept it, tune TCPNot 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.

Takina 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.

#Web Development #Performance #Networking
Takina 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.

#Web Development #Networking #Performance
Takina 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.

#Networking #Web Development #Performance