HTTP/2 multiplexing and head-of-line blocking

HTTP/2 sends every request and response at once over a single connection instead of queueing them on six. Load a page over HTTP/1.1, HTTP/2 and HTTP/3, simulated packet by packet, and see when that wins and what one lost packet does to it.

Load
Round trip
Bandwidth
Packet loss
Responses on a connection
Objects finished over time (40 objects, 1007 KB)
05001s400

HTTP/3 waterfall: one row per object
05001s

requested, waiting for the first bytearriving

Results
HTTP/1.1HTTP/2HTTP/3
Connections611
Setup before request200 ms200 ms100 ms
Half the objects done797 ms952 ms852 ms
All done1.19 s1.06 s965 ms
Packets lost000
Objects held up by another's loss000
HTTP/3 events
  1. 100 msconnection ready, after 1 round trip.

How to use the simulator

The top chart counts finished objects over time for each protocol; the waterfall below shows the selected protocol, one row per object, from request to last byte. Click a protocol's name to see its waterfall and its event log.

  • With no loss, compare HTTP/1.1's waterfall, a staircase of six connections taking one object at a time, with HTTP/2's, where everything is requested together and arrives interleaved.
  • Set loss to 1%. HTTP/2 rows turn red where an object had already arrived but was held behind another object's lost packet. HTTP/3 has none.
  • Notice that at 1% loss HTTP/1.1 may finish first: six connections keep six congestion windows, and each loss cuts only one of them.

What is HTTP/2?

HTTP/2 is the second major version of the Hypertext Transfer Protocol. It keeps HTTP's meaning, the same methods, status codes and headers, and changes how messages travel: instead of text, each request and response is split into binary frames on a numbered stream, and many streams share one TCP connection. Headers are compressed with HPACK. Browsers use HTTP/2 only over TLS, negotiated with the ALPN extension as h2.

The point was latency. A web page needs dozens of files from the same server, and HTTP/1.1 makes them wait in line.

The problem with HTTP/1.1

An HTTP/1.1 connection carries one response at a time: the next request on it waits until the previous response has been fully received. Pipelining, sending several requests without waiting, was in the standard but responses still had to come back in order, and buggy proxies made browsers disable it. So browsers open several connections to each server, typically six, and sites worked around the limit by spreading files over several hostnames (domain sharding), combining scripts into bundles and images into sprites.

Every extra connection costs a TCP and TLS handshake and starts with a small congestion window. With no loss, the 40-file page in the simulator (1007 KB) finishes at these times over a 20 Mbit/s link:

Round tripHTTP/1.1, 6 connectionsHTTP/2HTTP/3
20 ms521 ms498 ms478 ms
50 ms708 ms691 ms641 ms
100 ms1.19 s1.06 s965 ms
200 ms2.29 s1.91 s1.71 s

The longer the round trip, the more HTTP/1.1 loses by waiting a round trip between each request and the next on every connection.

Streams and frames

Every HTTP/2 frame starts with a 9-byte header: a 24-bit length, an 8-bit type, 8 bits of flags and a 31-bit stream identifier. The payload is at most 16,384 bytes unless the receiver allows more. The main frame types are HEADERS and CONTINUATION (a header block), DATA (the body), SETTINGS (connection parameters), WINDOW_UPDATE (flow control), RST_STREAM (cancel one stream), PING, GOAWAY (shut the connection down gracefully) and PUSH_PROMISE (server push).

A request is a HEADERS frame on a new stream, odd-numbered because the client opened it; the response comes back as HEADERS and DATA frames on the same stream. Because frames from different streams can be interleaved, a large response no longer blocks a small one behind it, and a request can be cancelled without closing the connection.

Head-of-line blocking in TCP

HTTP/2 removed head-of-line blocking from HTTP, but TCP has its own. TCP presents a single ordered byte stream, so when a packet is lost, every byte after it waits in the receiver's buffer until the retransmission arrives, roughly one round trip later, even bytes belonging to other HTTP/2 streams. HTTP/3 runs over QUIC, whose streams are ordered independently, so a loss holds back only the stream it belongs to.

The page over a 100 ms, 20 Mbit/s link, averaged over 5 loss patterns:

LossHTTP/1.1 half / allHTTP/2 half / allHTTP/3 half / allHeld up by another's loss, HTTP/2
0%797 ms / 1.19 s952 ms / 1.06 s852 ms / 965 ms0 of 40
0.5%834 ms / 1.47 s1.63 s / 2.07 s1.50 s / 1.97 s32 of 40
1%842 ms / 1.53 s1.80 s / 2.51 s1.67 s / 2.41 s36 of 40
2%964 ms / 2.08 s4.14 s / 5.84 s4.00 s / 5.74 s39 of 40

