Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HTTP/2: Multiplexing Revolution

HTTP/2 (2015) reimagined how HTTP works at the wire level while maintaining full compatibility with HTTP/1.1 semantics. The result: dramatically faster page loads with no application changes required.

The Core Innovation: Multiplexing

HTTP/2’s killer feature is multiplexing—sending multiple requests and responses over a single TCP connection simultaneously:

HTTP/1.1 (head-of-line blocking):
┌─────────────────────────────────────────────────────────────────────────┐
│  Connection 1: [──────Req 1──────][──────Req 2──────][──Req 3──]       │
│  Connection 2: [──────Req 4──────][──Req 5──]                          │
│  Connection 3: [──Req 6──][──────Req 7──────]                          │
│                                                                         │
│  Sequential on each connection. Multiple connections needed.            │
└─────────────────────────────────────────────────────────────────────────┘

HTTP/2 (multiplexed):
┌─────────────────────────────────────────────────────────────────────────┐
│  Single connection:                                                     │
│    [R1][R2][R3][R1][R4][R2][R5][R3][R1][R6][R7]...                      │
│                                                                         │
│  All requests interleaved on one connection!                            │
│  No head-of-line blocking at HTTP level.                                │
└─────────────────────────────────────────────────────────────────────────┘

Binary Framing Layer

HTTP/2 replaces text with binary frames:

HTTP/1.1 (text):
┌────────────────────────────────────────┐
│ GET /page HTTP/1.1\r\n                 │
│ Host: example.com\r\n                  │
│ Accept: text/html\r\n                  │
│ \r\n                                   │
└────────────────────────────────────────┘

HTTP/2 (binary frames):
┌────────────────────────────────────────┐
│ ┌─────────────┐ ┌─────────────┐        │
│ │HEADERS Frame│ │ DATA Frame  │        │
│ │ Stream ID: 1│ │ Stream ID: 1│        │
│ │ (compressed)│ │ (payload)   │        │
│ └─────────────┘ └─────────────┘        │
└────────────────────────────────────────┘

Binary format:
  + Efficient parsing (no text scanning)
  + Compact representation
  + Clear frame boundaries
  - Not human-readable (need tools)

Frame Structure

Every HTTP/2 message is a series of frames:

Frame Format:
┌────────────────────────────────────────────────────────────────────┐
│ Length (24 bits) │ Type (8) │ Flags (8) │ R │ Stream ID (31 bits) │
├────────────────────────────────────────────────────────────────────┤
│                        Frame Payload                               │
└────────────────────────────────────────────────────────────────────┘

Length:    Size of payload (max 16KB default, configurable)
Type:      DATA, HEADERS, PRIORITY, RST_STREAM, SETTINGS, etc.
Flags:     Type-specific flags (END_STREAM, END_HEADERS, etc.)
Stream ID: Which stream this frame belongs to

Frame Types

┌──────────────────────────────────────────────────────────────────┐
│  Type        │ Purpose                                           │
├──────────────┼───────────────────────────────────────────────────┤
│  DATA        │ Request/response body data                        │
│  HEADERS     │ Request/response headers (compressed)             │
│  PRIORITY    │ Stream priority information                       │
│  RST_STREAM  │ Terminate a stream                                │
│  SETTINGS    │ Connection configuration                          │
│  PUSH_PROMISE│ Server push notification                          │
│  PING        │ Connection health check                           │
│  GOAWAY      │ Graceful connection shutdown                      │
│  WINDOW_UPDATE│ Flow control window adjustment                   │
│  CONTINUATION│ Continuation of HEADERS                           │
└──────────────┴───────────────────────────────────────────────────┘

Streams

A stream is a bidirectional sequence of frames within a connection:

Single HTTP/2 connection with multiple streams:

┌─────────────────────────────────────────────────────────────────────┐
│                         TCP Connection                              │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ Stream 1: [HEADERS]──[DATA]──[DATA]──[DATA]                 │   │
│  │ Stream 3: [HEADERS]──[DATA]                                 │   │
│  │ Stream 5: [HEADERS]                                         │   │
│  │ Stream 7: [HEADERS]──[DATA]──[DATA]                         │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

