Learning objective. Understand what a HEADERS frame's payload actually contains (the QPACK field-line representations and the field-section prefix) and the two settings that govern the compression-versus-blocking trade-off, without yet diving into the dynamic-table machinery of Chapter 6.
Why not just HPACK? #
HTTP/2 compressed headers with HPACK, whose dynamic table required every header block to be processed strictly in order: each block could depend on table updates in the block before it. Over TCP's single ordered stream that was fine. Over QUIC's independent streams it would be a disaster: a HEADERS block on stream 8 could not be decoded until table updates carried on stream 4 arrived, reintroducing exactly the head-of-line blocking QUIC removed (§4.4). QPACK is HPACK redesigned for out-of-order delivery: it moves dynamic -table updates onto dedicated streams and adds an explicit dependency mechanism so decoding can proceed — or deliberately, boundedly, block — rather than stall unpredictably ([RFC 9204 §2]).
What a HEADERS payload contains #
The payload of a HEADERS frame is a QPACK encoded field section: a short prefix followed by one representation per header field.
Every field resolves against one of two tables ([RFC 9204 §3.1]):
- the static table: 99 predefined entries for common fields (
:method: GET,content-type: application/json, …), always available (constants.md§14); - the dynamic table: a connection-scoped table the encoder fills at runtime with fields this connection actually uses, built via the encoder stream (Chapter 6).
A field is then encoded as one of a few representations, distinguished by their leading
bits (constants.md §14):
| Leading bits | Representation | Meaning |
|---|---|---|
1 |
Indexed | name and value from a table entry — one byte for a common header |
01 |
Literal, name reference | name from a table entry, value spelled out |
001 |
Literal, literal name | both name and value spelled out |
The prefix, a Required Insert Count and a Base, declares which dynamic-table entries the section depends on; a Required Insert Count of 0 means the section uses only the static table and can always be decoded immediately.
The two settings that govern it #
Because the dynamic table is what creates both the compression win and the blocking risk,
two SETTINGS values bound it (constants.md §11):
SETTINGS_QPACK_MAX_TABLE_CAPACITY(0x01, default 0). The dynamic-table size the decoder is willing to keep. Zero disables the dynamic table entirely, forcing static-only encoding: no blocking is possible, at the cost of worse compression.SETTINGS_QPACK_BLOCKED_STREAMS(0x07, default 0). How many request streams the decoder will allow to be blocked waiting for dynamic-table entries at once. Zero means the encoder must never emit a field section that could block.
Together they let a deployment dial the trade-off: raise the capacity and blocked-stream budget for better compression on stable connections, or keep them low (even zero) to guarantee non-blocking decode at the cost of larger headers.
This is the residual head-of-line blocking of §4.4, named precisely: it lives in
QPACK decompression, not in the QUIC transport, and it is opt-in and bounded. An encoder
that references a just-inserted dynamic entry on a stream whose update has not yet arrived
creates a blocked stream; SETTINGS_QPACK_BLOCKED_STREAMS caps how many such streams can
exist, and setting it (or the table capacity) to zero removes the possibility.
editorial Start conservative. A dynamic table capacity of
zero (static-only) is a perfectly reasonable default: HTTP's common headers are already in
the static table, so the compression loss is modest and you get guaranteed non-blocking
decode with almost no encoder complexity. Turn the dynamic table on when you have measured
that header overhead matters (long custom headers repeated across many requests, typically),
and even then keep SETTINGS_QPACK_BLOCKED_STREAMS small. Chapter 6 (§6.4)
returns to tuning these for high-latency paths, where the update round trip changes the math.
Takeaways #
A HEADERS payload is a QPACK field section: a dependency-declaring prefix plus one
representation per field, each an index into the static or dynamic table or a literal. The
dynamic table buys compression at the risk of decompression-time blocking, bounded by
SETTINGS_QPACK_MAX_TABLE_CAPACITY and SETTINGS_QPACK_BLOCKED_STREAMS. With the message's
two payload kinds (body bytes and QPACK headers) understood, §5.3 looks at how
requests and responses actually flow, and interleave, across many concurrent streams.