Two effects are mixed here. Head-of-line blocking delays individual objects, which HTTP/3 avoids. The bigger effect at these loss rates is congestion control: a single connection has one congestion window, and every loss shrinks it, so HTTP/2 and HTTP/3 both slow down far more than six HTTP/1.1 connections that share the losses between them. The Mathis formula puts a single loss-limited connection's throughput near MSS/RTT × 1.22/√p, about 1.2 Mbit/s at 1% loss and 100 ms, well under the link's 20 Mbit/s.

Flow control and priorities

HTTP/2 adds flow control above TCP's: each stream and the whole connection have a receive window, 65,535 bytes initially, that the sender must stay within and the receiver reopens with WINDOW_UPDATE frames. It lets a receiver, often a proxy, pause one stream without stopping the rest.

With everything sent at once, the order matters: the stylesheet should arrive before the images. RFC 7540 defined a tree of stream dependencies and weights, which proved complex and was implemented inconsistently; RFC 9113 deprecated it, and RFC 9218 replaced it with a simpler scheme of an urgency level from 0 to 7 and an incremental flag, carried in a priority header. The simulator either interleaves all responses round-robin or sends them one at a time in request order, which is what RFC 9218 asks for when responses of equal urgency are not incremental.

Server push, where the server sends a response the client has not yet asked for, rarely helped in practice and was disabled by default in Chrome 106 in 2022. The 103 Early Hints status code now does the same job by telling the browser what to request.

How the simulation works

The model sends packets of 1,200 bytes through a bottleneck of fixed rate and round-trip time, dropping a random fraction of data packets with a seeded generator. TCP and QUIC follow the same rules for sending, so the comparison isolates the protocols' differences: a congestion window starting at 10 packets, slow start until the first loss, a cut to 0.7 of the window on loss, and loss detection by the packet and time thresholds of RFC 9002. HTTP/1.1 uses up to six connections with one request each at a time; HTTP/2 one TCP connection; HTTP/3 one QUIC connection. TCP and TLS 1.3 take two round trips before the first request, QUIC one.

It leaves out TLS record overhead, delayed ACKs, pacing, receive windows, HTTP/2 flow control, request bodies and server think time. It is checked headless: every byte of every object is delivered exactly once, in order within the object; the link rate is never exceeded; with no loss HTTP/2 and HTTP/3 differ by exactly the extra handshake round trip; and HTTP/3 never shows cross-stream blocking.

History

Google announced SPDY in 2009 and shipped it in Chrome; it introduced multiplexed streams, header compression and server push over TLS. The IETF's HTTPbis working group took SPDY as the starting point for HTTP/2, published as RFC 7540 in May 2015 with HPACK as RFC 7541, and browsers dropped SPDY the following year. In June 2022 RFC 9113 replaced RFC 7540, deprecating the original priority scheme, alongside RFC 9114 for HTTP/3.

HTTP/2 questions and answers

What is multiplexing in HTTP/2?

Sending many requests and responses at the same time over one connection, by splitting each into frames tagged with a stream number and interleaving the frames. HTTP/1.1 can carry only one response at a time per connection, so browsers open several connections per server and still queue requests behind each other.

Does HTTP/2 solve head-of-line blocking?

It solves it at the HTTP level, where one slow response held up the next one on the same connection, but not at the TCP level. TCP delivers bytes strictly in order, so one lost packet stops every stream on the connection until it is retransmitted. In this page’s model, at 1% loss and a 100 ms round trip, 36 of 40 objects on average were held up behind another object’s lost packet over HTTP/2, and none over HTTP/3.

Is HTTP/2 faster than HTTP/1.1?

On a clean connection usually yes: here the 40-object page finishes in 1.06 s over HTTP/2 and 1.19 s over HTTP/1.1 with six connections. With noticeable packet loss it can be slower, because one connection has one congestion window that every loss shrinks, while six HTTP/1.1 connections each lose only part of their sending rate: at 1% loss, 2.51 s against 1.53 s.

What is an HTTP/2 stream?

A logical, bidirectional sequence of frames for one request and its response, identified by a number: odd for streams the client opens, even for the server. Streams share the connection, have their own flow-control window and can be reset individually with RST_STREAM without closing the connection.

How does HTTP/2 flow control work?

Each stream and the connection as a whole have a receive window, 65,535 bytes at the start. The sender may not send DATA beyond the window; the receiver opens it again with WINDOW_UPDATE frames as it consumes data. This lets a receiver slow one stream down without stopping the others. Most implementations advertise much larger windows than the default.

Is HTTP/2 server push still used?

Hardly. Pushed responses were often already cached or arrived too late to help, and Chrome disabled push by default in version 106 in 2022. 103 Early Hints, which tells the browser what to fetch instead of sending it, has largely replaced it.

Further reading

Published Updated