Loading live status…
Loading live status…
Binary framing, multiplexing, HPACK, and the two headline features that were later removed. What HTTP/2 actually changed, where it loses to HTTP/1.1, and how to tell which protocol a site is really serving.
HTTP/2 is a rewrite of how an HTTP message is put on the wire. It is not a rewrite of HTTP.
That distinction is the whole thing, and it is the part most explanations skip. A GET /index.html with an Accept-Encoding header means exactly what it meant in 1999. Status codes are the same. Methods are the same. Caching semantics are the same. What changed is the encoding — from a text protocol you can type by hand into a telnet session, to a binary one you cannot — and the concurrency model that the new encoding makes possible.
Everything HTTP/2 is praised for and everything it is blamed for follows from one decision: all requests to an origin share a single TCP connection.
HTTP/1.1 sends one request per connection at a time. A response must finish before the next one starts.
The spec's answer to this was pipelining — send several requests without waiting — but responses still had to come back in order, so one slow response blocked every response queued behind it. That is head-of-line blocking, and it made pipelining useless in practice. Browsers disabled it. By 2010 essentially nobody shipped it.
What browsers did instead was open more connections: six per origin, by convention. Six parallel requests is better than one, and it is also the ceiling — a page with 80 assets moves them six at a time. So the industry built workarounds on top of the workaround:
img1.example.com and img2.example.com to get 6 connections per shard instead of 6 total.Each of these trades cache efficiency for concurrency. A sprite sheet has to be re-downloaded in full when one icon changes. An inlined stylesheet cannot be cached separately from the page. These were not good ideas; they were rent paid to a protocol limit.
HTTP/2 removes the limit, which makes all three obsolete — and turns two of them actively harmful, since sharding now splits a connection that was supposed to stay unified.
HTTP/1.1 is delimited by text. Headers end at a blank line, and the parser finds boundaries by scanning for CRLF. This is readable, and it is ambiguous enough that "request smuggling" is an entire vulnerability class built on two servers disagreeing about where a message ends.
HTTP/2 replaces this with fixed-shape binary frames. Every frame carries a 9-byte header:
+-----------------------------------------------+
| Length (24 bits) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+---------------+
|R| Stream Identifier (31) |
+=+=============================================+
| Frame Payload |
+-----------------------------------------------+
The length is declared, not discovered. There is no scanning for a delimiter, and no disagreement about where the body stops.
Ten frame types do all the work. The ones worth knowing:
| Frame | Purpose |
|---|---|
HEADERS |
Request or response headers; opens a stream |
DATA |
Message body |
SETTINGS |
Connection parameters, exchanged at startup |
WINDOW_UPDATE |
Flow control credit |
RST_STREAM |
Cancel one stream |
GOAWAY |
Shut down the connection gracefully |
PING |
Liveness and round-trip measurement |
Note that HEADERS and DATA are different frame types on the same stream. Headers and body are no longer one contiguous blob of text — they are separately framed, which is what allows headers to be compressed with a scheme of their own.
A stream is an independent, bidirectional sequence of frames within one connection, identified by that 31-bit stream ID. Client-initiated streams are odd-numbered, server-initiated are even.
Frames from many streams are interleaved on the wire and reassembled by ID at the other end. Ninety requests can be in flight at once over one connection, and a slow response holds up nothing but itself.
This is the feature. Everything else in HTTP/2 is either supporting machinery or a consequence.
It also comes with a limit worth knowing: SETTINGS_MAX_CONCURRENT_STREAMS, which servers commonly set to 100 or 128. Exceed it and further streams are refused rather than queued — a detail that surfaces later as a confusing browser error.
HTTP/2 eliminates head-of-line blocking at the HTTP layer. It cannot do anything about TCP.
TCP delivers a byte stream in order. If one packet is lost, the kernel holds every byte that arrived after it until the retransmission lands — and it has no idea those bytes belonged to 40 unrelated streams. One lost packet stalls the entire connection.
Under HTTP/1.1 with six connections, a lost packet stalls one of six. Under HTTP/2 with one connection, it stalls all of it.
So on a clean network HTTP/2 wins comfortably, and on a lossy network HTTP/1.1 can beat it. This is not a hypothetical; it is measurable on congested mobile links, and it is the specific problem HTTP/3 was built to solve.
HTTP/1.1 compresses bodies and not headers. This was a reasonable omission in 1997 and an expensive one by 2015: a typical request carries 500–800 bytes of headers, mostly cookies and a User-Agent string, repeated identically on every one of 80 requests.
HPACK (RFC 7541) removes that repetition with three mechanisms:
:method: GET is one byte on the wire, because both sides already agree it is index 2.Header overhead typically drops by 80–90% across a page load. On a request-heavy page this is a larger real-world win than multiplexing, and it is almost never the feature people mention.
HPACK exists at all — rather than reusing gzip — because of CRIME, an attack that recovers secrets from compressed data by observing how the compressed size changes with attacker-chosen input. HPACK's indexing is deliberately built so that a sensitive header can be marked never-indexed and kept out of the shared state.
If everything is in flight at once, the server needs to know what matters. A stylesheet that blocks rendering should not arrive after a below-the-fold image.
The original spec answered this with a dependency tree: streams declared parents and weights, forming a structure the server walked to allocate bandwidth. It was expressive, complicated, and implemented inconsistently enough that several CDNs ignored it and used their own heuristics instead.
RFC 9113, the 2022 revision that replaced the original HTTP/2 spec, deprecated the whole scheme. The replacement is RFC 9218: two values, an urgency from 0 to 7 and an incremental boolean, sent in a priority header or a PRIORITY_UPDATE frame. Less expressive, and far more likely to be implemented the same way by two vendors.
Server push let a server send a response the client had not asked for: you request index.html, the server pushes style.css alongside it on the theory that you are about to want it.
It was the most-marketed feature of HTTP/2 and it is gone.
The problem was that the server does not know what the client has cached. Pushing a stylesheet the browser already holds wastes the bandwidth you were trying to save, and the cache-digest mechanism meant to fix this never shipped. Measurements found push to be roughly neutral on average and negative often enough to be not worth the complexity.
Chrome removed support in version 106 (2022). Firefox disabled it. RFC 9113 deprecates it. If a tutorial is enthusiastic about server push, it was written before 2022.
What replaced it is simpler and works: 103 Early Hints (RFC 8297), an informational response sent while the real one is still being generated, telling the browser which resources to start fetching. The browser then applies its own cache knowledge — which the server never had.
One connection per origin, held open, is a meaningful saving on its own: a fresh HTTPS connection costs a TCP handshake plus a TLS handshake, two to three round trips before a single byte of content moves. On a 100ms link that is 200–300ms per connection avoided.
HTTP/2 goes further with connection coalescing. If example.com and assets.example.com resolve to the same IP and the certificate covers both, a browser may serve both from one connection. Two origins, one handshake.
This is also the mechanism that makes domain sharding backfire: sharding deliberately spreads assets across hostnames to defeat a limit that no longer exists, and it defeats coalescing while it is at it.
HTTP/2 does not require encryption. The spec defines h2 (over TLS) and h2c (cleartext).
No major browser has ever implemented h2c. In practice, on the web, HTTP/2 means HTTPS, and the requirement is TLS 1.2 or better with a restricted cipher list — HTTP/2 forbids a set of older ciphers and will fail the connection rather than negotiate one.
Protocol selection happens through ALPN, an extension inside the TLS handshake. The client offers ["h2", "http/1.1"] in its ClientHello, the server picks one, and the choice is made before any HTTP is spoken. There is no extra round trip — which is why HTTP/2 negotiation is free, and why a server without ALPN support silently serves HTTP/1.1 forever.
h2c does have a place server-side: behind a load balancer that terminates TLS, a proxy often speaks cleartext HTTP/2 to the backend on a trusted network.
HTTP/3 is not a further rewrite of HTTP semantics either. It moves the same framing ideas onto QUIC, a transport built on UDP.
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC (UDP) |
| Encoding | Text | Binary frames | Binary frames |
| Concurrency | ~6 connections | Streams on 1 connection | Streams on 1 connection |
| Header compression | None | HPACK | QPACK |
| TCP head-of-line blocking | Per connection | Whole connection | None |
| Handshake | TCP + TLS (2–3 RTT) | TCP + TLS (2–3 RTT) | 1 RTT, or 0-RTT resumed |
| Connection migration | No | No | Yes |
The decisive row is head-of-line blocking. QUIC understands streams at the transport layer, so a lost packet stalls only the stream whose data it carried. The other 39 keep flowing. That is HTTP/2's one structural weakness, fixed properly.
Connection migration is the underrated row. A QUIC connection is identified by a connection ID rather than by the four-tuple of IPs and ports, so moving from Wi-Fi to cellular keeps the connection alive instead of resetting it.
HTTP/3 does not make HTTP/2 obsolete. UDP is blocked or throttled on some corporate and mobile networks, so every HTTP/3 deployment keeps HTTP/2 as the fallback, advertised with an Alt-Svc header. The pair ships together.
The fastest check is one curl invocation:
curl -I -s -o /dev/null -w '%{http_version}\n' https://example.com
# 2
curl negotiates the best protocol it and the server share, so the number it prints is what actually got used. For HTTP/3, add --http3.
To see the negotiation itself rather than the result:
openssl s_client -alpn h2 -connect example.com:443 </dev/null 2>/dev/null | grep ALPN
# ALPN protocol: h2
If that line comes back empty, the server did not accept h2 — which usually means ALPN is not configured rather than that HTTP/2 is off.
In a browser, open DevTools → Network, right-click the column headers and enable Protocol. You will see h2, h3, or http/1.1 per request. This is the one worth doing on a real page, because it shows you the assets that are not on HTTP/2 — a third-party script on an old CDN will show up here and nowhere else.
ERR_HTTP2_PROTOCOL_ERROR — Chrome's catch-all for a frame it could not accept. In practice it is most often a server or intermediary emitting subtly invalid HTTP/2: a header with an uppercase name (illegal in HTTP/2, legal in HTTP/1.1), a Content-Length that disagrees with the DATA frames sent, or a connection-specific header like Transfer-Encoding that HTTP/2 forbids outright. The tell is that the same site works when you force --http1.1.
ERR_HTTP2_SERVER_REFUSED_STREAM — the server hit SETTINGS_MAX_CONCURRENT_STREAMS and sent RST_STREAM with REFUSED_STREAM. The browser retries, so this usually appears as unexplained slowness rather than a hard failure.
GOAWAY — not an error so much as a graceful shutdown: the server states the highest stream it will finish and declines new ones. Seen constantly and correctly during deploys and connection recycling.
ENHANCE_YOUR_CALM (0x0b) — the rate-limiting error code, genuinely named after a Demolition Man line. Your client is opening streams faster than the server will tolerate.
Flow-control stalls. Every stream and the connection each have a receive window, defaulting to 65,535 bytes. A client that does not send WINDOW_UPDATE promptly stalls a transfer that has no other problem. This is a common bug in hand-rolled HTTP/2 clients and almost never in browsers.
Rapid Reset (CVE-2023-44487). Worth its own mention because it is HTTP/2's design turned into a weapon. An attacker opens a stream and immediately cancels it with RST_STREAM. The cancellation frees the concurrency slot instantly, so the attacker never hits the stream limit, while the server has already begun the work. It produced the largest DDoS attacks recorded at the time, against Google, Cloudflare and AWS in October 2023. Mitigations are now standard in every major server, which is a reason to keep yours patched rather than a reason to avoid HTTP/2.
For almost everyone the answer is that it is already on. Cloudflare, Vercel, Fastly, CloudFront and every other major CDN serve HTTP/2 by default, and if you are behind one, your origin's protocol is not what visitors experience.
If you terminate TLS yourself:
# nginx 1.25.1+
listen 443 ssl;
http2 on;
# Apache with mod_http2
Protocols h2 h2c http/1.1
Three requirements, all easy to miss:
Then verify with the curl command above rather than trusting the config — the failure mode here is silent by design.
An uptime check is an HTTP request, so it inherits all of this.
Connection reuse is the part that catches people. A checker holding one HTTP/2 connection open across polls is not re-testing DNS, TCP or TLS on each poll — it is testing one path through an already-established tunnel. That is a faster check and a narrower one. An expired certificate will not be noticed by a client that has not renegotiated, which is a fine trade as long as you know you are making it.
GOAWAY is the other one. A server recycling connections during a deploy sends GOAWAY constantly and correctly. A monitor that treats it as an error reports an outage that is not happening — and the industry's most common monitoring bug is a checker that reports on its own condition as though it were reporting on the service.
That is the same failure we wrote about at length after shipping it ourselves. The protocol under the check is one more thing that shapes what a green dot actually means.