Stream IDs:
  - Odd numbers: Client-initiated
  - Even numbers: Server-initiated (push)
  - 0: Connection-level messages (SETTINGS, PING, GOAWAY)

Request/Response as Streams

HTTP/2 Request (Stream 1):
┌────────────────────────────────────────────────────────────────────┐
│  HEADERS Frame (Stream 1)                                          │
│    :method = GET                                                   │
│    :path = /index.html                                             │
│    :scheme = https                                                 │
│    :authority = example.com                                        │
│    accept = text/html                                              │
│    END_HEADERS, END_STREAM flags                                   │
└────────────────────────────────────────────────────────────────────┘

HTTP/2 Response (Stream 1):
┌────────────────────────────────────────────────────────────────────┐
│  HEADERS Frame (Stream 1)                                          │
│    :status = 200                                                   │
│    content-type = text/html                                        │
│    END_HEADERS flag                                                │
├────────────────────────────────────────────────────────────────────┤
│  DATA Frame (Stream 1)                                             │
│    [HTML content...]                                               │
│    END_STREAM flag                                                 │
└────────────────────────────────────────────────────────────────────┘

Header Compression (HPACK)

HTTP/2 compresses headers using HPACK:

HTTP/1.1 headers (sent every request):
  Host: example.com                    ~17 bytes
  User-Agent: Mozilla/5.0...           ~70 bytes
  Accept: text/html,application/...    ~100 bytes
  Accept-Language: en-US,en;q=0.5      ~25 bytes
  Accept-Encoding: gzip, deflate       ~25 bytes
  Cookie: session=abc123;...           ~50+ bytes
  ──────────────────────────────────────────────
  Total: ~300 bytes per request!

  10 requests = 3KB just in headers!

HPACK compression:
  1. Static table: 61 common headers (predefined)
  2. Dynamic table: Recently used headers (learned)
  3. Huffman coding: Compress literal values

  First request: ~300 bytes → ~150 bytes (Huffman)
  Second request: Same headers → ~30 bytes (indexed!)

  10 requests ≈ 300 bytes total (vs 3KB)

HPACK Example

First request headers sent:
  :method: GET           → Index 2 (static table)
  :path: /index.html     → Literal, indexed
  :authority: example.com → Literal, indexed, Huffman
  accept: text/html      → Literal, indexed

Dynamic table after request:
  [62] :path: /index.html
  [63] :authority: example.com
  [64] accept: text/html

Second request to same server:
  :method: GET           → Index 2 (static)
  :path: /style.css      → Literal (new path)
  :authority: example.com → Index 63 (dynamic!)
  accept: text/css       → Literal (different)

Much smaller because authority is now indexed!

Server Push

Servers can proactively send resources:

Without server push:
  Client: GET /index.html
  Server: (sends HTML)
  Client: (parses, sees style.css link)
  Client: GET /style.css       ← Extra round trip!
  Server: (sends CSS)

With server push:
  Client: GET /index.html
  Server: PUSH_PROMISE /style.css    ← "I'll send this too"
  Server: (sends HTML)
  Server: (sends CSS on separate stream)
  Client: (already has CSS when parsing HTML!)

Saves round trip for critical resources.

Server Push Caveats

Push sounds great but has issues:

1. May push already-cached resources
   Server doesn't know client cache state
   Waste bandwidth pushing what client has

2. Priority problems
   Pushed resources may compete with requested ones
   Can slow down critical content

3. Limited browser support
   Chrome deprecated push support (2022)
   Most CDNs recommend disabling

Alternative: 103 Early Hints
  Server sends hints before full response
  Client can preload without full push complexity

Stream Prioritization

Clients can indicate resource importance:

Priority information:
  - Weight: 1-256 (relative importance)
  - Dependency: Stream this depends on

