§6.3
Chapter 06 · QPACK Behavior and Header Compression Optimization

§6.3Blocking Avoidance and Decoder Stream Constraints

RFC 9204

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]).

DecoderNetworkEncoderInsert Count = 0 ·blocked-streambudget = 1reordering —HEADERS arrivesfirstRIC 1 > InsertCount 0 → stream 0BLOCKEDholds section, keepsbytes inflow-controlwindowInsert Count = 1 →RIC 1 ≤ 1 →UNBLOCKencoder stream — Insert C (abs 0)request stream 0 — HEADERS refabs 0, RIC=1request stream 0 — HEADERS(RIC=1)encoder stream — Insert C (abs 0)decoder stream — Section Ack (stream 0)
Fig. 6.3-1Reordering delivers the HEADERS section before the encoder-stream insert it references, so Required Insert Count (1) exceeds the decoder's Insert Count (0) and stream 0 blocks. The decoder holds the section — keeping its bytes in the flow-control window — until the insert arrives, then decodes and acknowledges.RFC 9204 §2.1.2, §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.

Note

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.

In practice

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.