Skip to content

HTTP/2 and HTTP/3

HTTP/2 (RFC 9113) and HTTP/3 (RFC 9114) are the modern versions of the Hypertext Transfer Protocol. HTTP/2 brought binary framing, multiplexing, and header compression to address the limitations of HTTP/1.1. HTTP/3 replaces TCP with QUIC (a UDP-based transport) to eliminate TCP-level head-of-line Blocking and enable features like connection migration.

Both protocols maintain the same HTTP semantics (methods, status codes, headers, URIs) as HTTP/1.1. The change is in how the bytes are formatted on the wire, not in what they mean.

HTTP/1.1 is a text protocol. HTTP/2 is a binary protocol. Every HTTP/2 communication is performed Over a single TCP connection, and all communication is split into smaller messages and frames.

+-----------------------------------------------+
| Frame Header (9 bytes) |
| |
| Length (24): frame payload length |
| Type (8): DATA, HEADERS, SETTINGS, etc. |
| Flags (8): END_STREAM, END_HEADERS, etc. |
| Reserved (1) + Stream Identifier (31) |
+-----------------------------------------------+
| Frame Payload (0 to 2^24-1) |
+-----------------------------------------------+

A stream is a bidirectional flow of frames within a single HTTP/2 connection. Multiple streams are Multiplexed over a single TCP connection. Each stream has a unique 31-bit identifier. Client-initiated streams use odd numbers; server-initiated streams use even numbers.

Client Server
| |
|--- HEADERS [stream 1] ----------------------->| GET /style.css
|--- HEADERS [stream 3] ----------------------->| GET /script.js
|--- HEADERS [stream 5] ----------------------->| GET /image.png
| |
|<-- HEADERS [stream 1] ------------------------| 200 OK
|<-- DATA [stream 1] ---------------------------| CSS content
|<-- HEADERS [stream 3] ------------------------| 200 OK
|<-- DATA [stream 3] ---------------------------| JS content
|<-- HEADERS [stream 5] ------------------------| 200 OK
|<-- DATA [stream 5] ---------------------------| Image data

No ordering requirement exists between streams. The server can send stream 5”s data before stream 1’s, and the client reassembles each stream independently.

HTTP/2 implements flow control at the stream level and the connection level. Each side advertises a Window size (initially 65,535 bytes, configurable via SETTINGS). The sender must not send more data Than the receiver’s window allows. The receiver sends WINDOW_UPDATE frames to increase the window.

This prevents a fast sender from overwhelming a slow receiver. Without flow control, a server Sending a large response could exhaust the client’s receive buffer.

Client Server
| |
|<-- DATA [stream 1, 16KB] --------------------| (client window decreases)
|<-- DATA [stream 1, 16KB] --------------------| (client window decreases)
|--- WINDOW_UPDATE [stream 1, +32KB] ---------->| (client grants more window)
|<-- DATA [stream 1, 32KB] --------------------| (server can send more)

HTTP/1.1 sends headers as plain text, repeated on every request. For a typical page load with 30-100 Requests, headers like User-Agent``Accept``CookieAnd Accept-Encoding are sent dozens of Times, each time consuming hundreds of bytes.

HPACK (RFC 7541) compresses headers using three techniques:

  1. Static table: 61 predefined common header field/value pairs (e.g., :method: GET :path: /``accept-encoding: gzip, deflate)
  2. Dynamic table: Header fields sent in previous messages are cached and referenced by index number
  3. Huffman coding: Literal header values are encoded using Huffman coding, which reduces common ASCII strings by ~20-30%
First request:
:method: GET
:path: /api/users
accept: application/json
user-agent: Mozilla/5.0...
Second request (to same server):
:method: GET <- static table index 2
:path: /api/users <- dynamic table index (from first request)
accept: application/json <- dynamic table index
user-agent: Mozilla/5.0... <- dynamic table index
Second request on wire: ~20 bytes (3 index references) vs ~200 bytes (full headers)

HPACK is designed to be resistant to CRIME/BREACH-style compression oracle attacks. It does not Compress across different origins (the dynamic table is per-origin), and it does not compress cookie Values in a way that leaks information.

HTTP/2 allows the server to proactively send resources to the client before the client requests Them. The server sends a PUSH_PROMISE frame that includes the request headers for the pushed Resource. The client can reject pushed resources with a RST_STREAM frame.

Client Server
| |
|--- HEADERS [stream 1]: GET /index.html ------>|
| |
|<-- HEADERS [stream 1]: 200 OK ----------------|
|<-- DATA [stream 1]: HTML body -----------------|
|<-- PUSH_PROMISE [stream 2]: GET /style.css -->| Server pushes CSS
|<-- PUSH_PROMISE [stream 4]: GET /app.js ---->| Server pushes JS
|<-- HEADERS [stream 2]: 200 OK ----------------|
|<-- DATA [stream 2]: CSS body ------------------|
|<-- HEADERS [stream 4]: 200 OK ----------------|
|<-- DATA [stream 4]: JS body -------------------|

GOAWAY does not immediately terminate the connection. Existing streams can continue to complete. The Sender should continue processing streams with IDs less than or equal to last_stream_id. Only new Streams with IDs greater than last_stream_id are rejected.

Sent by the server to announce that it will push a resource. Contains the request headers for the Pushed resource and the stream ID that will be used for the push.

HTTP/2 has two levels of flow control:

  1. Connection-level: Limits the total amount of buffered data across all streams
  2. Stream-level: Limits the amount of buffered data per stream

A sender must respect both limits. The effective window for a stream is:

effective_window = min(connection_window, stream_window)

The default initial window size is 65,535 bytes (65535 = 2^16 - 1). This can be changed via the SETTINGS frame’s INITIAL_WINDOW_SIZE parameter.