Learning objective. Understand the division of labour between the QPACK encoder and decoder, the dynamic table they share, and the two dedicated unidirectional streams that keep that table synchronized: the machinery §5.2 deferred to this chapter.
Two roles, one shared table #
QPACK compression is asymmetric by design. The encoder — whichever endpoint is sending a header section — owns all the hard decisions: it chooses what to insert into the dynamic table, when to evict, and whether a given field is worth a dynamic reference at all. The decoder is deliberately simple: it applies the instructions it is told and reports back what it has processed. RFC 9204 states the principle directly: QPACK "is designed to place the burden of optional state tracking on the encoder, resulting in relatively simple decoders" ([RFC 9204 §2.1]). Note that both endpoints play both roles at once: each has an encoder for the headers it sends and a decoder for the headers it receives, so a connection carries two independent QPACK instances in opposite directions.
The dynamic table is the shared state. It is a first-in-first-out list of field lines,
initially empty, built up over the connection ([RFC 9204 §3.2]). The encoder adds entries; the
decoder mirrors them. Each entry costs its name length + value length + 32 bytes of
overhead ([RFC 9204 §3.2.1]), and the table's capacity is capped by the decoder's
SETTINGS_QPACK_MAX_TABLE_CAPACITY, which is 0 by default, meaning the dynamic table is
off until the decoder opts in ([RFC 9204 §3.2.3], and §5.2).
Three streams, three jobs #
The reason QPACK can tolerate QUIC's out-of-order delivery is that it splits the work across three streams, each carrying exactly one kind of traffic ([RFC 9204 §4.2]):
- The request/response streams carry the HEADERS frames themselves, the encoded field sections of §5.2. These are ordinary bidirectional HTTP/3 streams.
- The encoder stream is unidirectional, type
0x02, and runs encoder→decoder. It carries an unframed run of encoder instructions: set the table capacity, and insert entries ([RFC 9204 §4.3]). There is at most one per direction; a second isH3_STREAM_CREATION_ERRORand closing it isH3_CLOSED_CRITICAL_STREAM(constants.md§9). - The decoder stream is unidirectional, type
0x03, and runs decoder→encoder. It carries decoder instructions: the feedback that tells the encoder what the decoder has processed ([RFC 9204 §4.4]), which §6.2 covers in full.
Splitting table updates onto their own stream is the crux. In HPACK, table-modifying instructions were interleaved inside the header block, so every block had to be processed in strict order ([RFC 9204 §2.2]). QPACK pulls those instructions onto the encoder stream, and the HEADERS section merely references the resulting entries by index — so a section can be parsed the moment its referenced entries have arrived, regardless of what is happening on other request streams.
Absolute indices and how an entry is inserted #
Entries in the dynamic table have a stable absolute index: the first entry ever inserted is
absolute index 0, and the index increases by one per insertion, fixed for the entry's lifetime
([RFC 9204 §3.2.4]). (The static table is separate and also indexed from 0, with
:authority at index 0, §5.2.) The encoder inserts with one of four encoder-stream
instructions, distinguished by leading bits (constants.md §14):
| Bits | Instruction | What it inserts |
|---|---|---|
001 |
Set Dynamic Table Capacity | (not an entry — sets/changes capacity) |
1 |
Insert with Name Reference | name reused from static/dynamic table, value literal |
01 |
Insert with Literal Name | both name and value spelled out |
000 |
Duplicate | re-inserts an existing entry at a fresh index |
Duplicate looks redundant but earns its place: re-inserting an about-to-be-evicted entry at a new index lets the encoder keep referencing a common field without pinning the old entry and stalling insertions ([RFC 9204 §4.3.4], and §6.3).
Worked example: one insert, one reference, one ack #
Trace the sequence in Figure 6.1-1. The encoder wants to send x-id: 42 and expects to reuse
it, so it (1) sends Insert with Literal Name on the encoder stream, creating absolute index
0 in the dynamic table. It then (2) sends a HEADERS frame on request stream 0 whose field
section references that entry, with a Required Insert Count of 1: "you need at least 1
insertion to decode me" ([RFC 9204 §4.5.1]). If the encoder-stream insert has arrived, the
decoder's Insert Count is already 1, so RIC 1 ≤ 1 and it decodes immediately. Finally (3) the
decoder emits a Section Acknowledgment on the decoder stream, which tells the encoder the
entry is safely received and may now be referenced without any blocking risk ([RFC 9204 §4.4.1]).
That acknowledgment loop (the Known Received Count it advances) is the whole subject of
§6.2.
The two unidirectional QPACK streams are critical streams: like the HTTP/3 control stream, they must exist for the lifetime of the connection and may not be closed. Because they are unidirectional, encoder-stream instructions and decoder-stream feedback flow independently and never block one another. That is exactly what lets table maintenance proceed in parallel with request traffic.
editorial When you open a fresh QUIC connection in a packet
trace, look for the two low-numbered unidirectional streams carrying stream types 0x02 and
0x03 right after the control stream (0x00). If you never see any bytes on the encoder
stream, the peer has chosen static-only encoding (table capacity 0). That is perfectly valid, and the
simplest thing to interoperate against. The moment you do see encoder-stream inserts, you have
signed up for tracking acknowledgments, and the failure modes in §6.5 become live.
Takeaways #
QPACK puts all state-tracking burden on the encoder and keeps the decoder simple. The two share
a FIFO dynamic table addressed by stable absolute indices, synchronized over a dedicated
unidirectional encoder stream (type 0x02, inserts) and reported on a dedicated decoder stream
(type 0x03, feedback), while the HEADERS sections on request streams merely reference entries
by index. That separation is what makes compression survive out-of-order delivery. How the
decoder's feedback advances the encoder's notion of what is safe to reference (the
acknowledgment mechanics) is §6.2.