HTTP/3 does not reuse HPACK; it defines QPACK instead. What property of QUIC made HPACK unusable, and what does QPACK do differently to avoid the problem?
answer
- HPACK needs total order; QUIC has per-stream order only
- encoder stream = insertions, decoder stream = acks
- Required Insert Count + Base
- SETTINGS_QPACK_BLOCKED_STREAMS caps blocked streams
- acked-only references = zero blocking, worse ratio
basics
~20 sHPACK's dynamic table is mutated in strict order, which TCP guarantees but QUIC does not across streams: a header block referencing an entry from a delayed stream would stall everything. QPACK moves table updates onto a dedicated ordered encoder stream and lets the encoder bound how many streams may block.
solid answer
~60 sHPACK assumes one totally ordered byte stream: header block N may reference entries inserted by block N-1, so blocks must be decoded in order. TCP gives that for free. QUIC deliberately does not - each stream is ordered independently, and that is the whole point of HTTP/3. Naively porting HPACK would mean stream 7's headers cannot be decoded until stream 3's arrive, restoring exactly the head-of-line blocking QUIC removes. QPACK (RFC 9204) splits the problem: - **Table insertions move onto a dedicated unidirectional encoder stream**, ordered within itself, plus a decoder stream carrying acknowledgements. - Each header section states a **Required Insert Count** - the table state it depends on. If the decoder has not reached that count, only *that* stream blocks, not the connection. - The encoder chooses the risk. Referencing only entries the decoder has **acknowledged** guarantees zero blocking at some compression cost; referencing fresh entries compresses better but may block. - `SETTINGS_QPACK_BLOCKED_STREAMS` caps how many streams the decoder tolerates being blocked. So QPACK trades a tunable slice of compression ratio for the absence of cross-stream head-of-line blocking.
code
http · 12 linesclient -> server
uni stream (QPACK encoder): INSERT cookie=session=8f3a... # ordered within itself
INSERT user-agent=Mozilla/5.0...
request stream 0: HEADERS RequiredInsertCount=2 Base=2 [idx][idx][:path literal]
request stream 4: HEADERS RequiredInsertCount=2 Base=2 [idx][idx][:path literal]
server -> client
uni stream (QPACK decoder): Insert Count Increment(2)
Section Acknowledgment(stream 0)
# if the encoder-stream packet is lost, ONLY streams with
# RequiredInsertCount > processed-count wait; others decode immediatelygo deeper
Know that HPACK needs strict ordering, QUIC does not provide it across streams, and QPACK is the redesign that fixes this.
Name the encoder and decoder streams, the Required Insert Count, and the fact that only the dependent stream blocks.
Discuss encoder policy - acknowledged-only references versus aggressive referencing - and SETTINGS_QPACK_BLOCKED_STREAMS as the explicit ratio/latency dial.
Frame it as the general problem of shared mutable state over an unordered multiplexed transport, and reason about capacity and blocked-stream settings against connection counts and memory at the edge.
## The assumption HPACK bakes in HPACK compression is *stateful and incremental*. Encoding of header block N depends on every insertion performed while encoding blocks 1 through N-1, and the decoder rebuilds that state by processing blocks in the identical order. If block 5 arrives before block 4, its indices refer to a table the decoder has not built yet, and the result is not a delay but a decode error - HPACK treats desynchronisation as a fatal connection error. Over TCP this costs nothing to guarantee: the transport delivers one byte stream in order, so HTTP/2 header blocks are inherently ordered. ## Why QUIC breaks it QUIC's central improvement is that streams are independent. A lost packet carrying stream 3's data does not hold back stream 7's data; the receiver delivers stream 7 immediately. There is **no total order across streams** - only per-stream order. Drop HPACK into that and you get the worst outcome: to keep the table consistent, the receiver must process header blocks in the order the sender produced them, so a lost packet on one request's headers stalls decoding of every later request's headers. You would have paid QUIC's complexity and re-imported head-of-line blocking at the HTTP layer, in the one place it hurts most - request headers, which gate everything downstream. ## QPACK's structure QPACK keeps HPACK's ideas - a static table (99 entries, indices 0-98, retuned to observed traffic), a size-bounded FIFO dynamic table, Huffman coding with the same code - and changes *where state updates travel*. **Two dedicated unidirectional QUIC streams per direction:** - The **encoder stream** carries table mutations only: insert-with-name-reference, insert-with-literal-name, duplicate, set-capacity. Being a single QUIC stream, it is internally ordered, so the dynamic table is still built deterministically. - The **decoder stream** carries feedback: Section Acknowledgment (this header section was decoded, so the entries it used are known-received), Stream Cancellation, and Insert Count Increment (I have processed this many insertions). **Header sections on request streams reference the table** by a *Required Insert Count* - the number of insertions the decoder must have processed before this section can be decoded - and a *Base*, which makes references relative so a section is not perturbed by insertions made after it was encoded. Post-Base indexing lets a section reference entries inserted as part of encoding it. ## Blocking, made explicit and bounded If a header section arrives with Required Insert Count 12 but the decoder has only processed 9 insertions (encoder-stream packets still in flight or lost), **that single stream** waits. Other streams proceed. The blocking is scoped, not connection-wide. Two controls govern it: - `SETTINGS_QPACK_BLOCKED_STREAMS` - the decoder tells the encoder how many simultaneously blocked streams it will tolerate (commonly zero to a small number, since a blocked stream costs buffered state). The encoder must not exceed it. - **Encoder policy** - the encoder can always avoid blocking by referencing only acknowledged entries, or by emitting literals. This is a pure ratio-versus-latency dial, chosen by the implementation, not by the format. Many deployed encoders are conservative: they insert aggressively but reference only acknowledged entries, so steady-state connections still index almost everything while never blocking. On a fresh connection the first few requests compress slightly worse than HTTP/2 would. ## The trade you should be able to state HPACK maximises ratio and assumes a total order. QPACK accepts a small, tunable ratio loss to preserve stream independence. That is the whole design: the compression context is inherently shared connection state, and the only ways to keep it consistent are to force an order (HPACK) or to make the dependency explicit and bounded (QPACK). ## Operational notes - QPACK settings are a real tuning surface at edge proxies: dynamic table capacity and blocked-stream allowance trade memory and latency against bytes. - A capacity of zero is legal and turns QPACK into static-table-plus-Huffman only - simple, stateless, and a defensible choice for a server with enormous connection counts and short-lived connections. - Symptoms of a bad encoder policy show up as HTTP/3 being no faster than HTTP/2 on lossy links, because header sections wait on the encoder stream.
- How can a QPACK encoder guarantee it never blocks a stream, and what does that cost?By referencing only dynamic table entries the decoder has already acknowledged, or by sending literals instead of references. Then every header section's Required Insert Count is already satisfied on arrival. The cost is ratio: on a new connection, and for any field first seen very recently, the encoder pays literal bytes where an aggressive encoder would have paid one index byte.
- What does setting the QPACK dynamic table capacity to zero do?It disables the dynamic table entirely, leaving the 99-entry static table plus Huffman coding. Compression is meaningfully worse for cookie-heavy traffic, but the encoder becomes effectively stateless, never blocks a stream, and holds no per-connection table memory - a reasonable trade for servers terminating very large numbers of short connections.
HPACK is a shared notebook two people fill in strictly page by page - fine when everything arrives in one queue. QPACK sends the notebook updates down their own dedicated lane and stamps each letter with 'readable once you have page 12', so only that letter waits.
saying these in an interview costs you the question
- Saying QPACK was needed because QUIC is UDP-based or because of encryption differences
- Claiming QUIC has no ordering at all - each individual stream is strictly ordered
- Thinking QPACK eliminates head-of-line blocking entirely rather than scoping and bounding it
- Assuming QPACK is simply HPACK with a bigger static table
- Believing blocked streams are a protocol bug rather than an encoder policy choice