- Published on
HTTP/2 Explained: A Digestible Guide to RFC 7540
HTTP/2 Explained: A Digestible Guide to RFC 7540
RFC 7540 defines HTTP/2, a major evolution of HTTP that keeps the same semantics but completely redesigns how messages are transmitted. This guide distills the protocol into digestible concepts.
The HTTP/1.1 Problems HTTP/2 Solves
Before diving into HTTP/2, understand what it fixes:
1. Head-of-Line Blocking
HTTP/1.1 problem:
Connection 1: [Request A] → waiting for response → [Request B]
↑
Blocked! B can't start until A completes
Even with pipelining, responses must come back in order. If Response A is slow, Response B is blocked even if it's ready.
HTTP/1.1 workaround: Open 6-8 parallel TCP connections per domain. Wasteful and limited.
2. Redundant Headers
Every HTTP/1.1 request sends full headers:
GET /page1.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0...
Cookie: session=abc123; prefs=xyz; ...
Accept: text/html,application/xhtml+xml...
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
GET /page2.html HTTP/1.1
Host: example.com ← Same as above
User-Agent: Mozilla/5.0... ← Same as above
Cookie: session=abc123; prefs=xyz; ... ← Same as above
Accept: text/html,application/xhtml+xml... ← Same as above
Accept-Encoding: gzip, deflate, br ← Same as above
Accept-Language: en-US,en;q=0.9 ← Same as above
Hundreds of bytes repeated for every request. Wasteful, especially on slow networks.
3. No Request Prioritization
All requests are equal. Can't say "fetch CSS before images" or "cancel this slow request."
4. Server Can't Push
Server must wait for client to request every resource. Even if server knows client will need style.css after parsing index.html, it must wait for the request.
What HTTP/2 Does Differently
HTTP/2 addresses all of these while keeping HTTP semantics identical. You still have GET, POST, status codes, headers, etc. Just a completely different wire format.
Key Concept: Binary Framing
HTTP/1.1: Text-based, delimited by CRLF
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n
\r\n
HTTP/2: Binary frames with fixed structure
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
Every HTTP/2 communication is broken into frames. No more parsing text, counting CRLFs, or dealing with whitespace.
Frame Structure
Every frame has a 9-byte header:
Frame Header Fields
| Field | Size | Description |
|---|---|---|
| Length | 24 bits | Payload length (0 to 16,777,215 bytes) |
| Type | 8 bits | Frame type (DATA, HEADERS, SETTINGS, etc.) |
| Flags | 8 bits | Type-specific flags |
| R | 1 bit | Reserved, must be 0 |
| Stream ID | 31 bits | Which stream this frame belongs to (0 = connection-level) |
Important: The 9-byte header is NOT included in the Length field.
Frame Types
| Type | ID | Purpose |
|---|---|---|
| DATA | 0x0 | Actual payload (response body, request body) |
| HEADERS | 0x1 | HTTP headers (compressed) |
| PRIORITY | 0x2 | Stream priority information |
| RST_STREAM | 0x3 | Terminate a stream |
| SETTINGS | 0x4 | Connection configuration |
| PUSH_PROMISE | 0x5 | Server push notification |
| PING | 0x6 | Keep-alive / RTT measurement |
| GOAWAY | 0x7 | Graceful connection shutdown |
| WINDOW_UPDATE | 0x9 | Flow control |
| CONTINUATION | 0xA | Continuation of HEADERS |
Streams and Multiplexing
The killer feature of HTTP/2.
What's a Stream?
A stream is an independent, bidirectional sequence of frames exchanged between client and server.
Single TCP Connection
┌──────────────────────────────────────────────────────┐
│ Stream 1: GET /index.html │
│ → HEADERS frame (stream=1) │
│ ← HEADERS frame (stream=1) │
│ ← DATA frame (stream=1) │
│ ← DATA frame (stream=1, END_STREAM) │
│ │
│ Stream 3: GET /style.css (concurrent with Stream 1!) │
│ → HEADERS frame (stream=3) │
│ ← HEADERS frame (stream=3) │
│ ← DATA frame (stream=3, END_STREAM) │
│ │
│ Stream 5: GET /script.js (concurrent too!) │
│ → HEADERS frame (stream=5) │
│ ← HEADERS frame (stream=5) │
│ ← DATA frame (stream=5, END_STREAM) │
└──────────────────────────────────────────────────────┘
Key points:
- Multiple streams on one TCP connection
- Frames from different streams are interleaved
- No head-of-line blocking: Stream 3 can complete before Stream 1
- Stream IDs: Client uses odd numbers (1, 3, 5...), server uses even (2, 4, 6...)
Multiplexing in Action
Actual frame sequence on the wire:
Time | Frame
------|---------------------------------------------
0.00 | HEADERS (stream=1) GET /index.html
0.01 | HEADERS (stream=3) GET /style.css
0.02 | HEADERS (stream=5) GET /script.js
|
0.10 | HEADERS (stream=3) 200 OK ← CSS responds first
0.11 | DATA (stream=3) "body { ..." ← CSS data
0.11 | DATA (stream=3, END_STREAM) ← CSS complete
|
0.15 | HEADERS (stream=5) 200 OK ← JS responds second
0.16 | DATA (stream=5) "function..."
0.16 | DATA (stream=5, END_STREAM)
|
0.20 | HEADERS (stream=1) 200 OK ← HTML responds last
0.21 | DATA (stream=1) "<!DOCTYPE..."
0.22 | DATA (stream=1) "...html..."
0.23 | DATA (stream=1, END_STREAM)
Notice:
- Requests sent concurrently
- Responses arrive in any order (CSS → JS → HTML)
- Smaller resources can complete first
- No blocking: All happening on ONE TCP connection
Stream States
Streams have a lifecycle:
+--------+
send PP | | recv PP
,--------| idle |--------.
/ | | \
v +--------+ v
+----------+ | +----------+
| | | send H / | |
| reserved | | recv H | reserved |
| (local) | | | (remote) |
+----------+ v +----------+
| +--------+ |
| recv ES | | send ES |
| ,-------| open |-------. |
| / | | \ |
v v +--------+ v v
+----------+ | +----------+
| half | | | half |
| closed | | send R / | closed |
| (remote) | | recv R | (local) |
+----------+ | +----------+
| | |
| send ES / | recv ES / |
| send R / v send R / |
| recv R +--------+ recv R |
`----------->| |<-----------'
| closed |
+--------+
Abbreviations:
- H = HEADERS frame
- PP = PUSH_PROMISE frame
- ES = END_STREAM flag
- R = RST_STREAM frame
States:
- idle: Initial state, stream doesn't exist yet
- open: Both sides can send frames
- half-closed (local): You've sent END_STREAM, but can still receive
- half-closed (remote): They've sent END_STREAM, but you can still send
- closed: Stream is done
Flow Control
HTTP/2 has per-stream and per-connection flow control to prevent fast senders from overwhelming slow receivers.
The Solution: WINDOW_UPDATE
Each stream and the connection have a flow control window (initial size: 65,535 bytes).
Sender's perspective:
- Window starts at 65,535 bytes
- Sending 10,000 bytes of DATA → window decreases to 55,535
- Can't send more than window allows
- Wait for WINDOW_UPDATE from receiver
Receiver's perspective:
- Receives 10,000 bytes of DATA
- Processes data (e.g., writes to disk, sends to app)
- Sends WINDOW_UPDATE(10,000) back
- Sender's window increases by 10,000
Header Compression (HPACK)
HTTP/2 uses HPACK (RFC 7541) to compress headers.
How It Works
HPACK maintains a dynamic table of previously seen headers:
First request:
:method: GET
:path: /index.html
:scheme: https
:authority: example.com
user-agent: Mozilla/5.0 Chrome/120.0
cookie: session=abc123
HPACK encodes this, adds to dynamic table.
Second request:
:method: GET ← Reference to index 2 in static table
:path: /style.css ← New path, add to dynamic table
:scheme: https ← Reference to index 7 in static table
:authority: example.com ← Reference to dynamic table entry
user-agent: Mozilla/5.0 Chrome/120.0 ← Reference to dynamic table
cookie: session=abc123 ← Reference to dynamic table
Result: Headers that were 300+ bytes now compress to ~30 bytes.
Pseudo-Headers
HTTP/2 introduces special headers starting with ::
| Pseudo-header | HTTP/1.1 Equivalent | Example |
|---|---|---|
:method | Request line method | GET |
:path | Request line path | /index.html?q=search |
:scheme | URL scheme | https |
:authority | Host header | example.com:443 |
:status | Status line code | 200 |
Server Push
Server can proactively send resources the client will need.
Traditional Flow (HTTP/1.1)
Client Server
│ │
│ GET /index.html │
│──────────────────────────────>│
│ │
│ 200 OK │
│ <html> │
│ <link rel=stylesheet │
│ href="/style.css"> │
│ </html> │
│<──────────────────────────────│
│ │
│ (parses HTML, discovers CSS) │
│ │
│ GET /style.css │ ← Wasted round trip!
│──────────────────────────────>│
│ │
│ 200 OK (CSS) │
│<──────────────────────────────│
HTTP/2 Server Push
Client Server
│ │
│ GET /index.html │
│──────────────────────────────>│
│ │
│ PUSH_PROMISE (stream=2) │
│ :method: GET │
│ :path: /style.css │
│<──────────────────────────────│
│ (Server announces push) │
│ │
│ HEADERS (stream=1) │
│ :status: 200 │
│<──────────────────────────────│
│ │
│ DATA (stream=1) │
│ <html>...</html> │
│<──────────────────────────────│
│ │
│ HEADERS (stream=2) │
│ :status: 200 │
│<──────────────────────────────│
│ │
│ DATA (stream=2) │
│ body { ... } │
│<──────────────────────────────│
Key points:
- Server sends PUSH_PROMISE before sending the pushed resource
- Client can reject push with RST_STREAM if already cached
- Pushed resources use even-numbered streams (server-initiated)
- Must be cacheable and safe (GET/HEAD only)
Security Considerations
1. TLS is Effectively Required
RFC says HTTP/2 can run over cleartext (h2c), but:
- Browsers ONLY support HTTP/2 over TLS
- Most servers require TLS for HTTP/2
- In practice: HTTP/2 = HTTPS
2. Cipher Suite Restrictions
HTTP/2 blacklists weak cipher suites:
- No RC4
- No 3DES
- No export-grade ciphers
- Must use AEAD or strong ciphers
3. Compression Attacks (CRIME/BREACH)
HPACK compression can leak secrets. Mitigations:
- Don't compress sensitive headers
- Use padding
- Randomize header order
Summary: The Essentials
If you remember nothing else:
- Binary framing: Everything is frames, not text
- Multiplexing: Multiple streams on one TCP connection
- No HTTP HOL blocking: Streams are independent
- Header compression: HPACK reduces redundancy
- Flow control: Per-stream and per-connection windows
- Server push: Server can proactively send resources
- Same semantics: GET, POST, 200, 404, all unchanged
- TLS required: Browsers only support
h2(over TLS) - One connection: No more 6-8 connections per domain
- Backward compatible: Can fallback to HTTP/1.1
Relationship to HTTP/3
HTTP/2 (RFC 7540):
- Transport: TCP + TLS
- Pros: Better than HTTP/1.1, widely supported
- Cons: TCP head-of-line blocking
HTTP/3 (RFC 9114):
- Transport: QUIC (UDP-based)
- Pros: No TCP HOL blocking, faster connection setup, better loss recovery
- Cons: Newer, less mature, UDP sometimes blocked
HTTP/3 keeps HTTP/2's frame model but runs over QUIC instead of TCP.
Additional Resources
- RFC 7540: HTTP/2 specification
- RFC 7541: HPACK header compression
- RFC 9113: HTTP/2 (updated version)
- RFC 9114: HTTP/3
- Ilya Grigorik's High Performance Browser Networking: Excellent HTTP/2 chapter
- http2 explained: By curl author Daniel Stenberg