HTTP/2: The Modern Web Protocol
Binary Framing: The Foundation of HTTP/2
Read aloud is unavailable in this browser.
The Protocol Revolution
In 2015, the web underwent one of its most significant transformations since the introduction of HTTP/1.1 in 1997. After nearly two decades of incremental improvements and workarounds, HTTP/2 emerged with a radical architectural change: the replacement of HTTP's text-based protocol with a binary framing layer.
This wasn't merely a technical optimization—it was a fundamental reimagining of how web browsers and servers communicate. The binary framing layer is the foundation upon which every other HTTP/2 improvement is built: multiplexing, header compression, server push, and stream prioritization all depend on this architectural shift.
To truly understand HTTP/2, you must first understand binary framing. Every decision, every performance gain, and every implementation detail traces back to this core innovation.
By the end of this page, you will understand: (1) Why HTTP/1.1's text-based protocol became a fundamental bottleneck, (2) How binary framing transforms HTTP messages into structured frames, (3) The complete anatomy of HTTP/2 frames including headers, types, and flags, (4) How the binary layer enables all other HTTP/2 features, and (5) The practical implications for web performance and security.
The HTTP/1.1 Bottleneck
To appreciate binary framing, we must first understand why HTTP/1.1's text-based approach became problematic. HTTP/1.1 was designed in an era when web pages contained a handful of resources, connections were expensive, and simplicity was paramount.
The Text-Based Protocol:
HTTP/1.1 messages are human-readable text. A typical request looks like:
GET /index.html HTTP/1.1\r
Host: example.com\r
User-Agent: Mozilla/5.0...\r
Accept: text/html\r
Accept-Language: en-US\r
Connection: keep-alive\r
\r
This text-based format was intentional. Early protocol designers valued debuggability—you could literally read HTTP traffic with tools like telnet or netcat. But this simplicity came with severe costs that weren't apparent until web complexity exploded.
- •Head-of-Line Blocking — Each connection can process only one request-response pair at a time. Even with pipelining, responses must return in order, so a slow response blocks everything behind it.
- •Parsing Complexity — Text parsing requires scanning for delimiters (CRLF sequences), handling variable-length headers, and managing edge cases in character encoding. This is computationally expensive and error-prone.
- •Ambiguous Message Boundaries — Determining where one message ends and another begins requires parsing Content-Length or Transfer-Encoding headers, with multiple fallback rules for malformed messages.
- •Header Redundancy — Every request repeats identical headers (cookies, user-agent, accept headers). A typical page might send 100+ requests, each carrying kilobytes of duplicate header data.
- •Connection Limits — Browsers limit concurrent connections per domain (typically 6). Developers invented workarounds like domain sharding, which add latency and complexity.
Modern web pages average 70+ resources (scripts, stylesheets, images, fonts, API calls). With HTTP/1.1's connection limits and head-of-line blocking, loading these resources efficiently requires complex workarounds: connection pooling, domain sharding, resource inlining, sprite sheets, and concatenation. Each workaround adds development complexity and often makes caching less effective.
The Parsing Tax:
Consider what happens when a server processes an HTTP/1.1 request:
- Read bytes until CRLF — Find the request line boundary
- Parse request method — Scan characters until space
- Parse URI — Scan until space, handle percent-encoding
- Parse HTTP version — Scan until CRLF
- Parse headers — Loop: read until CRLF, split on colon, handle line folding, repeat until empty line
- Determine body presence — Check method, Content-Length, Transfer-Encoding
- Read body — Either fixed-length or chunked encoding
Each step involves string operations, buffer management, and careful handling of edge cases. At scale, this text parsing becomes a measurable CPU cost. More importantly, the variable-length nature of text makes it impossible to divide a stream into independent, parallel-processable units—which is exactly what binary framing solves.
The Binary Framing Layer
HTTP/2's binary framing layer solves HTTP/1.1's fundamental problems by introducing a structured binary format between the application (HTTP semantics) and the transport (TCP). This layer doesn't change what HTTP means—GET, POST, headers, and bodies remain conceptually identical—but completely transforms how messages are encoded and transmitted.
The Key Insight:
Instead of sending HTTP messages as monolithic text blobs, HTTP/2 breaks them into small frames with fixed headers and well-defined boundaries. This simple change enables massive improvements:
- Frames are self-describing — Fixed-size headers tell you exactly how many bytes to read
- Frames are independent — Each frame can be processed without reference to others
- Frames can be interleaved — Multiple requests/responses share one connection simultaneously
- Frames enable efficient encoding — Binary representations are compact and fast to parse
HTTP/2's binary format doesn't mean you can't debug it. Tools like Wireshark, Chrome DevTools, and nghttp2 decode frames into readable representations. The 'binary' designation refers to the wire format—the structured byte layout that enables efficient parsing and multiplexing. HTTP semantics (methods, headers, status codes) remain identical to HTTP/1.1.
The Layering Architecture:
HTTP/2 introduces a clean separation of concerns:
┌─────────────────────────────────────┐
│ HTTP Semantics (Application) │
│ GET, POST, Headers, Status Codes │
├─────────────────────────────────────┤
│ Binary Framing Layer (HTTP/2) │
│ Frames, Streams, Flow Control │
├─────────────────────────────────────┤
│ Transport Layer (TCP/TLS) │
│ Reliable, Ordered Byte Stream │
└─────────────────────────────────────┘
This layering means that application developers continue using familiar HTTP concepts—request methods, headers, response codes—while the binary framing layer handles efficient transmission transparently. The transition from HTTP/1.1 to HTTP/2 requires no changes to application logic, only infrastructure updates.
Frame Structure and Anatomy
Every HTTP/2 frame follows a precise binary structure. Understanding this structure is essential for grasping how HTTP/2 achieves its performance characteristics.
The Fixed Frame Header:
Every HTTP/2 frame begins with a 9-byte header:
+-----------------------------------------------+
| Length (24 bits) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0 to 2^24-1 bytes) |
+---------------------------------------------------------------+
This fixed structure is the key to HTTP/2's parsing efficiency. Upon receiving data, a parser knows immediately:
- Bytes 0-2 (Length): Exactly how many payload bytes follow
- Byte 3 (Type): What kind of frame this is
- Byte 4 (Flags): Type-specific control bits
- Bytes 5-8 (Stream ID): Which logical stream this frame belongs to
| Field | Size | Description | Constraints |
|---|---|---|---|
| Length | 24 bits | Length of the frame payload (excluding the 9-byte header) | Max 16,384 bytes by default; can be increased via SETTINGS |
| Type | 8 bits | Defines the format and semantics of the frame | 10 defined types (0x0-0x9); unknown types must be ignored |
| Flags | 8 bits | Boolean flags specific to the frame type | Undefined flags must be ignored; preserves extensibility |
| R (Reserved) | 1 bit | Reserved for future use | Must be 0; receivers ignore this bit |
| Stream Identifier | 31 bits | Unique identifier for the stream this frame belongs to | 0 for connection-level frames; odd=client-initiated, even=server-initiated |
Why This Structure Matters:
The fixed 9-byte header provides several critical properties:
-
Predictable Parsing — No scanning for delimiters. Read 9 bytes, extract fields with bit operations, read
Lengthpayload bytes. Done. -
Stream Demultiplexing — The Stream Identifier allows frames from different requests/responses to be interleaved. A single TCP connection can carry thousands of concurrent streams.
-
Efficient Buffering — Knowing the exact payload length before reading allows optimal buffer allocation—no over-allocation or reallocation during parsing.
-
Protocol Extensibility — Unknown frame types and flags must be ignored, allowing the protocol to evolve without breaking existing implementations.
-
Connection vs. Stream Frames — Stream ID 0 indicates connection-level frames (SETTINGS, PING, GOAWAY), while non-zero IDs indicate stream-specific frames.
By default, frame payloads are limited to 16,384 bytes (16KB). This limit balances efficiency (larger frames reduce header overhead) against interleaving fairness (smaller frames allow finer-grained multiplexing). The SETTINGS frame allows endpoints to negotiate larger limits, up to 16MB (2^24-1 bytes), though most implementations use conservative values.
Frame Types in Detail
HTTP/2 defines ten frame types, each serving a specific purpose in the protocol. Understanding these types reveals how HTTP semantics map to the binary layer.
Core Frame Types:
| Type | ID | Purpose | Key Details |
|---|---|---|---|
| DATA | 0x0 | Carries request/response body data | Subject to flow control; can be split across multiple frames |
| HEADERS | 0x1 | Carries HTTP headers (request/response) | Compressed with HPACK; may include priority information |
| PRIORITY | 0x2 | Specifies stream priority | Deprecated in favor of HEADERS priority; rarely used now |
| RST_STREAM | 0x3 | Immediately terminates a stream | Contains error code; enables fast error recovery |
| SETTINGS | 0x4 | Configuration parameters for connection | Must be acknowledged; sent in both directions |
| PUSH_PROMISE | 0x5 | Initiates server push | Contains promised request headers; reserves stream ID |
| PING | 0x6 | Connection liveness and RTT measurement | 8-byte opaque data; must be echoed back |
| GOAWAY | 0x7 | Graceful connection shutdown | Contains last processed stream ID and error code |
| WINDOW_UPDATE | 0x8 | Flow control window adjustment | Increments available window for stream or connection |
| CONTINUATION | 0x9 | Continues a HEADERS or PUSH_PROMISE | Used when headers exceed frame size limit |
DATA Frames (0x0):
DATA frames carry the actual content being transferred—the response body for a webpage, the file being downloaded, or the JSON payload of an API response.
+---------------+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
| Data (*) |
+---------------------------------------------------------------+
| Padding (*) |
+---------------------------------------------------------------+
Key characteristics:
- Subject to flow control (both stream-level and connection-level)
- END_STREAM flag indicates this is the final data for the stream
- Optional padding for security (obscures message length)
- Can be split across multiple frames for better multiplexing
HEADERS Frames (0x1):
HEADERS frames carry HTTP headers—both request headers (method, path, authority) and response headers (status code, content-type). Headers are compressed using HPACK.
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) |
+---------------------------------------------------------------+
| Padding (*) |
+---------------------------------------------------------------+
Critical flags:
- END_STREAM: No more data follows (complete request, or response with no body)
- END_HEADERS: This frame completes the header block (no CONTINUATION follows)
- PRIORITY: Stream dependency and weight fields are present
When headers are too large to fit in a single frame (after HPACK compression), CONTINUATION frames carry the remaining header block fragments. Between a HEADERS frame without END_HEADERS and the final CONTINUATION with END_HEADERS, no other frames for that stream may be interleaved—this ensures the header block can be decompressed correctly.
Streams and Stream Identifiers
The Stream Identifier in each frame header enables HTTP/2's most revolutionary feature: multiplexing. A stream represents a single request-response exchange, and multiple streams share a single TCP connection.
Stream Identifier Rules:
| Initiator | Stream IDs | Example |
|---|---|---|
| Client | Odd numbers | 1, 3, 5, 7, ... |
| Server | Even numbers | 2, 4, 6, 8, ... |
| Connection-level | 0 | SETTINGS, PING, GOAWAY |
This simple numbering scheme eliminates coordination—clients and servers can independently create streams without risk of ID collision.
Stream Lifecycle:
Every stream progresses through a well-defined state machine:
+--------+
send PP | | recv PP
,--------+ idle +--------.___
/ | |
v +--------+ v
+----------+ | +----------+
| | | send H / | |
,------+ reserved | | recv H | reserved +-----.
| | (local) | | | (remote) | |
| +----------+ v +----------+ |
| | +--------+ | |
| | recv ES | | send ES | |
| send H | ,-------+ open +-------. | recv H |
| | / | | \ | |
| 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 | |
| send R / `----------->| |<-----------' send R /|
| recv R | closed | recv R |
`----------------------->| |<----------------------'
+--------+
Key states:
- idle: Initial state; stream hasn't been used
- open: Both sides can send frames
- half-closed: One side has finished sending
- closed: Stream is complete; resources can be reclaimed
- •send H / recv H: Sending or receiving HEADERS frame transitions from idle to open
- •send ES / recv ES: END_STREAM flag transitions to half-closed state
- •send R / recv R: RST_STREAM immediately transitions to closed
- •send PP / recv PP: PUSH_PROMISE creates reserved streams for server push
The SETTINGS_MAX_CONCURRENT_STREAMS parameter limits active streams per connection (default: unlimited, recommended: 100). Stream identifiers are never reused—once a stream closes, its ID is permanently retired. This prevents confusion when delayed frames arrive for closed streams. After exhausting 2^31-1 stream IDs, a new connection must be established.
Binary Parsing Advantages
The binary framing layer provides dramatic parsing efficiency improvements over HTTP/1.1's text format. These improvements compound at scale, making HTTP/2 significantly more efficient for high-traffic servers.
Parsing Comparison:
- •Scan bytes for CRLF delimiters
- •Handle variable-length fields
- •Parse ASCII characters to values
- •Manage buffer reallocations
- •Handle line folding edge cases
- •Multiple parsing state machines
- •Character encoding validation
- •Vulnerability to parsing exploits
- •Read fixed 9 bytes
- •Extract fields with bit masks
- •Read exactly 'Length' payload bytes
- •Pre-allocate exact buffer size
- •No edge cases in frame structure
- •Single, simple state machine
- •Binary values—no encoding issues
- •Constrained format limits exploits
Performance Implications:
The binary format enables several optimizations:
-
Zero-Copy Parsing: Frame boundaries are known immediately—no scanning required. Parsers can work directly on the receive buffer without copying data.
-
Parallelizable Processing: Once frames are demultiplexed by stream ID, they can be processed in parallel. HTTP/1.1's sequential nature prevents this.
-
Reduced Syscalls: Knowing exact frame sizes means fewer
read()calls with precise buffer sizes, reducing kernel/user-space transitions. -
Hardware Acceleration: Fixed-length binary formats are amenable to SIMD instructions and hardware offloading—important for high-performance servers.
-
Predictable Memory: Fixed header sizes and known payload lengths allow optimal memory allocation strategies, reducing GC pressure in managed runtimes.
1234567891011121314151617181920212223242526272829
// HTTP/2 frame header parsing - remarkably simpletypedef struct { uint32_t length; // 24 bits uint8_t type; uint8_t flags; uint32_t stream_id; // 31 bits (R bit ignored)} http2_frame_header_t; int parse_frame_header(const uint8_t *buf, http2_frame_header_t *hdr) { // Extract 24-bit length (big-endian) hdr->length = (buf[0] << 16) | (buf[1] << 8) | buf[2]; // Single byte fields hdr->type = buf[3]; hdr->flags = buf[4]; // Extract 31-bit stream ID (ignore R bit) hdr->stream_id = ((buf[5] & 0x7F) << 24) | (buf[6] << 16) | (buf[7] << 8) | buf[8]; // Validate length against settings if (hdr->length > max_frame_size) { return FRAME_SIZE_ERROR; } return SUCCESS;}HTTP/2 uses big-endian (network byte order) for all multi-byte fields. This is consistent with TCP/IP conventions and ensures interoperability across different CPU architectures. Modern compilers provide efficient byte-swapping intrinsics (like __builtin_bswap32) that compile to single CPU instructions on little-endian machines.
Connection Preface
Every HTTP/2 connection begins with a connection preface—a specific sequence that confirms both endpoints support HTTP/2 and establishes initial settings. This handshake ensures protocol compatibility before any frames are exchanged.
Client Connection Preface:
The client sends:
- A 24-byte magic string (ASCII + binary marker):
PRI * HTTP/2.0\r
\r
SM\r
\r
- Immediately followed by a SETTINGS frame
The magic string serves multiple purposes:
- It's deliberately invalid HTTP/1.1, ensuring HTTP/1.1 servers reject it gracefully
- It contains 'HTTP/2.0' for easy protocol identification in traffic analysis
- The 'SM' portion creates binary bytes that can't appear in valid HTTP/1.1
12345678910111213
// Client connection preface (hex representation)0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a// Decoded: "PRI * HTTP/2.0\r\rSM\r\r" // Followed immediately by SETTINGS frame:0x000000 // Length: 0 (empty settings = use defaults)0x04 // Type: SETTINGS (4)0x00 // Flags: none0x00000000 // Stream ID: 0 (connection-level)Server Connection Preface:
The server responds with its own SETTINGS frame. This SETTINGS frame may include:
- SETTINGS_HEADER_TABLE_SIZE (0x1): Maximum size of header compression table
- SETTINGS_ENABLE_PUSH (0x2): Whether server push is allowed
- SETTINGS_MAX_CONCURRENT_STREAMS (0x3): Maximum parallel streams
- SETTINGS_INITIAL_WINDOW_SIZE (0x4): Initial flow control window size
- SETTINGS_MAX_FRAME_SIZE (0x5): Maximum frame payload size
- SETTINGS_MAX_HEADER_LIST_SIZE (0x6): Maximum uncompressed header size
Both sides must acknowledge received SETTINGS with a SETTINGS frame with the ACK flag set. Until acknowledgment, the sender must respect the receiver's previous settings.
| Setting | ID | Default | Range | Purpose |
|---|---|---|---|---|
| HEADER_TABLE_SIZE | 0x1 | 4096 | 0 to 2^32-1 | HPACK dynamic table size |
| ENABLE_PUSH | 0x2 | 1 (enabled) | 0 or 1 | Allow server push |
| MAX_CONCURRENT_STREAMS | 0x3 | unlimited | 0 to 2^31-1 | Maximum active streams |
| INITIAL_WINDOW_SIZE | 0x4 | 65535 | 0 to 2^31-1 | Flow control window |
| MAX_FRAME_SIZE | 0x5 | 16384 | 16384 to 2^24-1 | Maximum frame payload |
| MAX_HEADER_LIST_SIZE | 0x6 | unlimited | 0 to 2^32-1 | Maximum header size |
In practice, HTTP/2 is negotiated during TLS handshake using ALPN (Application-Layer Protocol Negotiation). The client advertises 'h2' in its ClientHello, and the server confirms support. This allows protocol selection before any HTTP data is exchanged, avoiding extra round trips. Clear-text HTTP/2 (h2c) uses HTTP Upgrade instead, but is rarely deployed given security concerns.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.