HTTP/2’s flow control isn’t a simple "go fast" or "slow down" switch; it’s a granular, byte-by-byte negotiation managed by WINDOW_UPDATE frames.

Let’s watch this in action. Imagine a client requesting a large file from a server.

# On the server, using nghttpd (an HTTP/2 server implementation)
nghttpd -v 8080

# On the client, using nghttp (an HTTP/2 client implementation)
nghttp -v http://localhost:8080/path/to/large_file

As nghttp starts receiving data, you’ll see a flurry of network activity. The server sends DATA frames. But it doesn’t just blast them out. It’s constantly listening for WINDOW_UPDATE frames from the client. The client, in turn, sends WINDOW_UPDATE frames back to the server, indicating how much buffer space it has available for incoming data on a per-stream and per-connection basis.

The core problem WINDOW_UPDATE solves is preventing a fast sender from overwhelming a slow receiver. In HTTP/1.1, this was often handled at the TCP level, but HTTP/2 brings this control into the application layer, allowing for more sophisticated management. This is crucial because a single TCP connection can carry multiple HTTP/2 streams, and each stream needs its own flow control.

Internally, both the client and server maintain two sets of flow control windows:

  1. Connection-level window: This window applies to all streams on the connection. It’s a global limit on how much data can be in transit across the entire connection.
  2. Stream-level window: This window applies to a single stream. It’s a per-stream limit, allowing finer-grained control.

When a client receives a DATA frame, it consumes the data and then sends a WINDOW_UPDATE frame back to the sender (the server, in this case) to replenish the window. The value in the WINDOW_UPDATE frame indicates the number of bytes the receiver is now willing to accept. The sender must track these windows; if a window reaches zero, it must stop sending DATA frames for that stream (or connection) until a WINDOW_UPDATE frame increases the window size again.

Consider this common scenario: a client is processing data from multiple streams simultaneously. It might be fast at reading from stream A but slow at processing stream B. Without per-stream flow control, stream A could consume all available connection bandwidth, starving stream B. With WINDOW_UPDATE frames, the client can tell the server to slow down on stream B while allowing stream A to continue at its own pace, all over the same TCP connection.

The WINDOW_UPDATE frame has two key fields:

  • Type: Must be 0x8 for WINDOW_UPDATE.
  • Stream Identifier: 0 for connection-level flow control, or a non-zero value for a specific stream.
  • Window Size Increment: The number of bytes by which the window is being increased.

A typical interaction might look like this:

  1. Server: Sends DATA frame for stream 5 with payload of 1024 bytes.
  2. Client: Receives the DATA frame, processes 512 bytes, and has 1024 bytes of buffer remaining for stream 5. It sends WINDOW_UPDATE frame with Stream ID 5 and Window Size Increment 1024. The server’s window for stream 5 is now increased by 1024.
  3. Client: Also has 4096 bytes of buffer remaining for the connection. It sends WINDOW_UPDATE frame with Stream ID 0 and Window Size Increment 4096. The server’s connection-level window is now increased by 4096.

The sender (server) is responsible for tracking the current window size for each stream and the connection. When it wants to send N bytes of data, it checks if N is less than or equal to the current available window for that stream and the connection. If it is, it sends the data and decrements both windows by N. If either window is insufficient, it must wait.

The most surprising part is how this mechanism interacts with TCP’s own flow control. HTTP/2’s flow control operates on top of TCP. This means you have two layers of flow control working in tandem. If the HTTP/2 window is large, TCP’s window will likely be the bottleneck. If the HTTP/2 window is very small, it can artificially throttle the TCP connection, even if TCP has plenty of buffer space. Misconfiguration of these can lead to suboptimal performance.

The next challenge you’ll face is understanding how to leverage this granular control for optimal performance, especially when dealing with different types of content and varying network conditions.

Want structured learning?

Take the full Http2 course →