HTTP/1.1 and HTTP/2
- HTTP/1.1 reuses one TCP connection for many requests (persistent connections), but each connection has one request in flight at a time. Browsers hide that by opening about six connections per host.
- HTTP/2 (RFC 9113) keeps HTTP's meaning (methods, headers, status codes) and changes the wire: binary frames, many streams interleaved on one connection, and HPACK header compression.
- Multiplexing removes head-of-line blocking at the HTTP layer. It does not add bandwidth, and it does not touch TCP's own in-order delivery.
- Because everything is on one TCP connection, one lost segment stalls every stream. That is why HTTP/2 can lose to HTTP/1.1 on a lossy link, and why HTTP/3 moved to QUIC.
- h2 is negotiated inside the TLS handshake by ALPN (
h2orhttp/1.1) at no extra round trip; browsers only speak it over TLS. - Dead or replaced: server push (removed from Chrome 106), tree-based stream priorities (RFC 9113 deprecates them; RFC 9218 replaces them). Famous attack: Rapid Reset (CVE-2023-44487).
Mental model
A supermarket. HTTP/1.1 is a checkout lane where the cashier finishes one customer's whole trolley before starting the next; a customer with a huge trolley holds up everyone behind. Browsers cope by opening six lanes at once. HTTP/2 is one lane where the cashier scans a few items for customer A, then a few for B, then a few for C, and so on in rotation, so a small basket is never stuck behind a big one.
Where it breaks: rotating between customers does not make the scanner faster. The belt (bandwidth) is the same, so the total time to scan everything is unchanged. And with one lane, a single jam on the belt (one lost packet) stops every customer, whereas six lanes would have kept five moving.
HTTP/1.1 is a blocking RPC over a socket with one outstanding call per socket: send the request, wait for the whole response, then send the next. HTTP/2 is a multiplexed RPC channel: each call gets a stream id (a correlation id), many calls are in flight at once, and their messages are chopped into frames that share the connection. Head-of-line blocking is just a single-threaded FIFO queue: the item at the front gates everything behind it.
Where it breaks: a correlation-id channel usually sits on a transport that can reorder or lose messages independently. TCP cannot: it is one ordered byte stream, so under HTTP/2 the "many calls" are still serialised through one FIFO at the transport layer.
How it works
HTTP/1.1: persistent, but one request at a time
HTTP/1.0 (RFC 1945, 1996) opened a TCP connection per request. HTTP/1.1 made connections persistent by default: after a response the connection stays open and the next request reuses it, and a side that wants to stop sends Connection: close (RFC 9112 section 9.3). That saves the TCP and TLS handshakes for every object after the first (the handshakes are on the ladder below).
What it does not give you is concurrency. A response must be complete before the next one can be sent on the same connection. HTTP/1.1 does allow pipelining: send request 2 before response 1 has finished. But the server must return responses in request order (RFC 9112 section 9.3.2), so one slow response still blocks everything behind it (head-of-line blocking at the application layer), a failure mid-pipeline forces the client to guess which requests to retry, and some proxies and servers mishandled it. Browsers never made it a default; MDN's connection management guide says pipelining is not activated by default in modern browsers.
The basic request and response format is covered in HTTP basics. Browsers got parallelism the blunt way: several connections per host. RFC 2616 (1999) told clients to keep at most two connections to a server; RFC 9112 now only says a client ought to limit them (section 9.4). In practice major browsers settle on about six per host (see High Performance Browser Networking, HTTP/1.X).
Engineers worked around the six-connection cap and the per-request cost with tricks that you will still see in old codebases:
- Domain sharding: serve assets from
static1.example.comandstatic2.example.comso the browser opens six connections to each host. - Bundling and spriting: concatenate JavaScript and CSS into one file, merge icons into one image, so there are fewer requests to queue.
- Inlining: embed small assets in the HTML.
Each trick trades something: shards cost extra DNS lookups, handshakes and a cold congestion window per host, and bundles invalidate the whole file when one byte changes. High Performance Browser Networking lists these as HTTP/1.x optimisations that HTTP/2 makes unnecessary or harmful.
What every connection costs before the first byte
Every new connection pays a TCP handshake (1 RTT) and then, for HTTPS, a TLS 1.3 handshake (1 RTT, RFC 8446). The first byte of the first response therefore comes back 3 RTT after the client starts: 2 for setup, 1 for the request itself. See TCP connection setup and TLS and HTTPS for the handshakes; this page only needs their RTT counts.
Worked example: 30 objects at 80 ms RTT
The old version of this example counted the six HTTP/1.1 handshakes as one and ignored bandwidth and slow start. Here is the accounting done properly. It is a simplified model, so treat the results as estimates:
- RTT 80 ms, link 50 Mbit/s, no loss, server answers instantly, TLS 1.3, and the browser knows all 30 URLs at t = 0 (a real page discovers them as it parses, which hurts both versions equally).
- TCP starts with an initial window of 10 segments of 1,460 bytes, about 14.6 KB (RFC 6928), and doubles each round trip in slow start (see congestion control).
- HTTP/2 flow-control windows are not the bottleneck (browsers raise them at connection start; see the Deep dive).
- Link bandwidth-delay product: 50 Mbit/s x 0.080 s / 8 = 500,000 bytes, about 342 segments.
Case A: 30 small objects, 2 KB each (60 KB total). Each object is two segments, so bandwidth barely matters: serialising 60 KB at 50 Mbit/s takes 9.6 ms. Slow start still counts for HTTP/2: the 60,000 bytes pack into 42 full segments, which do not fit one initial window of 10.
| Step | HTTP/1.1, 6 connections | HTTP/2, 1 connection |
|---|---|---|
| Setup (TCP + TLS, 2 RTT) | 160 ms (six handshakes in parallel) | 160 ms (one handshake) |
| Requests and slow start | 5 waves x 1 RTT = 400 ms (each connection's window of 10 holds an object) | all 30 sent together, but 42 segments need flights of 10, 20 and 12: 3 RTT = 240 ms |
| Serialising the bytes | about 10 ms spread over the waves | last flight only: about 3 ms |
| Time to last byte | about 570 ms | about 403 ms |
Multiplexing wins by removing the serial request round trips, five waves down to three flights: roughly 1.4 times faster, a real but modest win once slow start is counted.
Case B: 30 medium objects, 20 KB each (600 KB total). Now bytes matter. 20 KB is 14 segments, so it overflows the initial window of 10, and the transfer time for all bytes through the link is 600,000 x 8 / 50,000,000 = 96 ms.
| Step | HTTP/1.1, 6 connections | HTTP/2, 1 connection |
|---|---|---|
| Setup | 160 ms | 160 ms |
| Slow start | Per connection: object 1 needs 2 RTT (14 segments, window 10); the window is then 20 or more, so objects 2 to 5 need 1 RTT each: 6 RTT = 480 ms | One window for everything: 411 segments need flights of 10, 20, 40, 80, 160 and the last 101 segments: 6 RTT = 480 ms |
| Serialising the bytes | 5 waves x 19.2 ms = 96 ms (adds to the waves) | last flight only: 101 segments = 24 ms |
| Time to last byte | about 736 ms | about 664 ms |
The gap shrinks to about 10 percent. HTTP/2 saved the serial request round trips, but it gave up five of the six congestion windows: one connection must ramp up alone, while six HTTP/1.1 connections ramp in parallel and start with 6 x 10 segments between them. This is why "HTTP/2 is always faster" is not a safe claim, and why a high initial window and a warm connection matter as much as the protocol.
Both cases are the same: multiplexing shares the link, it does not enlarge it.
The lab below reproduces both cases with the page-example buttons, then lets you change the round trip, bandwidth and loss, and drop one packet to see which streams stall.
TCP connection
- First byte
- 240 ms
- Page complete
- 403 ms
- Retransmissions
- 0
- Streams stalled by loss
- 0
- Longest stall
- 0 ms
- Bytes delivered
- 60 KB
| Mode | Setup RTTs | First byte | Page complete | Retransmissions | Streams stalled |
|---|---|---|---|---|---|
| HTTP/1.1 | 2 | 240 ms | 570 ms | 0 | 0 |
| ▸ HTTP/2 | 2 | 240 ms | 403 ms | 0 | 0 |
| HTTP/3 | 1 | 160 ms | 323 ms | 0 | 0 |
Every mode moves the same 60 KB through the same pipe. Multiplexing saves round trips, it does not create bandwidth.
Simplified model: initial window 10 segments of 1,460 bytes, window grows by the segments acknowledged and halves once per lossy flight, HTTP/2 packs several streams into one segment while HTTP/3 keeps one stream per packet, ACKs are free, retransmissions are not lost again. A lost tail packet waits for a timer of max(200 ms, 2 RTT); a lost handshake flight waits 1000 ms.
HTTP/2: frames, streams, one connection
HTTP/2 (RFC 9113, obsoleting RFC 7540) changes the encoding, not the meaning. The request line and headers of HTTP/1.1 are text, one message after another; HTTP/2 chops each message into binary frames and tags each frame with a stream id.
Every frame starts with a fixed 9-byte header (RFC 9113 section 4.1):
| Field | Size | What it does |
|---|---|---|
| Length | 24 bits | payload length; default maximum 16,384 bytes (214), raised only by SETTINGS_MAX_FRAME_SIZE |
| Type | 8 bits | DATA, HEADERS, SETTINGS, WINDOW_UPDATE, RST_STREAM, GOAWAY and others |
| Flags | 8 bits | type-specific, for example END_STREAM and END_HEADERS |
| Reserved | 1 bit | unused, must be 0 |
| Stream identifier | 31 bits | which stream the frame belongs to; 0 means the connection itself |
Frame types you should be able to name: HEADERS (starts a stream, carries the request or response headers), DATA (the body), SETTINGS (each side's parameters, acknowledged by the peer), WINDOW_UPDATE (flow-control credit), RST_STREAM (cancel one stream), PING, and GOAWAY (graceful shutdown).
A stream is one request and its response. Client-initiated streams use odd ids (1, 3, 5, ...), ids are never reused, and the recipient may cap concurrency with SETTINGS_MAX_CONCURRENT_STREAMS (the RFC recommends no smaller than 100). Because frames from different streams are simply interleaved on the wire, a small response can finish while a big one is still streaming: that is the removal of head-of-line blocking at the HTTP layer.
HTTP/2 also changes how headers travel. HTTP/1.1 resends every header on every request, and the bulk of them (user-agent, accept, cookie) never change. HPACK (RFC 7541) keeps a table on each side:
- A static table of 61 predefined entries (Appendix A).
:method: GETis entry 2, so it is sent as the single byte0x82;:scheme: httpsis entry 7, byte0x87;:path: /is entry 4, byte0x84. - A dynamic table (4,096 bytes by default) that both ends fill as headers are sent. After the first request,
user-agent: curl/8.7.1is a one-byte reference to table entry 62 or higher. - Huffman coding for string literals that must still be sent.
Illustrative numbers: assume each request carries a 600-byte header block (a realistic size once cookies are included, but an assumption). Thirty HTTP/1.1 requests send 30 x 600 = 18,000 bytes of headers, which is 13 segments and overflows the 10-segment initial window by itself. With HPACK the first request costs about as much, and each later one is little more than a new :path plus one-byte references, perhaps 30 bytes: roughly 600 + 29 x 30 = 1,500 bytes in total. These are estimates, not measurements.
HTTP/2 starts with a fixed connection preface: the client sends the 24-byte string PRI * HTTP/2.0 plus SM (with CRLFs), then a SETTINGS frame (RFC 9113 section 3.4). It is designed so that an HTTP/1.1-only server rejects it instead of misreading it.
| Feature | HTTP/1.1 | HTTP/2 |
|---|---|---|
| Connections per host | about 6 in browsers | usually 1 |
| In flight per connection | 1 request (pipelining unused) | many streams, 100 or more recommended |
| Wire format | text | binary frames |
| Header compression | none (Content-Encoding covers bodies only) | HPACK |
| Head-of-line blocking | at HTTP and at TCP | removed at HTTP, still at TCP |
| TLS in practice | optional | required by browsers |
| Prioritisation | none (connection count is the lever) | RFC 7540 tree deprecated; RFC 9218 urgency |
| Server push | none | specified, removed from Chrome 106 |
For how gRPC maps calls onto HTTP/2 streams see REST, gRPC, GraphQL and webhooks; HTTP/3 and QUIC are on their own page.
Deep dive
Head-of-line blocking, one layer at a time
"Head-of-line blocking" names a queue, so always say which queue.
| Layer | What waits | Fixed by |
|---|---|---|
| HTTP/1.1 application layer | responses on a connection, in request order | HTTP/2 streams |
| TCP | every byte after a lost segment, until it is retransmitted | QUIC, which keeps streams independent |
HTTP/2 fixes the first and inherits the second, because TCP delivers one ordered byte stream and knows nothing about HTTP streams.
How long the stall lasts is a TCP question: fast retransmit needs three duplicate ACKs, so it is at least about one RTT; a lost tail segment waits for the retransmission timer. See retransmission and timers.
There is a second cost on lossy links. A loss event halves the congestion window (congestion control). With HTTP/1.1 over six connections of equal window W, one halving takes the total from 6W to 5.5W, an 8.3 percent drop. With HTTP/2 over one connection, the same event drops the total from W to 0.5W, a 50 percent drop. Derived, not measured, but it is the arithmetic behind "HTTP/2 hurts on a very lossy link".
Flow control: per stream and per connection
TCP already has flow control for the byte stream. HTTP/2 adds its own on top, because without it one slow stream could make the receiver buffer without bound. Each stream and the connection as a whole have a window; the sender may only send DATA up to the window, and the receiver grants more with WINDOW_UPDATE. The initial window is 65,535 bytes (RFC 9113 section 6.9.2).
That default is a throughput trap. A sender that must wait for credit can have at most one window in flight per round trip: 65,535 bytes / 0.080 s = about 819 KB/s, 6.55 Mbit/s at 80 ms RTT, however fast the link. Implementations therefore raise the window at connection start. See also TCP flow control for the transport-level version.
Prioritisation, and its replacement
HTTP/2 as first specified (RFC 7540) let a client describe streams as a dependency tree with weights, so it could say "CSS before images". RFC 9113 deprecates that scheme (section 5.3). The replacement is the Extensible Prioritization Scheme (RFC 9218): a Priority header (or PRIORITY_UPDATE frame) carrying an urgency from 0 to 7 (default 3) and an incremental flag that says whether the response is useful when only partly received. It works the same on HTTP/2 and HTTP/3.
Server push: removed
The idea: when the client asks for index.html, the server sends PUSH_PROMISE frames and delivers style.css before the client has parsed the HTML far enough to ask. The flaw: the server cannot know what the client already has cached, so push often wasted bandwidth, and it was hard to tune. Chrome removed it in version 106 (Chrome team, "Removing HTTP/2 Server Push from Chrome"). The alternatives are <link rel=preload> and the 103 Early Hints status code (RFC 8297). The protocol still defines push; clients can refuse it with SETTINGS_ENABLE_PUSH = 0.
TLS, ALPN and cleartext h2c
The HTTP/2 specification does not require encryption, but implementors of the major browsers said they would only support it over TLS (HTTP/2 FAQ), so on the open web h2 means HTTPS. A client picks the protocol with ALPN (RFC 7301): its ClientHello lists h2, http/1.1, and the server's reply (inside EncryptedExtensions in TLS 1.3) names the winner. That rides on messages already being sent, so negotiation costs no extra round trip. The cleartext variant is h2c: a client that already knows the server speaks it can send the preface directly ("prior knowledge"); the old Upgrade: h2c path is deprecated in RFC 9113. h2c is mostly seen between a proxy and a backend inside a trusted network.
HPACK was designed against CRIME
SPDY, HTTP/2's predecessor, compressed headers with DEFLATE and a shared context. In 2012 the CRIME attack showed that if an attacker can inject guesses next to a secret (such as a cookie) inside the same compressed stream, the compressed size leaks whether a guess matched. HPACK avoids this by never matching substrings across header values: an attacker must guess an entire header value to get a hit, and headers can be flagged never indexed so intermediaries do not store sensitive values (RFC 7541 section 7). That is the answer to "why not just gzip the headers".
Connection reuse, coalescing and GOAWAY
HTTP/2 wants one connection per origin, and may reuse that connection for another hostname if the server's certificate covers both names and the host resolves to the same server (RFC 9113 section 9.1.1). This is connection coalescing, and it is why sharding across hostnames either fails to create new connections or splits the single connection you wanted. If a server cannot answer for a coalesced host, it replies 421 Misdirected Request and the client retries on a new connection. To shut down cleanly a server sends GOAWAY with the last stream id it processed; the client finishes those streams and retries the rest elsewhere (section 6.8).
Rapid Reset (CVE-2023-44487)
SETTINGS_MAX_CONCURRENT_STREAMS limits how many streams a client may have open. An attacker sends HEADERS to open a stream and immediately RST_STREAM to cancel it. A cancelled stream no longer counts against the limit, so the client can open streams as fast as it can send frames, while the server has already started work on each request. Google reported a peak of 398 million requests per second and Cloudflare 201 million, disclosed on 10 October 2023 (Google Cloud analysis, Cloudflare technical breakdown, NVD entry). Mitigations: rate-limit RST_STREAM per connection, close abusive connections with GOAWAY, count cancelled streams toward a budget, and patch the server.
Textbook vs real world
| The textbook says | Production does | Why |
|---|---|---|
| HTTP/2 is always faster than HTTP/1.1 | It wins big on many small objects (about 1.4 times in Case A) and by about 10 percent on 20 KB objects (Case B); on lossy links it can lose | One connection means one congestion window and one TCP queue |
| HTTP/2 eliminates head-of-line blocking | It removes it across streams at the HTTP layer only; TCP still holds every stream behind a lost segment | TCP delivers one ordered byte stream and has no idea about streams |
| Bundling and spriting are obsolete under h2 | Teams still bundle, in fewer and larger chunks | Fewer requests cut per-request overhead and a bundle compresses better as one file; cache granularity is the trade-off |
| Domain sharding raises parallelism | Under h2 it is removed, because it creates extra connections, DNS lookups and cold windows | One connection already carries every stream; coalescing may merge shards anyway |
| Pipelining solves head-of-line blocking | Browsers do not use it; h2 replaced it | Responses must return in request order, so the queue is still FIFO; proxy bugs made it unsafe |
| HTTP/2 push preloads resources | Removed from Chrome 106; use preload hints or 103 Early Hints | Server cannot see the client's cache, so pushes were often wasted |
| HTTP/2 needs no TLS | Browsers speak h2 only over TLS; h2c is used inside private networks | Browser vendors chose it, and ALPN gives a free negotiation channel |
In production
- Google SPDY became HTTP/2. Google announced SPDY in November 2009 (Chromium blog, "A 2x faster web"); the IETF used it as the starting point for HTTP/2, and Chrome announced it would drop SPDY in favour of HTTP/2 (Chromium blog, "Hello HTTP/2, goodbye SPDY").
- Adoption moves every year, so read it live rather than from this page: Cloudflare Radar publishes the share of requests by HTTP version, the HTTP Archive Web Almanac HTTP chapter reports crawl data, and W3Techs tracks the share of sites.
- nginx serves h2 to clients (ngx_http_v2_module), but its proxy module's
proxy_http_versionoffers 1.0 and 1.1 only, so a plainproxy_passupstream is HTTP/1.x; h2 to a gRPC upstream goes throughgrpc_pass. Check the current docs, since this is a moving target. - Envoy can be configured per upstream cluster to speak HTTP/2 to backends (Envoy docs), which is how many service meshes carry gRPC.
- gRPC is defined as a mapping onto HTTP/2 streams, with trailers carrying the status (gRPC over HTTP/2).
- Attack side: Rapid Reset hit Google, Cloudflare and AWS in 2023; see the sources under the Deep dive.
History and evolution
- 1996 HTTP/1.0 (RFC 1945) opens a fresh TCP connection for every request, so each object pays a handshake.
- 1997 HTTP/1.1 (RFC 2068, revised as RFC 2616 in 1999) makes connections persistent by default and adds pipelining. RFC 2616 tells clients to keep at most two connections per server. What broke: responses return in order, so one slow response blocks the rest, and pipelining proved brittle in the field.
- 2009 Google announces SPDY, multiplexed streams with header compression over one connection, to cut page load time.
- 2012 The IETF HTTP working group takes SPDY as the starting point for HTTP/2. The CRIME attack the same year shows DEFLATE header compression leaks secrets, which shapes HPACK.
- 2015 HTTP/2 is published as RFC 7540 with HPACK as RFC 7541. What forced it: pages with many objects and the six-connection workaround. Chrome later drops SPDY in its favour.
- 2022 RFC 9113 replaces RFC 7540 and deprecates tree priorities; RFC 9218 adds extensible priorities; Chrome 106 removes server push; HTTP/3 is published as RFC 9114.
- 2023 Rapid Reset (CVE-2023-44487) shows that stream cancellation can be a DDoS amplifier.
- Now HTTP/2 is deployed on a large share of sites and HTTP/3 on a growing one; Cloudflare Radar and the HTTP Archive publish the live figures.
- Next HTTP/3 over QUIC removes the TCP head-of-line problem (next page).
Hands-on
Compare the two versions from the command line. The commands were run with curl 8.7.1 on macOS; output is abbreviated, and Linux curl built against OpenSSL prints slightly different verbose lines.
%{http_version} is what was actually negotiated. If you ask for --http2 and the server does not offer it, curl falls back and prints 1.1.
See ALPN and the stream in action:
Stream id 1 is the first client-initiated stream, and the four : lines are the pseudo-headers that replaced HTTP/1.1's request line. Ask for the same host twice in one command and watch connection reuse:
One Connected line for two requests: the second used a new stream on the same connection. (Your IP will differ.)
Check what the server offers without HTTP at all:
To see raw frames, install nghttp (brew install nghttp2 on macOS, apt install nghttp2-client on Debian or Ubuntu) and run nghttp -nv https://example.com. It prints each SETTINGS, HEADERS, DATA and WINDOW_UPDATE frame with its stream id. (Output is not reproduced here because it was not captured for this page.) In Chrome DevTools, open the Network tab, right-click a column header and enable Protocol: you will see h2, h3 or http/1.1 per request.
For cleartext h2c against your own server, curl --http2-prior-knowledge http://localhost:8080/ skips the upgrade dance and sends the preface directly.
Gotchas
Interview questions
Show answer
Both carry the same HTTP semantics (methods, headers, status codes). HTTP/1.1 sends text messages and handles one request at a time per connection, so browsers open about six connections per host. HTTP/2 sends binary frames tagged with stream ids, so many requests and responses share a single connection at once, and it compresses headers with HPACK.
Follow-ups:
- What is multiplexing? Interleaving frames from many independent request-response streams on one connection, so a slow response does not block the others at the HTTP layer.
- Does it make downloads use more bandwidth? No. It shares the same link; it removes waiting, not the bandwidth limit.
Show answer
At the HTTP layer, HTTP/1.1 returns responses in request order on a connection, so one slow response blocks the ones behind it. At the TCP layer, a lost segment holds back all later bytes on the connection until it is retransmitted, even if they belong to other requests. HTTP/2 fixes the HTTP-layer problem with streams but still sits on one TCP connection, so it keeps the TCP-layer problem.
Follow-ups:
- Which does HTTP/2 fix? The HTTP-layer one.
- Who fixes the other? QUIC, because it delivers each stream independently (HTTP/3 page).
Show answer
They existed to work around the six-connection cap and per-request cost of HTTP/1.1. Under HTTP/2 one connection already carries many streams, so shards only add extra DNS lookups, handshakes and cold congestion windows, and they split what should be one prioritised, compressed connection. Spriting couples unrelated assets into one cache entry, which is wasteful once requests are cheap.
Follow-ups:
- What is connection coalescing? Reusing one HTTP/2 connection for several hostnames when the certificate covers them and they resolve to the same server (RFC 9113 section 9.1.1).
- Is bundling dead? No: fewer requests and a better compression ratio still help, so teams bundle moderately.
Show answer
Through ALPN, a TLS extension. The client's ClientHello lists h2 and http/1.1; the server picks one in its reply, so negotiation costs no extra round trip. After the handshake both sides speak the chosen protocol, and an h2 client opens with the connection preface and a SETTINGS frame.
Follow-ups:
- What about cleartext? h2c: a client with prior knowledge sends the preface directly; the old
Upgrade: h2croute is deprecated, and browsers do not do either. - What if the server does not know h2? ALPN returns
http/1.1(or nothing) and the connection stays HTTP/1.1.
Show answer
HPACK compresses headers with a static table of 61 common entries, a per-connection dynamic table of recently seen headers, and Huffman coding for literals. Repeated headers become a one-byte index, so thirty requests that share a 600-byte header set shrink dramatically after the first. A shared DEFLATE context was ruled out because of CRIME: compression ratio leaks whether an attacker's guess matched a secret.
Follow-ups:
- What is CRIME? A 2012 attack that recovered secrets such as cookies by observing how the compressed size changed as the attacker injected guesses.
- How does HPACK defend? It matches whole header values only, and sensitive headers can be marked never indexed.
Show answer
Everything shares one TCP connection. A lost segment stalls every stream behind it until the retransmission arrives, and the loss halves the single congestion window, cutting total throughput by 50 percent. HTTP/1.1 over six connections stalls only the connection that lost a segment, and halves one of six windows (about an 8 percent drop in total throughput).
Follow-ups:
- How does HTTP/3 fix it? QUIC delivers streams independently, so a lost packet stalls only the streams whose data it carried.
- Does it hurt on clean links? No: with low loss HTTP/2 usually wins, which is why the effect shows up on mobile and congested Wi-Fi.
Show answer
A 2023 DDoS technique (CVE-2023-44487). The attacker opens a stream with HEADERS and cancels it at once with RST_STREAM. Cancelled streams stop counting against the concurrent-stream limit, so the attacker opens streams as fast as the link allows while the server pays to start each request. Google saw a peak of 398 million requests per second.
Follow-ups:
- What are the mitigations? Rate-limit stream resets per connection, close abusive connections with GOAWAY, count cancelled streams toward a budget, and patch the server.
- Why did the stream limit not stop it? It counts open streams, not streams started.
Show answer
HTTP/2 puts many requests on one TCP connection, so they share fate. One lost segment stalls every stream behind it, and one loss event halves the only congestion window, a 50 percent drop in total throughput against about 8 percent for six HTTP/1.1 connections. Requests that were independent now suffer together, which fattens the tail even when the mean falls. Measure the per-request latency distribution, not just the average, and per connection the retransmits and the number of concurrent streams, because a per-connection request count no longer means "one request at a time".
Follow-ups:
- Where does the gain come from, then? Saved round trips on many small objects and one connection setup instead of six; on 20 KB objects the gap shrinks to about 10 percent.
- What would reduce the tail? Moving lossy mobile clients to HTTP/3, where a lost packet stalls only the streams whose data it carried (HTTP/3 page).
Fun facts
- The HTTP/2 connection preface begins with the string
PRI * HTTP/2.0followed bySMon its own line. It is widely read as a joke about the PRISM surveillance programme; the RFC itself does not say so. - Client-initiated stream ids are odd and server-initiated (push) ids are even; ids are 31 bits and never reused, so a very long-lived connection must eventually be replaced.
- The default maximum frame payload is 16,384 bytes (214), and the initial flow-control window is 65,535 bytes (216 - 1).
421 Misdirected Requestexists because of connection coalescing.- Entry 2 of the HPACK static table is
:method: GET, so the most common HTTP method costs one byte on the wire.
Cheat sheet
- About 6 - browser connections per host under HTTP/1.1
- 3 RTT - to first response byte on a fresh HTTPS connection (TCP 1, TLS 1.3 1, request 1)
- 9 bytes - HTTP/2 frame header
- 65,535 bytes - initial flow-control window (6.55 Mbit/s cap at 80 ms RTT)
- 16,384 bytes - default maximum frame payload
- 61 / 4,096 - HPACK static table entries / default dynamic table bytes
- 100 - recommended minimum for
SETTINGS_MAX_CONCURRENT_STREAMS - 0 to 7, default 3 - RFC 9218 urgency
- 8.3 percent vs 50 percent - throughput lost to one loss event with six connections vs one