HTTP/2 defines its own flow-control windows on top of TCP. Explain how the per-stream and per-connection windows work, and what symptom appears when the window is left at the default 65,535 bytes on a high-bandwidth, high-latency link.
answer
- credit-based, DATA frames only
- two scopes: stream window + connection (stream 0)
- SETTINGS_INITIAL_WINDOW_SIZE tunes streams, not the connection
- default 65,535 → window/RTT throughput ceiling
- too big = no backpressure, memory blowup
basics
~20 sEach stream and the whole connection have a credit window in bytes; DATA frames consume it and WINDOW_UPDATE frames replenish it. With the 65,535-byte default on a long fat link, a sender stalls waiting for credit each round trip, capping throughput at roughly window divided by RTT regardless of available bandwidth.
solid answer
~60 sHTTP/2 flow control is credit-based and applies only to **DATA** frames. There are two windows: one per stream and one for the connection (stream 0). Sending a DATA frame deducts its payload size from *both*; the receiver replenishes by sending WINDOW_UPDATE with an increment, again at both levels. `SETTINGS_INITIAL_WINDOW_SIZE` sets the initial per-stream window (default 65,535); the connection window starts at 65,535 and can only be raised by WINDOW_UPDATE — a common configuration bug is tuning the stream window and forgetting the connection one. The classic symptom on a long fat network is a hard throughput ceiling: with a 65,535-byte window and 100 ms RTT you get about 5 Mbit/s per stream no matter how fast the link is, because the sender must idle for a round trip waiting for credit. Fix by raising both windows (megabytes for bulk transfer) or letting the stack auto-tune. The reason the mechanism exists at all: a receiver must be able to throttle one slow stream without stalling the whole shared connection, which TCP alone cannot express.
code
http · 9 linesC->S SETTINGS INITIAL_WINDOW_SIZE=65535
S->C DATA stream=1 16384 bytes (stream window 49151, conn 49151)
S->C DATA stream=1 16384 bytes (stream window 32767, conn 32767)
S->C DATA stream=1 16384 bytes (stream window 16383, conn 16383)
S->C DATA stream=1 16383 bytes (stream window 0, conn 0) <-- stalled
... one RTT of idle ...
C->S WINDOW_UPDATE stream=1 increment=65535
C->S WINDOW_UPDATE stream=0 increment=65535
S->C DATA stream=1 16384 bytesgo deeper
Know that HTTP/2 has byte credits per stream, that DATA consumes them, and that WINDOW_UPDATE hands more back.
Distinguish the two scopes, name the 65,535-byte default, and compute the window/RTT throughput ceiling.
Diagnose the sawtooth stall from a frame log, know that SETTINGS does not resize the connection window, and tune both ends plus each proxy hop.
Frame window sizing as a memory-versus-throughput contract per connection, including bufferbloat and the effect on latency-sensitive streams sharing the connection.
## Why HTTP/2 needs its own flow control TCP already has a receive window, but it applies to the whole byte stream. On a multiplexed connection that is too coarse: if an application reads a video stream slowly, TCP backpressure would freeze *every* stream on the connection, including the small JSON call you need immediately. HTTP/2 therefore adds a second, per-stream credit scheme so a receiver can say "stop sending on stream 7" while stream 9 keeps flowing. ## The mechanics Flow control covers **DATA frames only**. Control frames — HEADERS, SETTINGS, PING, RST_STREAM, WINDOW_UPDATE, GOAWAY — are never blocked, which is what keeps a stalled connection controllable. Every sender tracks two windows for every stream it sends on: - the **stream window**, initialized to `SETTINGS_INITIAL_WINDOW_SIZE` (default 65,535 bytes); - the **connection window** for stream 0, initialized to 65,535 bytes. Sending a DATA frame with N payload bytes subtracts N from both. When either window reaches zero the sender must stop sending DATA on the affected scope, even if the other window has room. Windows are signed 31-bit values; a receiver that lowers `SETTINGS_INITIAL_WINDOW_SIZE` mid-connection can push an already-consumed window negative, and the sender must handle that rather than treat it as an error. The receiver replenishes credit with **WINDOW_UPDATE**, which carries an increment (1 .. 2^31-1) and is sent either on a specific stream or on stream 0 for the connection. Typically a stack sends WINDOW_UPDATE once the application has actually consumed buffered bytes — that is what makes the backpressure real rather than a formality. Exceeding a window is a `FLOW_CONTROL_ERROR`. Crucially, `SETTINGS_INITIAL_WINDOW_SIZE` affects only **stream** windows; the connection window is never changed by SETTINGS, only by WINDOW_UPDATE. Many stacks default the connection window to 65,535 too, so a deployment that raises the stream window to 16 MB but never raises the connection window still tops out at 65,535 bytes in flight across *all* streams combined. This is one of the most common HTTP/2 performance bugs in proxies and gRPC deployments. ## The long-fat-network symptom With credit-based flow control the achievable rate per scope is bounded by window / RTT (the same bandwidth-delay product argument as TCP's receive window): - 65,535 bytes over 100 ms RTT ≈ 655 KB/s ≈ 5.2 Mbit/s. - Over a 30 ms intra-region RTT ≈ 17 Mbit/s. So on a 1 Gbit/s transcontinental path, a large download over HTTP/2 crawls at a few megabits while `iperf` on the same path is fast, and the packet capture shows a sawtooth: a burst of DATA, then idle, then a WINDOW_UPDATE, then another burst. People often misdiagnose this as a server or disk problem. The fix is to raise both windows — gRPC and proxies commonly use 1–16 MB, or enable dynamic window auto-tuning (gRPC's BDP estimator, nginx's `http2_body_preread_size` and buffer settings, Go's `http2.Transport` window options) — and to make sure the TCP receive buffer is not the narrower constraint. ## The other direction: too large is also wrong A huge window disables the backpressure that flow control exists to provide. If a receiver grants 64 MB per stream on 100 concurrent streams, a fast sender can force gigabytes of buffering — memory exhaustion, and "bufferbloat" where a high-priority response sits behind megabytes of already-committed low-priority data. Sizing is therefore a real tradeoff: roughly bandwidth × RTT for the paths you serve, multiplied by expected concurrent bulk streams, capped by memory you are willing to commit per connection. ## Diagnosing it Look for a stalled sender with a non-empty application queue. Concretely: capture with Wireshark's HTTP/2 dissector or enable the stack's frame log (`GODEBUG=http2debug=2` for Go, `nghttp -v`, Chrome net-export) and watch for DATA bursts of exactly window size followed by WINDOW_UPDATE arrivals one RTT later. Server metrics that help: time blocked on flow control, current window per stream, and bytes buffered but unwritten. ## Where it does not apply HTTP/3 keeps the same idea but moves it into QUIC, which has per-stream and connection-level flow control plus limits on the number of streams. And on the receiving side of a *proxy*, remember each hop has its own windows: the edge-to-client and proxy-to-origin connections are tuned independently, so a stall can live on either leg.
- You raised SETTINGS_INITIAL_WINDOW_SIZE to 16 MB and throughput barely moved. Why?That setting only changes per-stream windows. The connection-level window still starts at 65,535 bytes and is only enlarged by WINDOW_UPDATE on stream 0, so total in-flight data across all streams is still capped. You must also raise the connection window (most stacks expose it as a separate option), and confirm the TCP receive buffer is not the tighter limit.
- Why not just give every stream an enormous window?Flow control is the receiver's backpressure. With very large windows a fast sender can commit hundreds of megabytes of buffered data per connection, exhausting memory and putting urgent responses behind a long queue of already-sent low-priority bytes. Size the window near bandwidth × RTT for your paths and bound total per-connection buffering.
Each stream has a prepaid data allowance and the connection has a shared family allowance. You can be blocked because your own allowance ran out, or because the family plan did — and topping up only one of them fixes nothing.
saying these in an interview costs you the question
- Claiming TCP's receive window makes HTTP/2 flow control redundant, missing that TCP cannot throttle one stream
- Thinking flow control applies to HEADERS or control frames — it applies only to DATA
- Believing SETTINGS_INITIAL_WINDOW_SIZE also resizes the connection window
- Treating a low-throughput long-haul transfer as a bandwidth problem without checking window/RTT