arthrop0d

Brief articles on varied topics.

HTTP/2 Multiplexing, Streams, and Flow Control

networking

HTTP/2 multiplexes many independent request-and-response streams over a single TCP connection. It divides messages into small frames, labels each frame with a stream identifier, and interleaves frames from different streams on the same connection.

The problem HTTP/2 solves

With HTTP/1.1, a client generally had two imperfect choices:

The first creates head-of-line blocking at the HTTP layer: a slow response delays requests behind it. The second costs extra connection setup and resources, and it still does not provide a clean way to coordinate many concurrent requests.

HTTP/1.1 also supports pipelining, where requests are sent before earlier responses arrive, but responses must still arrive in request order. In practice, one delayed response can hold up all the responses behind it.

HTTP/2 keeps one TCP connection but removes that HTTP-level ordering requirement. A response for one request can make progress while another response is delayed.

HTTP/2 multiplexing means “many logical streams, one connection,” not “many TCP connections hidden behind HTTP/2.”

Frames carry the pieces

An HTTP/2 connection is a byte sequence containing frames. A frame is a typed unit with a small header and a payload. Its header includes:

HEADERS frames carry compressed HTTP headers, such as :method, :path, and content-type. DATA frames carry request or response bodies. A large body can be split across many DATA frames, allowing frames belonging to other streams to be placed between them.

For example, the wire order might conceptually look like this:

stream 1: HEADERS  (GET /index.html)
stream 3: HEADERS  (GET /app.js)
stream 1: DATA     (first part of HTML)
stream 3: DATA     (first part of JavaScript)
stream 1: DATA     (rest of HTML)
stream 3: DATA     (rest of JavaScript)

The TCP connection carries these bytes in order, but HTTP/2 interprets each frame independently. The receiver uses the stream identifier to put each frame back into the correct request or response.

A frame belongs to one stream unless its stream identifier is zero. Stream zero is reserved for connection-wide control, such as settings, connection-level flow control, and ping messages. It is not an HTTP request or response stream.

Stream identifiers separate conversations

A stream is one logical exchange of messages: typically one request and its response. Each stream has an integer identifier chosen by the endpoint that creates it. Client-initiated streams use odd numbers; server-initiated streams use even numbers. Stream identifiers increase over the lifetime of a connection and are not reused.

A client might send the request headers for stream 1 and stream 3 before either response is complete. The server can then send response headers and body frames for both streams in whichever order its scheduler chooses. The client matches each response to the original request by stream identifier, rather than by arrival order.

A stream also has a lifecycle: it is opened, may carry frames in both directions, and eventually is closed. END_STREAM flags on suitable frames mark that one side has no more data. A RST_STREAM frame can cancel one stream without closing the entire connection. This is one practical meaning of “independent”: an application can abandon a slow or failed request while keeping other streams alive.

The streams are independent at the HTTP/2 layer, but not at every layer. TCP still delivers bytes in order. If a TCP packet is lost, later bytes cannot be delivered to HTTP/2 until TCP retransmits the missing packet. Consequently, one packet loss can temporarily delay all HTTP/2 streams on that connection. HTTP/2 removes HTTP response-order blocking; it does not remove TCP’s transport-level ordering.

Two levels of flow control

Multiplexing creates a new risk: one large response could consume the receiver’s buffers and prevent other streams from making progress. HTTP/2 addresses this with flow control, a receiver-driven limit on how much body data a sender may transmit before receiving permission to continue.

HTTP/2 has two flow-control windows:

  1. A per-stream window limits DATA frames for one stream.
  2. A connection-level window limits the total DATA frames for all streams on the connection.

A window is a byte count. The sender may send a DATA frame only when both the relevant stream window and the connection window have enough remaining capacity. Sending body bytes reduces both windows by that number of bytes.

Suppose stream 1 has 20 KB available and the connection has 100 KB available. A 12 KB DATA frame on stream 1 reduces the windows to 8 KB for stream 1 and 88 KB for the connection. Another stream might still send data, but stream 1 cannot send more than its remaining stream allowance unless the receiver increases it.

The initial window size is 65,535 bytes in HTTP/2. The peer can change the initial per-stream size with a SETTINGS frame, and each side tracks the windows governing what it is allowed to receive. A receiver grants more capacity with WINDOW_UPDATE, which identifies either a particular stream or stream zero:

The receiver normally sends these updates as its application consumes buffered data. If an application reads a response slowly, its endpoint may delay window updates, causing the sender to pause that stream. If the connection-level window is exhausted, all streams can be forced to pause until the receiver grants more connection-wide capacity.

Flow control applies to DATA payloads, not to every frame. Headers and control frames have their own protocol rules and are not charged against these data windows. This distinction explains errors and stalls involving messages such as “flow-control window exceeded” or a stream that stops receiving body bytes while the TCP connection itself remains established.

In short, frames provide the pieces, stream identifiers keep request-and-response conversations separate, and two flow-control windows prevent multiplexing from turning one connection into an uncontrolled buffer. HTTP/2 can therefore share one connection efficiently while still letting the receiver regulate both each stream and the connection as a whole.

← All articles