§6.1
Chapter 06 · QPACK Behavior and Header Compression Optimization

§6.1QPACK Encoder and Decoder Roles with Dynamic Tables

RFC 9204

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

Decoder (receiverof headers)Encoder (sender ofheaders)three streams, three jobsdynamic table nowhas abs index 0RIC 1 ≤ InsertCount 1 → decodenowentry 0acknowledged →safe to referencefreelyencoder stream (uni, type 0x02) —Insert "x-id: 42"request stream 0 — HEADERS,RIC=1, ref abs index 0decoder stream (uni, type 0x03) —Section Ack (stream 0)
Fig. 6.1-1The encoder inserts on the encoder stream, references the entry from a HEADERS section on a request stream, and learns it was received via a Section Acknowledgment on the decoder stream. The three streams carry table updates, header sections, and feedback respectively.RFC 9204 §4.2, §4.3, §4.4
  • 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 is H3_STREAM_CREATION_ERROR and closing it is H3_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.

Note

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.

In practice

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.