QPACK is the header compression mechanism for HTTP/3, and it’s designed to avoid the Head-of-Line (HOL) blocking that plagued its predecessor, HPACK, in HTTP/2.
Let’s see QPACK in action. Imagine we have a simple HTTP/3 request with a few headers.
:method: GET
:scheme: https
:authority: example.com
:path: /
user-agent: my-awesome-client/1.0
accept: */*
QPACK’s magic happens in two tables: a dynamic table and a literal table. The dynamic table is where frequently used header fields are stored and assigned an index. The literal table is a set of static, pre-defined header fields. When a header is sent, QPACK tries to represent it using an index into one of these tables. If a header isn’t in the dynamic table, it can be added.
The key innovation for avoiding HOL blocking is how QPACK handles dynamic table updates and references. In HPACK, if a header in a request stream was waiting for a dynamic table update from another stream, the entire request stream would stall. QPACK decouples this. It uses two main mechanisms:
-
Insert Count: When QPACK sends an instruction to add a header to the dynamic table, it also sends an
insert count. Thisinsert counttells the receiver how many dynamic table entries have been added since the beginning of the connection. The receiver keeps track of its owninsert count. When the receiver gets a header that references an entry in the dynamic table, it checks if itsinsert countis greater than or equal to theinsert countassociated with that dynamic table entry. If it is, the entry is definitely available. -
Post-Base Indexing: If the
insert countisn’t yet high enough, the receiver knows the entry might not have arrived yet. Instead of blocking, it uses a "post-base index." This means the index is relative to the current number of dynamic table entries the receiver has processed. The receiver can then fetch the header from its local dynamic table, and if the entry isn’t there yet, it will be filled in later when the actual insertion arrives. This "late binding" is what prevents HOL blocking.
Here’s a simplified look at how a compressed header might be represented. Suppose example.com is already in the dynamic table at index 10, and the current insert count is 50.
A header like :authority: example.com could be encoded as a reference to the dynamic table entry. If it’s an absolute index, it would be 0x80 (indicating dynamic table, absolute index) followed by the index 10. However, if the receiver’s insert count is, say, 48, and the header insertion had an insert count of 50, the receiver wouldn’t have that entry yet. QPACK would use post-base indexing. It would encode the index relative to the receiver’s current state. If the receiver has 20 entries, and the target entry is the 10th from the start, it might be encoded as 0x40 (dynamic table, relative index) followed by an offset. The receiver, upon receiving this, would know to look at its 20th entry and add the offset to find the correct header.
The problem QPACK solves is efficient header compression over UDP (which is what HTTP/3 uses). Unlike TCP, UDP doesn’t guarantee in-order delivery of packets. If a packet containing a dynamic table update is lost or delayed, a traditional stream-based compression like HPACK would cause the entire stream to halt, waiting for that update. QPACK’s design, with its explicit insert count and post-base indexing, allows streams to make progress even if some dynamic table updates haven’t been received yet. The necessary information is eventually delivered, and the compressed headers are resolved.
The exact levers you control are mostly at the HTTP/3 stack implementation level. You can influence the size of the dynamic table, which affects how many header fields can be cached. A larger dynamic table can lead to more effective compression but also increases memory usage and the potential for cache churn. The max_table_capacity setting in the HTTP/3 connection settings informs the peer about the maximum dynamic table size you’re willing to use.
What most people don’t realize is that QPACK’s dynamic table can be filled with any header field, not just the ones that appear in the current request. This means if your application frequently sends a specific custom header alongside common ones, QPACK can learn to represent that custom header efficiently over time, leading to significant bandwidth savings.
The next challenge you’ll encounter is understanding how QPACK interacts with stream multiplexing and error handling in HTTP/3.