§4.2
Chapter 04 · Stream Multiplexing and Flow Control for Performance

§4.2QUIC Flow Control Limits and Their Impact on Throughput

RFC 9000

Learning objective. Explain QUIC's two-level flow control, how a receiver advances the limits, and the single relationship that decides whether flow control caps your throughput: window versus bandwidth-delay product.

Two windows: per stream and per connection #

Flow control lets a receiver bound how much unread data a sender can push at it, so a slow or busy application is not overrun. QUIC applies it at two levels simultaneously ([RFC 9000 §4]):

  • Stream level. MAX_STREAM_DATA gives the maximum absolute byte offset the sender may reach on one stream. Each stream has its own limit.
  • Connection level. MAX_DATA caps the sum of bytes sent across all streams. This bounds total buffering regardless of how many streams are open.

A sender MUST NOT exceed either limit ([RFC 9000 §4.1]); it is held to whichever binds first. When it runs into a limit it announces the stall (STREAM_DATA_BLOCKED for a stream limit, DATA_BLOCKED for the connection limit), which is both a diagnostic and a prompt for the receiver to extend credit.

ReceiverSenderstream limit MAX_STREAM_DATA =100 KB · connection limit MAX_DATA= 1 MBreached the streamlimit — cannot sendmore on this streamapplication reads thebuffered data →advance the windowsustained throughput ≈ window / RTT— the window must cover the BDPSTREAM data, offsets 0 → 100 KBSTREAM_DATA_BLOCKED (at100 KB)MAX_STREAM_DATA = 200 KBSTREAM data, offsets 100 → 200KB
Fig. 4.2-1The flow-control cycle. The sender fills its window, signals STREAM_DATA_BLOCKED, and resumes once the receiver — having delivered data to its application — raises the limit with MAX_STREAM_DATA.RFC 9000 §4.1

The receiver drives the loop: as its application reads buffered data, it advances the limit by sending MAX_STREAM_DATA / MAX_DATA frames with a higher value (a smaller value is ignored). Sending these early and often (before the sender actually hits the wall) is what keeps data flowing; the spec explicitly notes that timely limit updates improve throughput ([RFC 9000 §4.2]).

The credit itself is tiny on the wire, a type byte and one or two varints ([RFC 9000 §19.9–§19.11], constants.md §4):

The relationship that governs throughput #

Here is the rule worth memorizing: a stream's sustained throughput is at most its flow-control window divided by the round-trip time. A sender fills the window, then must wait roughly one RTT for the MAX_STREAM_DATA update to arrive before it can send more. If the window is smaller than the bandwidth-delay product (BDP = bandwidth × RTT), the pipe drains during that wait and throughput is capped well below the link's capacity — no matter how much bandwidth is available.

Concretely, a 100 KB window on a 100 ms path caps a stream at 100 KB / 0.1 s = 1 MB/s ≈ 8 Mb/s, even on a gigabit link. To fill a 100 Mb/s × 100 ms path you need a window of at least ~1.25 MB. This is the classic receive-window problem, and it is why serious QUIC stacks auto-tune the window (growing it based on the observed consumption rate and RTT) rather than leaving it at a small default. Long-fat networks are the subject of Chapter 8; the lever there is almost always this window.

Note

The connection-level MAX_DATA window is shared across every stream, which creates a subtle coupling: a stream whose application has stopped reading still occupies its buffered bytes against the connection limit, shrinking the credit available to other streams. So a single stalled reader can throttle unrelated downloads on the same connection: a flow-control cousin of head-of-line blocking, distinct from the transport kind QUIC otherwise removes (§4.4).

In practice

editorial When a transfer plateaus below the available bandwidth, check the flow-control window before the congestion window. A capture that shows frequent STREAM_DATA_BLOCKED / DATA_BLOCKED frames, or a sender that idles for ~1 RTT between bursts, is flow-control-limited: the receiver is not advancing limits fast enough for the BDP. Raise initial_max_data and initial_max_stream_data_* (§7.2) and enable window auto-tuning. Conversely, do not set the initial windows absurdly high on a memory-constrained server: the connection window is a commitment to buffer that much per connection.

Takeaways #

QUIC flow control is two windows — per stream and per connection — that a receiver advances as its application consumes data, with *_BLOCKED frames signalling stalls. The window must cover the bandwidth-delay product or it, not the network, sets the ceiling; and the shared connection window couples streams together. §4.3 turns to the opposite question: when many streams can send, which should go first?