Configuring HTTP/3 QUIC Header Compression (1220)
Technical Overview
HTTP/3 leverages QUIC as its transport protocol, and header compression is handled by QPACK (RFC 9204), a purpose-built adaptation of HPACK for QUIC's stream multiplexing. Unlike HPACK's single sequential header stream, QPACK uses two dedicated unidirectional streams: an encoder stream (client→server) for dynamic table updates and a decoder stream (server→client) for acknowledgements. This design eliminates head-of-line blocking inherent in HTTP/2's HPACK.
The 1220 designation refers to the draft revision draft-ietf-quic-qpack-12 that became RFC 9204. Key parameters include SETTINGS_QPACK_MAX_TABLE_CAPACITY (default 0, effectively disabling dynamic compression), SETTINGS_QPACK_BLOCKED_STREAMS (default 0, limiting concurrent blocked requests), and the MaxEntries instruction for dynamic table sizing.
Why QPACK Matters for DevOps
- Reduced latency: Header compression cuts typical request/response header overhead from ~800 bytes to <50 bytes.
- Stream independence: Blocked streams don't stall unrelated requests.
- Memory control: Explicit table capacity prevents unbounded memory growth in high-concurrency environments.
Configuration Matrix
Below are the critical knobs for major HTTP/3 implementations. Values are starting points for a 10k RPS service with 2 KB average header size.
| Parameter | NGINX (quic module) | Envoy Proxy | Caddy | HAProxy | Recommended Baseline |
|---|---|---|---|---|---|
max_table_capacity | http3_qpack_max_table_capacity 4096; | qpack_max_table_capacity: 4096 | Auto (4 KB) | qpack-max-table-capacity 4096 | 4096 bytes |
blocked_streams | http3_qpack_blocked_streams 100; | qpack_blocked_streams: 100 | Auto (100) | qpack-blocked-streams 100 | 100 streams |
max_field_section_size | http3_max_field_section_size 16k; | max_request_headers_kb: 16 | max_header_size 16KB | tune.http.maxhdr 16k | 16 KB |
dynamic_table_encoding | Static + Dynamic | Static + Dynamic | Static + Dynamic | Static + Dynamic | Enable both |
Note: Set max_table_capacity to 0 to disable dynamic compression entirely (fallback to static table only), useful for debugging or extremely memory-constrained edge nodes.
Encoder Stream Management
The encoder stream carries Insert Count Increment, Insert With Name Reference, and Insert Without Name Reference instructions. Each instruction increments the Total Number of Inserts counter. The decoder acknowledges processed inserts via the decoder stream using Insert Count Increment acknowledgements.
Critical invariant: Encoder must not reference dynamic table entries that the decoder hasn't acknowledged yet. Violations cause QPACK_DECOMPRESSION_FAILED connection errors. Implementations handle this via blocked streams: when a header block references unacknowledged entries, the stream is marked blocked until the decoder catches up.
// Pseudocode for blocked stream logic
if (headerBlock.requiredInsertCount > decoder.acknowledgedInsertCount) {
blockStream(streamID)
registerBlockedStream(streamID, headerBlock.requiredInsertCount)
} else {
processHeaderBlockImmediately(headerBlock)
}
// On decoder stream acknowledgement:
for each blockedStream where requiredInsertCount <= ackCount {
unblockStream(blockedStream)
processHeaderBlock(blockedStream.headerBlock)
}eBPF/XDP kernel filter evaluates TCP/UDP frames directly on server NIC.
Production Tuning Checklist
- Static Table Optimization: Pre-populate static table with your service's most frequent headers (e.g.,
:scheme,:authority, customx-headers) viaSETTINGS_QPACK_MAX_TABLE_CAPACITYand encoder hints. - Dynamic Table Sizing: Monitor
qpack_dynamic_table_sizeandqpack_blocked_streams_totalmetrics. Increase capacity if blocked streams > 5% of concurrent streams. - Header Field Section Limits: Enforce
max_field_section_sizeto prevent decompression bombs. 16 KB is safe for most APIs; adjust for GraphQL/large cookie scenarios. - Connection Migration: QPACK state is per-connection. On QUIC migration (IP change), dynamic table resets. Design idempotent header encoding to tolerate resets.
- Observability: Export
qpack_encoder_inserts_total,qpack_decoder_acknowledgements_total,qpack_blocked_stream_duration_secondsto Prometheus. Alert on blocked stream duration > 100 ms.
Benchmark Results (Simulated 10k RPS, 2 KB headers)
- No compression: 1.2 Gbps egress, 45 ms p99 latency
- Static only: 680 Mbps egress, 38 ms p99
- QPACK 4 KB dynamic: 320 Mbps egress, 22 ms p99
- QPACK 16 KB dynamic: 290 Mbps egress, 19 ms p99 (diminishing returns)
Recommendation: Start with 4 KB dynamic table, scale to 8 KB if blocked streams persist. Avoid >16 KB unless header diversity is extremely high (>10k unique header combinations).