HTTP/2 Protocol
One TCP connection, many streams. HTTP/2 removed the per-connection bottleneck that shaped a decade of frontend workarounds, then quietly deprecated its own priority scheme. This module builds the frame-level model you need to read an h2 trace: how bytes become frames, why HPACK makes the second request cheap, and which of the two flow-control windows is actually stalling you.
What you'll explore
- 01
Bytes Become Frames
HTTP/1.1 was a text protocol you could type by hand. HTTP/2 is a binary framing layer: every message is a sequence of length-prefixed frames carrying a 9-octet header of length, type, flags and stream ID. The connection is a single byte stream that must be re-assembled into frames before anything resembling a request exists. That is why a truncated h2 capture tells you nothing until you find a frame boundary, and why max_frame_size is a real tuning knob rather than cosmetics. The size field is 24 bits, so the protocol ceiling is 16777215 — but every implementation negotiates in multiples of the 16384 default, and the slider here moves on that grid.
- 02
Many Streams, One Connection
Every request opens a stream with its own odd-numbered ID, and frames from different streams interleave freely on the same connection. This is the feature that killed domain sharding and sprite sheets. It is also where the protocol's most famous mistake lives: RFC 7540 shipped an elaborate priority tree of dependencies and weights, and RFC 9113 deprecated the whole thing after implementations could not agree on it. nghttp2 v1.70.0 ignores the dependency scheme outright.
- 03
The Second Request Is Cheap
Request headers are enormously repetitive — the same user-agent, the same cookie, the same accept-encoding, on every single request. HPACK attacks this with two tables: a fixed static table of 61 common entries present on every connection before a byte is exchanged, and a dynamic table that learns this connection's own repeated fields. A header already in a table costs a single index reference instead of its full text.
- 04
Sending What Was Never Asked For
Server push lets the server open a stream the client never requested, announced by a PUSH_PROMISE frame. The idea was to skip a round trip on resources the page will obviously need. The problem is that the server cannot see the client's cache, so a returning visitor receives bytes it already has. A client has exactly two refusals available and neither is good: pre-emptively advertise SETTINGS_ENABLE_PUSH as 0 and give up push entirely, or accept the promise and cancel the stream after the fact, by which point the bytes are already in flight. What it cannot do is say 'push what I lack, skip what I hold' — that needs cache knowledge the protocol never gives the server. Chromium removed push on those grounds, and did not merely stop using it: SpdySession::OnPushPromise() (chromium-154.0.8037.57 net/spdy/spdy_session.cc:3162) discards all three parameters and calls DoDrainSession(ERR_HTTP2_PROTOCOL_ERROR, "PUSH_PROMISE received") (:3166), so a pushed stream now tears down the connection. 103 Early Hints replaced it by letting the client, which does know its own cache, make the request itself.
- 05
Two Windows, Not One
HTTP/2 runs credit-based flow control at two independent levels simultaneously: each stream has its own window, and the whole connection has another that every stream draws from. Both must have room or the data waits. This is the single most common source of mysterious h2 stalls — one large download drains the shared connection window and unrelated small requests freeze while their own stream windows sit wide open.
Validated against nghttp2 (HTTP/2 framing + HPACK reference implementation) nghttp2-1.70.0 — lib/nghttp2_session.c and friends. Every simulated behavior cites a real source function, and a server-side correlation engine computes how each layer's state cascades into the next. See the methodology →
This module is part of zerohop Pro — it unlocks alongside the full kata library, progress tracking, and weak-area analysis.