Networking Fundamentals

Ch.30: HTTP/2 Frames in Depth

By Ayush Arora9 min read

Inspired by: YouTube

Ch.29 introduced HTTP/2 multiplexing and explained how Stream IDs allow multiple requests and responses to overlap on a single TCP connection without head-of-line blocking. While Ch.29 viewed requests and responses as high-level streams, this post zooms into the actual binary unit of exchange on the wire: the HTTP/2 frame.


The frame: HTTP/2's atomic unit of exchange

In HTTP/1.1, a request or response was transmitted as a single, contiguous text block formatted with headers separated by \r\n CRLF (carriage return + line feed) delimiters and an optional body at the end. HTTP/2 fundamentally changes this model by breaking protocol communication down into discrete binary messages called frames.

Every layer of the networking stack operates with its own specific data unit:

Instead of treating an HTTP request as one giant text stream, HTTP/2 breaks the request into specialized frames. The HTTP headers travel in one frame type, while the request or response body payload travels in another frame type.

RFC 9113 specifies several frame types (such as PRIORITY, RST_STREAM, SETTINGS, PING, and GOAWAY), but the two primary frames responsible for carrying application data are:


Frame header format: RFC 9113 section 4.1

According to RFC 9113 section 4.1 ("Frame Format"), every HTTP/2 frame begins with a mandatory 9-byte (72-bit) header, followed by a variable-length payload.

1st Byte (0-7)2nd Byte (8-15)3rd Byte (16-23)4th Byte (24-31)
012345678910111213141516171819202122232425262728293031
Length (24 bits)Type (8 bits)
Flags (8 bits)
RStream Identifier (31 bits)
Frame Payload (length in bytes given by the Length field above)

Click any field to highlight it. Length, Type, and Flags each occupy one byte; the Reserved bit (R) and Stream Identifier share the next 4 bytes; the Frame Payload follows, sized by the Length field.

The fields inside the 9-byte frame header perform specific control functions:

RFC 9113 section 4.1 dictates that frames are transmitted as a continuous sequence of octets on the wire. The diagram representation above arranges the bits into 32-bit words for readability, but on the network wire, the 9-byte header and payload flow sequentially.


How GET and POST requests map to frames

Because HTTP/2 splits messages into frames, requests with a body (like POST) behave differently from requests without a body (like GET).

POST Request (with body payload)

A POST request carries both metadata headers (:method: POST, :path: /api/v1/users, :authority, content-type, content-length) and a payload body (such as JSON data). HTTP/2 splits this request into two distinct frames:

  1. HEADERS Frame: Contains the compressed request headers. The flag END_HEADERS is set to true (1), but END_STREAM is set to false (0) because the request body has yet to arrive.
  2. DATA Frame: Contains the actual payload body bytes. The flag END_STREAM is set to true (1), signaling to the server that no further frames will be sent for this stream ID.

GET Request (without body payload)

A GET request carries metadata headers (:method: GET, :path: /api/v1/orders, :authority) but has no request body payload. HTTP/2 converts this request into a single frame:

  1. HEADERS Frame: Contains the compressed request headers. Both END_HEADERS and END_STREAM are set to true (1). Because there is no body data, setting END_STREAM=1 on the HEADERS frame immediately informs the server that the request is complete upon receiving this single frame.

Response frames

HTTP responses follow the exact same structural pattern. A server returning data sends a HEADERS frame containing status codes (:status: 200) and response headers (END_STREAM=0), followed by one or more DATA frames containing the response body payload (END_STREAM=1 on the final DATA frame).

Note: a stream doesn't always end with a DATA frame. Sometimes the sender only knows a piece of metadata after the body has already gone out, for example a checksum of the data, or whether the operation succeeded, so HTTP/2 allows one more HEADERS frame at the very end to carry that metadata. These are called trailers, and that final HEADERS frame is the one with END_STREAM=1. Regular web traffic almost never needs this, but gRPC (which runs on top of HTTP/2) uses trailers to report the RPC's success or failure status after streaming the response body.

In the sequence diagram above, the client sends a POST request on Stream 1 (split into a HEADERS frame and a DATA frame) and a GET request on Stream 3 (a single HEADERS frame). Because each frame embeds its Stream ID in its 9-byte header, the server processes Stream 3 faster and returns its response frames first. The client uses the Stream ID on each incoming frame to correctly reassemble the responses out of order without confusion.

Frames from different streams can be sent in any order: for example, Stream ID 3's frames can travel before Stream ID 1's frames. What HTTP/2 does not allow is reordering frames within the same stream. A DATA frame for Stream ID 1 can never arrive before the HEADERS frame for Stream ID 1; each stream's own frame sequence stays in order, only the interleaving across streams is free.


Inspecting HTTP/2 frames on the wire: a Wireshark walkthrough

Because HTTP/2 is almost always deployed over TLS (HTTPS), inspecting HTTP/2 frames in Wireshark requires providing TLS master secrets to decrypt the encrypted TCP payload. Once decrypted, Wireshark dissects the packet payload into the HTTP/2 protocol layer and displays the underlying frame fields.

Wireshark trace: POST request and response

When inspecting a POST request on Stream ID 1, Wireshark exposes the binary frame details:

When the server replies to Stream ID 1, Wireshark reveals the corresponding response frames:

Wireshark trace: GET request

Inspecting a GET request on Stream ID 1 (sent over a separate TCP connection to a distinct destination host) shows a different flag configuration:

Because END_STREAM is set to true on the HEADERS frame itself, Wireshark confirms that no DATA frame is transmitted following this HEADERS frame. The server reads END_STREAM=1 and knows immediately that the client has finished sending the request.


Summary