Skip to main content

HTTP/1.1 and HTTP/2

TL;DR
  • 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 (h2 or http/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​

ANALOGYEveryday

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.

ANALOGYFor engineers

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

HTTP/1.1: six connections, each answers one request at a timewave 1wave 2wave 3wave 4wave 5conn 1conn 2conn 3conn 4conn 5conn 6obj 1obj 7obj 13obj 19obj 25obj 2obj 8obj 14obj 20obj 26obj 3obj 9obj 15obj 21obj 27obj 4obj 10obj 16obj 22obj 28obj 5obj 11obj 17obj 23obj 29obj 6obj 12obj 18obj 24obj 30HTTP/2: one connection, 30 streams interleaved as framesconn 1s1s3s5s1s3s5s1s3s5s1s3s5...Each box is one DATA frame, labelled with its stream id. 30 streams share this lane.Same link in both panels: scheduling differs, bandwidth does not.
Top: 30 objects spread over six HTTP/1.1 connections finish in five waves, because each connection serves one object at a time. Bottom: HTTP/2 puts all 30 on one connection as interleaved frames labelled by stream id. The link is the same size in both.

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.com and static2.example.com so 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.

clientserver0 ms80 ms160 ms240 msSYNSYN-ACKACK + ClientHello (ALPN)ServerHello..FinishedFinished + GET /response beginsTCP: 1 RTTTLS 1.3: 1 RTTrequest: 1 RTTFirst response byte at 3 RTT = 240 ms when RTT is 80 ms.
At an 80 ms round trip: TCP takes 1 RTT, TLS 1.3 takes 1 RTT, and the request-response takes a third. Six HTTP/1.1 connections each walk this ladder, in parallel; HTTP/2 walks it once.

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.

StepHTTP/1.1, 6 connectionsHTTP/2, 1 connection
Setup (TCP + TLS, 2 RTT)160 ms (six handshakes in parallel)160 ms (one handshake)
Requests and slow start5 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 bytesabout 10 ms spread over the waveslast flight only: about 3 ms
Time to last byteabout 570 msabout 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.

StepHTTP/1.1, 6 connectionsHTTP/2, 1 connection
Setup160 ms160 ms
Slow startPer 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 msOne window for everything: 411 segments need flights of 10, 20, 40, 80, 160 and the last 101 segments: 6 RTT = 480 ms
Serialising the bytes5 waves x 19.2 ms = 96 ms (adds to the waves)last flight only: 101 segments = 24 ms
Time to last byteabout 736 msabout 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.

in flightdeliveredlostretransmitted copyarrived, blocked behind a loss
one TCP connection
Path and page
80 ms
50 Mbit/s
30
2 KB
0 %

TCP connection

TCP0 ms to 80 ms
TLS 1.380 ms to 160 ms
Flight 1: 10 packets, leaves at 200 ms
12345678910
Flight 2: 20 packets, leaves at 280 ms
1112131415161718192021222324252627282930
Flight 3: 12 packets, leaves at 360 ms
13691114171922252830
First byte
240 ms
Page complete
403 ms
Retransmissions
0
Streams stalled by loss
0
Longest stall
0 ms
Bytes delivered
60 KB
Finish time per streams1 400s2 400s3 400s4 400s5 400s6 401s7 401s8 401s9 401s10 401s11 401s12 401s13 401s14 401s15 401s16 401s17 402s18 402s19 402s20 402s21 402s22 402s23 402s24 402s25 402s26 402s27 402s28 403s29 403s30 403
Same path, same 60 KB, same loss seed, all three modes
ModeSetup RTTsFirst bytePage completeRetransmissionsStreams stalled
HTTP/1.12240 ms570 ms00
▸ HTTP/22240 ms403 ms00
HTTP/31160 ms323 ms00

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.

t = 0 ms. Client starts TCP + TLS 1.3.
event 0 / 5

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.

HTTP semantics (RFC 9110)HTTP/1.1 messages (RFC 9112)HTTP/2 frames (RFC 9113)TLS 1.3TLS 1.3TCPTCPmethods, URLs, headers, status codes, caching: identical in bothtext lines, one message at a timebinary frames, streams, HPACKALPN picks http/1.1ALPN picks h2about 6 connections per hostusually 1 connection per host
Same HTTP meaning on top, different framing in the middle. ALPN, carried inside the TLS handshake, is how both ends agree which framing to use.

Every frame starts with a fixed 9-byte header (RFC 9113 section 4.1):

FieldSizeWhat it does
Length24 bitspayload length; default maximum 16,384 bytes (214), raised only by SETTINGS_MAX_FRAME_SIZE
Type8 bitsDATA, HEADERS, SETTINGS, WINDOW_UPDATE, RST_STREAM, GOAWAY and others
Flags8 bitstype-specific, for example END_STREAM and END_HEADERS
Reserved1 bitunused, must be 0
Stream identifier31 bitswhich 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: GET is entry 2, so it is sent as the single byte 0x82; :scheme: https is entry 7, byte 0x87; :path: / is entry 4, byte 0x84.
  • 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.1 is 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.

FeatureHTTP/1.1HTTP/2
Connections per hostabout 6 in browsersusually 1
In flight per connection1 request (pipelining unused)many streams, 100 or more recommended
Wire formattextbinary frames
Header compressionnone (Content-Encoding covers bodies only)HPACK
Head-of-line blockingat HTTP and at TCPremoved at HTTP, still at TCP
TLS in practiceoptionalrequired by browsers
Prioritisationnone (connection count is the lever)RFC 7540 tree deprecated; RFC 9218 urgency
Server pushnonespecified, 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.

LayerWhat waitsFixed by
HTTP/1.1 application layerresponses on a connection, in request orderHTTP/2 streams
TCPevery byte after a lost segment, until it is retransmittedQUIC, 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.

On the wire (sender)In the receiver's TCP bufferWhat the HTTP/2 layer can readseg 1s1seg 2s3seg 3s5seg 4s1 lostseg 5s3seg 6s5seg 7s1seg 8s3seg 9s5seg 1readyseg 2readyseg 3readygapmissingseg 5heldseg 6heldseg 7heldseg 8heldseg 9heldseg 1s1seg 2s3seg 3s5No stream gets data: segments 5 to 9 wait for segment 4s3 and s5 lost nothing; they stall behind s1's segmentRepair takes about one RTT with fast retransmit, longer if the retransmission timer has to fire.
Three streams interleaved over one TCP connection. Segment 4 is lost. Segments 5 to 9 arrive but the kernel withholds them, so s3 and s5, which lost nothing, stall too.

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 saysProduction doesWhy
HTTP/2 is always faster than HTTP/1.1It 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 loseOne connection means one congestion window and one TCP queue
HTTP/2 eliminates head-of-line blockingIt removes it across streams at the HTTP layer only; TCP still holds every stream behind a lost segmentTCP delivers one ordered byte stream and has no idea about streams
Bundling and spriting are obsolete under h2Teams still bundle, in fewer and larger chunksFewer requests cut per-request overhead and a bundle compresses better as one file; cache granularity is the trade-off
Domain sharding raises parallelismUnder h2 it is removed, because it creates extra connections, DNS lookups and cold windowsOne connection already carries every stream; coalescing may merge shards anyway
Pipelining solves head-of-line blockingBrowsers do not use it; h2 replaced itResponses must return in request order, so the queue is still FIFO; proxy bugs made it unsafe
HTTP/2 push preloads resourcesRemoved from Chrome 106; use preload hints or 103 Early HintsServer cannot see the client's cache, so pushes were often wasted
HTTP/2 needs no TLSBrowsers speak h2 only over TLS; h2c is used inside private networksBrowser 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_version offers 1.0 and 1.1 only, so a plain proxy_pass upstream is HTTP/1.x; h2 to a gRPC upstream goes through grpc_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​

HISTORY
  • 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.

curl -sI --http1.1 https://example.com -o /dev/null -w '%{http_version}\n'
curl -sI --http2 https://example.com -o /dev/null -w '%{http_version}\n'
1.1
2

%{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:

curl -sv --http2 https://example.com -o /dev/null 2>&1 | grep -E 'ALPN|using HTTP|\[HTTP/2\]'
* ALPN: curl offers h2,http/1.1
* ALPN: server accepted h2
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://example.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: example.com]
* [HTTP/2] [1] [:path: /]

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:

curl -sv --http2 https://example.com https://example.com/ -o /dev/null -o /dev/null 2>&1 | grep -E 'Connected|Re-using'
* Connected to example.com (172.66.147.243) port 443
* Re-using existing connection with host example.com

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:

openssl s_client -alpn h2 -connect example.com:443 </dev/null 2>/dev/null | grep ALPN
ALPN protocol: h2

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​

GOTCHAS5
HTTP/2 removes head-of-line blocking
Multiplexing gives you more bandwidth
HTTP/2 needs no TLS
Server push is a good optimisation
Keep domain sharding and sprite sheets, they always help

Interview questions​

INTERVIEWWhat is the difference between HTTP/1.1 and HTTP/2?junior
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.
INTERVIEWWhat is head-of-line blocking at the HTTP layer versus the TCP layer?mid
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).
INTERVIEWWhy did domain sharding and sprites become anti-patterns under HTTP/2?mid
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.
INTERVIEWHow is HTTP/2 negotiated?mid
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: h2c route 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.
INTERVIEWWhat is HPACK, and why not just gzip the headers?mid
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.
INTERVIEWWhat was HTTP/2 Rapid Reset?senior
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.
INTERVIEWAfter moving from an HTTP/1.1 connection pool to HTTP/2, p99 latency got worse while average latency improved. Why, and what do you measure?senior
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​

FACTSHTTP/2 trivia
  • The HTTP/2 connection preface begins with the string PRI * HTTP/2.0 followed by SM on 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 Request exists 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​

NUMBERSHTTP/1.1 and HTTP/2 numbers to remember
  • 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
DECISIONHTTP/1.1, HTTP/2 or HTTP/3?
Many small objects, low loss, TLS available→HTTP/2
Mobile or lossy clients, many concurrent streams→HTTP/3 (with HTTP/2 fallback)
Legacy client or intermediary that only speaks 1.x→HTTP/1.1
gRPC between services→HTTP/2
Cleartext inside a trusted network→HTTP/1.1 or h2c with prior knowledge
Pick this when: you can say which of loss, request count and middlebox policy dominates your clients