>_
Published on

HTTP/2 Explained: A Digestible Guide to RFC 7540

Written by Claude

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

FieldSizeDescription
Length24 bitsPayload length (0 to 16,777,215 bytes)
Type8 bitsFrame type (DATA, HEADERS, SETTINGS, etc.)
Flags8 bitsType-specific flags
R1 bitReserved, must be 0
Stream ID31 bitsWhich stream this frame belongs to (0 = connection-level)

Important: The 9-byte header is NOT included in the Length field.

Frame Types

TypeIDPurpose
DATA0x0Actual payload (response body, request body)
HEADERS0x1HTTP headers (compressed)
PRIORITY0x2Stream priority information
RST_STREAM0x3Terminate a stream
SETTINGS0x4Connection configuration
PUSH_PROMISE0x5Server push notification
PING0x6Keep-alive / RTT measurement
GOAWAY0x7Graceful connection shutdown
WINDOW_UPDATE0x9Flow control
CONTINUATION0xAContinuation 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 OKCSS 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 OKJS responds second
0.16  | DATA (stream=5) "function..."
0.16  | DATA (stream=5, END_STREAM)
      |
0.20  | HEADERS (stream=1) 200 OKHTML 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:

  1. Window starts at 65,535 bytes
  2. Sending 10,000 bytes of DATA → window decreases to 55,535
  3. Can't send more than window allows
  4. Wait for WINDOW_UPDATE from receiver

Receiver's perspective:

  1. Receives 10,000 bytes of DATA
  2. Processes data (e.g., writes to disk, sends to app)
  3. Sends WINDOW_UPDATE(10,000) back
  4. 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: GETReference to index 2 in static table
:path: /style.cssNew path, add to dynamic table
:scheme: https        ← Reference to index 7 in static table
:authority: example.comReference to dynamic table entry
user-agent: Mozilla/5.0 Chrome/120.0Reference 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-headerHTTP/1.1 EquivalentExample
:methodRequest line methodGET
:pathRequest line path/index.html?q=search
:schemeURL schemehttps
:authorityHost headerexample.com:443
:statusStatus line code200

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:

  1. Binary framing: Everything is frames, not text
  2. Multiplexing: Multiple streams on one TCP connection
  3. No HTTP HOL blocking: Streams are independent
  4. Header compression: HPACK reduces redundancy
  5. Flow control: Per-stream and per-connection windows
  6. Server push: Server can proactively send resources
  7. Same semantics: GET, POST, 200, 404, all unchanged
  8. TLS required: Browsers only support h2 (over TLS)
  9. One connection: No more 6-8 connections per domain
  10. 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