Example:
  Stream 1 (HTML):      Weight=256 (highest)
  Stream 3 (CSS):       Weight=128, depends on Stream 1
  Stream 5 (JS):        Weight=128, depends on Stream 1
  Stream 7 (image):     Weight=64, depends on Stream 3

Priority tree:
          [Stream 1 - HTML]
              /       \
    [Stream 3-CSS]  [Stream 5-JS]
          |
    [Stream 7-image]

Server should:
  1. Complete Stream 1 first
  2. Then CSS and JS equally
  3. Images last

Reality: Server implementation varies
         Many servers ignore priorities

Flow Control

HTTP/2 has stream-level flow control:

Connection flow control:
  Each side advertises receive window
  Similar to TCP flow control

Stream flow control:
  Each stream has its own window
  Prevents one stream from consuming all bandwidth

WINDOW_UPDATE frame:
  Signals capacity for more data

┌────────────────────────────────────────────────────────────────────┐
│  Stream 1: Window=65535                                            │
│  Stream 3: Window=65535                                            │
│  Connection: Window=1048576                                        │
│                                                                    │
│  Server sends 32768 bytes on Stream 1:                             │
│    Stream 1: Window=32767                                          │
│    Connection: Window=1015808                                      │
│                                                                    │
│  Client sends WINDOW_UPDATE (Stream 1, 32768):                     │
│    Stream 1: Window=65535 (restored)                               │
└────────────────────────────────────────────────────────────────────┘

HTTP/2 Connection Setup

HTTP/2 requires TLS in practice (browsers require HTTPS):

1. TCP handshake
2. TLS handshake (ALPN negotiates HTTP/2)
3. HTTP/2 connection preface:
   Client sends: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
   Both send: SETTINGS frame

4. Ready for requests!

ALPN (Application-Layer Protocol Negotiation):
  Client TLS hello includes: "I support h2, http/1.1"
  Server chooses: "Let's use h2"
  Connection established as HTTP/2

The Remaining Problem: TCP HOL Blocking

HTTP/2 solved HTTP-level head-of-line blocking but TCP has its own:

HTTP/2 over TCP problem:

Stream 1: [Frame 1][Frame 2][Frame 3]
Stream 3: [Frame A][Frame B]
Stream 5: [Frame X][Frame Y]

TCP sees: [1][A][2][X][B][3][Y]

If TCP packet containing [2] is lost:
  TCP retransmits [2]
  ALL subsequent data waits!
  Frames [X][B][3][Y] all blocked

Even though X,B,Y are independent streams!

This is TCP-level head-of-line blocking.
HTTP/3 solves this with QUIC.

HTTP/2 Performance

When HTTP/2 shines:
  - Many small resources (multiplexing wins)
  - High latency connections (fewer round trips)
  - Header-heavy requests (compression helps)
  - HTTPS (required anyway, TLS overhead amortized)

When HTTP/2 helps less:
  - Single large download (one stream anyway)
  - Very low latency networks (overhead matters less)
  - Lossy networks (TCP HOL blocking hurts)

Typical improvements:
  Page load time: 10-50% faster
  Time to first byte: Similar or slightly better
  Number of connections: 1 vs 6+ (simpler)

Debugging HTTP/2

# curl with HTTP/2
$ curl -I --http2 https://example.com
HTTP/2 200
content-type: text/html

# nghttp client
$ nghttp -nv https://example.com

# Chrome DevTools
  Network tab → Protocol column shows "h2"

# Wireshark
  Filter: http2
  Decode TLS with SSLKEYLOGFILE

Summary

HTTP/2’s key innovations:

FeatureBenefit
Binary framingEfficient parsing, clear boundaries
MultiplexingMultiple requests on one connection
Header compression~85% reduction in header size
Stream prioritizationBetter resource loading order
Server pushProactive resource delivery
Flow controlPer-stream bandwidth management

Limitations:

  • TCP head-of-line blocking remains
  • Server push deprecated in browsers
  • Complexity increased

HTTP/2 is a significant improvement over HTTP/1.1, but TCP’s head-of-line blocking motivated HTTP/3’s move to QUIC—which we’ll cover next.