Learning objective. Understand precisely what a blocked stream is, how
SETTINGS_QPACK_BLOCKED_STREAMS bounds it, the obligation it places on the encoder, and the
concrete strategies — reference only acknowledged entries, duplicate near-eviction entries,
respect flow control — that keep a fast connection from stalling on its own headers.
What "blocked" means, exactly #
A request stream is blocked when the decoder receives a field section whose Required Insert Count exceeds the decoder's current Insert Count: the section names a dynamic entry the decoder has not yet inserted ([RFC 9204 §2.1.2]). The cause is structural: the insert travels on the encoder stream and the reference on a request stream, and QUIC gives no ordering between streams (§4.4), so the reference can arrive first. When it does, the decoder cannot decode the headers yet; it must hold the section until the missing insertions arrive on the encoder stream, at which point the stream unblocks ([RFC 9204 §2.2.1]).
This is the residual head-of-line blocking §4.4 warned about, now named at its source: it is not in the QUIC transport but in QPACK decompression, and it is opt-in: it can only happen when the encoder references dynamic entries that are still in flight.
The budget: SETTINGS_QPACK_BLOCKED_STREAMS #
Blocking is bounded, not eliminated, by a setting the decoder advertises:
SETTINGS_QPACK_BLOCKED_STREAMS (0x07, default 0), the maximum number of streams the
decoder is willing to hold in the blocked state at once (constants.md §11). The obligation
this creates falls entirely on the encoder: it MUST limit the number of streams that could
become blocked to that value at all times ([RFC 9204 §2.1.2]). Note the phrasing: could
become blocked, not are blocked. The encoder must count every stream carrying a reference to
an entry the decoder has not yet acknowledged, because any of them might block under
reordering. If the decoder ever finds more streams blocked than it promised to support, that is
a connection error QPACK_DECOMPRESSION_FAILED (0x0200).
While a stream is blocked, the decoder SHOULD keep the section's bytes counted against that stream's flow-control window rather than releasing them ([RFC 9204 §2.2.1]). Releasing early would let a peer pin unbounded decoder memory with sections it never lets decode: a memory-exhaustion attack. So flow control is the backpressure that makes the blocked-stream budget safe.
Encoder strategies to stay under budget #
Everything the encoder does to avoid blocking follows from one rule: an entry with absolute index below the Known Received Count is safe (§6.2). The strategies differ in how aggressively they reach past that line ([RFC 9204 §2.1.2]):
- Never risk it (block budget 0 or conservative encoder). Reference only acknowledged dynamic entries; for anything newer, fall back to the static table or a literal. Zero blocking, at the cost of larger headers, and larger headers can themselves stall the encoder on congestion or flow-control limits, so "never index" is not free.
- Spend the budget deliberately. When
SETTINGS_QPACK_BLOCKED_STREAMS > 0, reference a just-inserted entry and accept that this stream might block if the insert is reordered. This is worthwhile when the compression win is large and the path is reliable. The encoder simply keeps its count of at-risk streams under the advertised limit. - Duplicate instead of pinning. An entry close to eviction is dangerous to reference: doing so pins it and can prevent new insertions ([RFC 9204 §2.1.1]). The encoder can maintain a draining index (a low-water mark below which it will not emit references) and instead Duplicate the entry to a fresh index and reference that ([RFC 9204 §2.1.1.1], and §6.1).
The decoder-stream constraints #
The decoder side has hard rules of its own. The decoder stream is a critical stream: exactly
one per direction, never closed; a second is H3_STREAM_CREATION_ERROR, closing it is
H3_CLOSED_CRITICAL_STREAM (§6.1). And the encoder must avoid a subtler trap: a
flow-control deadlock. If the encoder stream and a request stream mutually wait on each
other's flow-control credit, neither progresses. The rule is preventive: an encoder SHOULD NOT
write an instruction unless enough stream and connection flow-control credit is available for
the whole instruction ([RFC 9204 §2.1.3]), so a half-written insert can never wedge the
connection.
Worked example: a stream that blocks and recovers #
Figure 6.3-1 is the canonical case. The decoder advertised a blocked-stream budget of 1, so the
encoder is allowed one at-risk stream. It inserts C (abs 0) and immediately references it from a
HEADERS section on stream 0 with RIC = 1, one at-risk stream, within budget. The network
reorders the two: the HEADERS arrive first. RIC 1 > Insert Count 0, so stream 0 blocks;
the decoder parks the section and holds its bytes in the flow-control window. When the encoder
stream's Insert C finally lands, the decoder's Insert Count becomes 1, 1 ≤ 1 clears, the
section decodes, and a Section Acknowledgment goes back. No error, because the encoder stayed
within the one-stream budget the decoder promised to absorb.
Static-table references and literals never risk blocking, because they carry no dynamic
dependency: Required Insert Count 0 always decodes immediately ([RFC 9204 §2.1]). This is why
SETTINGS_QPACK_MAX_TABLE_CAPACITY = 0 (dynamic table off) makes blocking structurally
impossible: with no dynamic entries to reference, every section has RIC = 0.
editorial Treat SETTINGS_QPACK_BLOCKED_STREAMS as a
reliability knob, not a compression knob. On a clean, low-loss path a modest budget (say 8–16)
lets the encoder reference fresh entries and compress well, and the occasional blocked stream
clears in well under a round trip. On a lossy or high-reorder path the same budget means more
streams sitting blocked, each holding decoder memory and stalling that request — so the win
inverts. Size it to the path, and remember the encoder is obligated to honour whatever the
decoder advertises: if you raise the decoder's budget, you are inviting the peer to take more
risk against you.
Takeaways #
A stream blocks when a field section's Required Insert Count outruns the decoder's Insert Count
— a reference arriving ahead of the insert it needs. SETTINGS_QPACK_BLOCKED_STREAMS caps how
many such streams may exist, an obligation the encoder must honour for every potentially
blocking stream, with flow control as the safety backpressure. Encoders stay within budget by
referencing acknowledged entries, spending the budget deliberately, or duplicating near-eviction
entries, and must avoid flow-control deadlocks by never writing an instruction they lack credit
to finish. How to tune these settings when the round trip is long (where every acknowledgment
costs dearly) is §6.4.