Response HPACK
Bounded response-header reuse against a stock nginx control; revalidated on nginx 1.31.7.
nxTLS uses focused experiments to answer one engineering question at a time against fixed, reviewable source inputs. The goal is not to prove old code right. It is to find out which ideas still survive contact with current nginx, BoringSSL and protocol behavior.
One number per experiment. Summary jumps to the evidence on this page; Full report opens the detailed write-up. These are index numbers, not page-section numbers or production milestones.
Bounded response-header reuse against a stock nginx control; revalidated on nginx 1.31.7.
Dynamic-table response compression, decoder feedback and the limits of current runtime evidence.
Static 4 KiB records, nginx buffering and client-visible delivery trade-offs.
Independent before/after validation of the upstream reload fix, then a 1.31.7 retest.
RFC 9218 scheduling versus actual delivery of critical DATA frames, across 240 trials.
A short TLS record at the urgent DATA boundary, plus four intact controlled selected-prefix retries.
Four-case HPACK/G0A integration: decoded responses and stateful safety controls verified; the combined latency question remains open.
The experiment log separates observed mechanism from product policy. Project-owned spikes start from the pinned baseline; an upstream validation instead fixes an exact before/after source pair. In both cases, the evidence must not become an architectural decision by accident.
Pin exact source revisions. Use the nxTLS baseline for policy spikes and an explicit commit pair for upstream validation.
Patch only the behavior under test. Historical code is evidence, not the implementation base.
Observe protocol bytes or TLS records directly rather than substituting proxy metrics for the thing being tested.
Document what worked, what did not, and which gate remains. A successful spike does not select a production design.
Official nginx 1.31.7 and BoringSSL 594a44291086dcc7cef06cabec4c0b47afd674c0 replaced the previous pins. The canonical build, BoringSSL suites and four focused experiment regressions passed. The earlier reports remain dated evidence, not measurements retroactively reassigned to new source revisions.
220 stock and 220 experimental HTTP/2 responses. Bounded reuse and fallback passed on nginx 1.31.7; 204 B remains 204 B in stock and falls to 107 B after the experimental first block.
280 stock and 280 experimental HTTP/3 responses passed. The separate pinned PR #60 decoder accepts all five disputed RIC fixtures and 172/172 saved field sections; Debian 0.3.23 still fails the known cases.
Stock 16 KiB writes, 4 KiB buffering and static 4096-byte fragments preserved payloads and observed record boundaries. The small timing sample supports direction, not equivalence or tail behavior.
The unpatched nginx 1.31.7 release passed every fresh post-SIGHUP H3 probe and all four established transfers in the isolated IPv4, IPv6 and stacked-reload checks.
docs/UPSTREAM_REFRESH_2026-10-18.md.TinX carried a real response-side dynamic HPACK encoder. Years later, the surviving archive could prove the mechanism existed but could not prove that it caused the remembered rendering difference. nxTLS turned that memory into a current A/B question.
The original nginx 1.31.6 control dynamically decoded request HPACK, and some generated response fields could use incremental indexing, but nginx did not retain and reuse a persistent dynamic response table. The October 18 regression reproduced the same missing response reuse on stock 1.31.7.
The historical TinX encoder did retain connection state, insert and evict entries, and reuse complete name/value matches. That made it a useful clue, not code to port.
The old production configuration, packet traces and controlled before/after browser measurements are gone. The exact 2018 runtime symptom behind the zero-table-size revert is also not preserved.
The modern experiment therefore measures HPACK behavior directly and makes no claim about page rendering, latency or the cause of the remembered difference.
Repeated response fields remain literal in stock nginx even when the peer advertises a nonzero decoder table. An explicit table-size setting only adds the expected table-size update behavior; it does not create encoder reuse.
The isolated response encoder kept connection-level state, reused complete dynamic entries, handled dynamic names and exercised insertion, eviction and peer table-size changes without importing the TinX implementation.
In the stable repeated-response fixture, the patched first block stayed at 204 bytes and the next 19 blocks were 107 bytes. The measurement is compressed HPACK block size, not end-to-end performance.
The hard part is now state ownership and ordering: normal responses, generated fields, table-size changes and trailers must all agree on one connection-wide encoder history before production design can be considered.
The complete report contains the historical/current source comparison, runtime fixtures, implementation spike, table-size cases, churn behavior, validation and limitations.
QPACK can pursue the same product goal — bounded reuse of safe response fields — but it is not HPACK with different opcodes. The encoder stream, decoder feedback, Required Insert Count, blocked-stream budget and reference lifetime are part of the mechanism.
nginx 1.31.6 had dynamic QPACK machinery for request decoding, but no dynamic response encoder. The 1.31.7 stock regression likewise showed no persistent dynamic response reuse; normal responses, 103 and trailers still use its non-reusing response path.
The spike added separate outbound encoder state, a server encoder stream, bounded feedback/reference tracking, eviction protection and literal fallback without changing the request decoder.
With peer capacity 4096 and a nonzero blocked-stream allowance, stock produced 202-byte field sections throughout. The experimental path produced 202 bytes once and 104 bytes for the next 19 responses.
With decoder feedback withheld, the experiment stopped taking dynamic-reference risks, retained bounded state and fell back to literals. Cancellation and Insert Count Increment paths were also exercised.
The original extended 8192 / 4096 RIC-wrap diagnostic failed in Debian pylsqpack 0.3.23 but passed in nghttp3 1.17.0. The October 18 replay confirmed five failing boundary fixtures and 171/172 accepted captured sections in Debian's decoder.
The exact merged pylsqpack PR #60 commit 9e705dfeadeb… accepted all five and all 172 sections on identical saved bytes. PyPI 0.3.24 still lacks the fix. This resolves the specific decoder disagreement, not every possible QPACK interoperability concern.
The fixed-decoder replay does not promote the experimental response encoder to production design. Runtime evidence is still thin for delayed feedback under concurrency, stream backpressure, partial/error paths, loss/reordering and sustained load.
HPACK and QPACK may eventually share semantic policy. Their state machines and wire mechanisms remain separate.
The main report covers the response encoder and runtime suite. The decoder-validation note preserves the old Debian failure and documents the successful isolated upstream PR #60 replay.
TinX historically carried adaptive TLS record sizing. The first nxTLS transport campaign rejected the tested 1024/8192/16384 policy. A focused follow-up then separated nginx output buffering from TLS record sizing itself: could nginx keep its 16 KiB write boundary while BoringSSL emitted the same 4 KiB records?
With nginx left at a 16 KiB buffer, a post-handshake SSL_set_max_send_fragment(..., 4096) kept full successful SSL_write() calls at 16 KiB while BoringSSL emitted four 4 KiB plaintext records.
The stock 4 KiB-buffer case instead issued 4 KiB writes and emitted 4 KiB records. The follow-up therefore isolated write boundary from record boundary.
Across HTTP/1.1, HTTP/2 and clean, WAN and adverse paths, the two 4 KiB-record cases differed by less than 0.1 ms in median first-decrypted, 4 KiB and 8 KiB delivery.
This is an already-available static-file result. Streaming and proxy-producer timing remain explicitly unmeasured.
ssl_buffer_size to 16 KiB and documents 4 KiB as a smaller TTFB-oriented setting. Cloudflare's 2016 dynamic-record patch asked a more aggressive version of the same latency question. nxTLS first retested the adaptive idea, then isolated the mechanism. The current evidence points to static 4 KiB TLS records, not specifically to a 4 KiB nginx buffer. nginx docs ↗ · Cloudflare 2016 ↗Stock nginx with ssl_buffer_size 4k produced full 4 KiB successful writes and full 4 KiB TLS plaintext records.
With nginx's 16 KiB buffer retained, the static BoringSSL fragment limit produced the same record granularity without shrinking nginx's write boundary.
D/F median first-decrypted, 4 KiB and 8 KiB timings differed by less than 0.1 ms across both protocols and all three network paths.
The data support static 4 KiB TLS records for the tested objective. They do not yet choose between stock nginx configuration and an nxTLS-owned BoringSSL fragment policy.
The full report contains both campaigns: BoringSSL record controls, isolated TCP topology, adaptive-policy measurements, the static 4 KiB D/F mechanism proof, kernel revalidation, record/write traces, loss-path caveats and the still-open integration boundary.
A separate question from our HPACK/QPACK policy spikes: can nginx preserve existing QUIC connections and accept new ones during graceful reload? We built the exact upstream commits before and after the merged QUIC BPF fix with the same pinned BoringSSL, then exercised real HTTP/3 across worker-generation transitions.
With four workers, quic_bpf on and a running H3 transfer, the pre-merge upstream revision accepted only 13 of 32 fresh connections after SIGHUP. The existing transfer completed, but 19 new requests failed.
The merged revision accepted 32/32 new IPv4 H3 connections. IPv6 also passed 32/32. Three successive IPv4 reloads passed 96/96 new requests, while all three overlapping slow transfers completed.
Exact first parent of the upstream merge. Four nginx workers, BPF enabled, 19 failed post-reload requests.
Same BoringSSL, Clang, kernel and fixture. Existing long-running transfer also completed successfully.
Isolated ::1 listener, one SIGHUP, valid new generation and completed inflight transfer.
Three successive reloads with overlapping old-generation H3 transfers; all finished with their expected generation.
Read the question, exact upstream SHAs, build provenance, test invariants, IPv4/IPv6 results, stacked worker-generation controls and explicitly excluded scenarios.
A separate fifth experiment asks how quickly nginx 1.31.7's RFC 9218 scheduling reaches the client. The HTTP/2 queue, nginx SSL buffering, BoringSSL records and TCP output are different boundaries. Improving one is not proof that the others changed.
With competing HTTP/2 streams on one TLS/TCP connection, explicit RFC 9218 urgency moved critical delivery forward by a paired median 27.8 ms (simultaneous) and 29.1 ms (late request) versus equal priorities. Aggregate completion was essentially unchanged.
With RFC 9218 already active, 4 KiB records advanced the first complete critical DATA frame by another paired median 3.1 ms or 9.9 ms. The fixture emitted many more TLS records and finished slightly later overall. Without priority, small records even regressed simultaneous delivery.
Two workloads, four factors (A/B/C/D), thirty randomized repeats per cell. Payload integrity, HTTP/2 flow control and record boundaries checked by the host harness.
Paired median improvement for the first complete critical DATA frame in simultaneous / late-request workloads. Direction was consistent in all 30 paired repeats.
Additional paired median improvement with explicit priorities, at the cost of 387 / 192 additional TLS records (simultaneous / late).
Independent instrumented controls confirmed u=5,i=1 to u=0,i=0 for an active stream, while bytes were already pending in SSL and TCP output.
The full report separates client-visible DATA bytes from complete hyper-h2 frames, A→B scheduling from B→D record effects, and primary performance runs from the instrumented PRIORITY_UPDATE control.
Priority-Aware Dynamic TLS Record Sizing began as a question about changing fragment sizes using HTTP/2 urgency. Source tracing pointed instead to a narrower boundary: end one nginx output batch immediately after an eligible small urgency-0 DATA frame, then resume normal bulk output. The candidate is called G0A.
In two trace-enabled observations, G0A emitted 4,381- and 4,241-byte TLS writes. The client decrypted complete critical DATA frames earlier than with B in those samples, while later bulk writes returned to 16 KiB. The extra cost was only two and one TLS records respectively. No H2 frame reordering or BoringSSL fragment-policy change was needed.
Four deliberately blocked selected-prefix writes produced real BoringSSL SSL_ERROR_WANT_WRITE through a test BIO. Retry inputs, frame ordering and response integrity remained correct. The original G0A patch did not change. A native socket prefix block is still unobserved.
Randomized B/H/D/S/SH trials. Stock 4 KiB SSL buffer plus 4 KiB H2 chunks gave the lowest observed median critical-DATA latency, but required many more TLS records than B.
Client-confirmed short TLS records in two instrumented single-run observations; +2 / +1 records over B, not a statistically demonstrated latency win.
Controlled BoringSSL WANT_WRITE in both workloads and two injected transport states; 20/20 responses intact across diagnostic transfers.
All four native B/G transfers completed; neither selected G0A prefix blocked. Fresh-record and native socket retry remain separate gates.
The full report documents the discovered output boundary, short TLS records and Phase 3E's four successful controlled prefix retries, alongside the independent native-socket and fresh-record gaps.
Does shrinking response HEADERS make the fully decoded critical answer arrive sooner when G0A is already selecting an early TLS-output boundary? EXP 07 separates compression, priority-aware output and their interaction with four controlled builds.
The combined PG build preserved HPACK table progress, HTTP/2 frame order, exact bodies and G0A's selected short TLS write across the tested table sizes and receive pause. In a warm reuse example, HEADERS fell from 241 to 144 bytes.
SETTINGS 4096 → 0 → 4096, cancellation and bounded parallel controls produced no reported integrity defect. The server-side graceful GOAWAY fixture did not exercise the intended master/worker drain and remains open.
E7-1; 52 complete responses across the B/P/G/PG variants.
E7-2A; 156 complete responses with correct HEADERS, payload hashes and TLS accounting.
E7-2B; 32 cells, 48 connections, 12 intentional cancellations. G2 remains open.
E7-3 is blocked. The additive or overlapping latency effect has not been established.
A dated research checkpoint describing the four-case experiment, bounded HPACK state/backpressure controls, observed byte-boundary interaction, the separate G0A prefix-retry evidence and the remaining GOAWAY and performance gates.
The current experiment set does not ask for a nostalgia merge. It narrows the next questions enough that future work can stay small, reviewable and explicit.
The current response-reuse direction stays as-is. Further experiment redesign and tuning are deferred until integration work gives a concrete reason to reopen it.
The current response encoder and decoder-validation direction stays intact. Its protocol-specific state machine remains separate from HPACK even where semantic policy may converge.
The historical 1024/8192/16384 adaptive policy remains rejected. Stock 4 KiB settings improved critical-frame arrival in the measured workloads, at a much higher record count. G0A demonstrates a different trade-off: one priority-aware short output boundary, then normal bulk writes. It remains experimental.
HPACK and QPACK may converge on which fields are eligible, sensitive or budgeted. Their protocol mechanisms should remain independently correct.
The experiment log tracks evidence and progress. Architecture, crypto policy, configuration, lineage and the broader roadmap remain on the main nxTLS page.