Architecting HTTP/3 QUIC Header Compression (6260)
Introduction: Why Header Compression Matters at Scale
HTTP/3's migration to QUIC (RFC 9000) brings mandatory encryption and multiplexed streams, but the header compression layer—QPACK (RFC 9204)—is where the real bandwidth savings live. At Votion Cloud we serve 12M+ req/s across bare-metal edges; a 1% header overhead reduction translates to ~40 Gbps global egress savings. This article dissects QPACK's dynamic table management, blocking avoidance, and the kernel-bypass optimizations we deploy on our DPDK/XDP data planes.
QPACK Architecture Refresher
Unlike HPACK's single sequential context, QPACK splits compression into two unidirectional streams:
- Encoder Stream: Inserts dynamic table entries (indexed header fields).
- Decoder Stream: Acknowledges insertions and signals table drops.
This decoupling allows the encoder to proceed without head-of-line blocking—a critical property for QUIC's independent stream model. The dynamic table size is negotiated via SETTINGS_QPACK_MAX_TABLE_CAPACITY (default 0, effectively disabled). We set 16 KiB per connection on metal, balancing memory pressure against compression ratio.
Dynamic Table Eviction & Insertion Strategies
QPACK uses a FIFO eviction policy. However, naive FIFO thrashes on high-cardinality headers (e.g., per-request tokens). We implement a frequency-aware insertion filter in userspace:
- Count header field occurrences per 10k requests via a Count-Min Sketch.
- Only insert fields with estimated frequency > 0.5%.
- On eviction, demote low-frequency entries to a secondary LRU cache before full removal.
This reduced dynamic table churn by 37% and improved compression ratio from 2.1x to 2.8x on our API gateway workload.
Bare-Metal Kernel Bypass for QPACK Streams
On our Xeon Scalable (Ice Lake) nodes we run a custom DPDK + XDP hybrid. QUIC packets are processed in userspace via liburing + io_uring SQPOLL. QPACK encoder/decoder state machines live in the same NUMA-local memory domain as the NIC RX queues.
Key tunables:
net.core.netdev_max_backlog=250000net.ipv4.udp_mem=1024000 2048000 4096000vm.max_map_count=2000000(for massive dynamic table mmap)
We pin QPACK worker threads to isolated cores (cset shield) and use hugepages (1 GiB) for the dynamic table arena to eliminate TLB misses. Measured p99 latency for header encode/decode: 1.8 µs vs 4.7 µs on kernel stack.
eBPF/XDP kernel filter evaluates TCP/UDP frames directly on server NIC.
Blocking Avoidance: Max Blocked Streams & Credit Management
QPACK introduces SETTINGS_QPACK_BLOCKED_STREAMS (default 100). If the decoder cannot process an insertion (e.g., table full), the encoder must mark subsequent header blocks as blocked until acknowledgment. Excessive blocking stalls request streams.
Our adaptive algorithm:
if (blocked_streams > 0.8 * SETTINGS_QPACK_BLOCKED_STREAMS) {
reduce_table_capacity(0.9); // shrink to drain faster
pause_non_critical_insertions();
} else if (blocked_streams == 0 && table_utilization < 0.6) {
increase_table_capacity(1.1);
}This keeps blocked streams near zero while maximizing table usage. In production, we observe <0.01% blocked streams at 500k concurrent connections.
Observability: QPACK Metrics We Export
Every edge node emits Prometheus metrics via a lock-free ring buffer to our telemetry sidecar. Critical series:
qpack_dynamic_table_size_bytes(gauge)qpack_insertions_total{result="success|dropped|blocked"}(counter)qpack_blocked_streams(gauge)qpack_compression_ratio(histogram)qpack_encode_latency_us(histogram)
Dashboards alert on qpack_blocked_streams > 10 for >30s, triggering automatic table capacity re-negotiation via QUIC SETTINGS frames.
Conclusion & Next Steps
QPACK's stream-separated design is a perfect fit for bare-metal, kernel-bypass architectures. By combining frequency-aware insertion, NUMA-local hugepage tables, and adaptive blocking control, we achieve near-optimal header compression with sub-2µs latency. Future work includes hardware offload of Huffman encoding via Intel QAT and integrating QPACK state into our eBPF-based connection migration logic.
Try it yourself: Deploy the CLI builder above to spin up a test edge in FRA, inject synthetic QUIC traffic, and watch the QPACK metrics in real time.