Repository navigation
httpjet v0.0.2
httpjet v0.0.2 — September 2026: correctness fixes, page-cache efficiency, lower per-connection cost
This release carries everything since v0.0.1 (September 17): a review round of correctness and
hardening fixes, page-cache CPU work, progressive streaming for cacheable misses, lower
per-connection memory, and a dependency refresh. New features are opt-in; defaults that changed
are listed under Behavior changes below.
Measured figures come from one production configuration (a CDN-fronted forum plus two
template-driven sites) or from A/B runs against the previous build, and will vary with workload.
Fixes
- Rewrite
[P]targets: the loopback screen now pins the dial. The screen resolved the target
name once, then the proxy dialed by name and resolved again, so a DNS answer that changed between
the two (rebinding) could still reach loopback. The dial now uses the exact address that was
screened. IP-literal targets skip the resolver entirely. - CGI environment.
SSL_CLIENT_*variables can no longer be overridden bySetEnvIf/[E=]; they are denied
like the other server-computed variables.- A user verified by a scoped Basic-auth realm reaches the app as
REMOTE_USER/AUTH_TYPEagain. - The page cache's stale-while-revalidate refresh marker (
HJ_CACHE_REFRESH) reaches the app
again, so an application-level cache can no longer answer a background refresh with its own
aging copy. - v0.0.1's environment deny-list had dropped both of these.
.htaccess.- An unreadable file is still deny-all (fail closed), but the deny now heals as soon as the file
is readable again. It used to persist until the file's mtime changed, andchmoddoesn't
change mtime. - The per-directory walk stops at the first directory that does not exist, so front-controller
URLs no longer fill the cache with negative entries. It used to saturate at 16,384 entries and
stop memoizing new directories.
- An unreadable file is still deny-all (fail closed), but the deny now heals as soon as the file
- HTTP/2.
- WINDOW_UPDATE credit that arrived for an already-finished stream was parked indefinitely, and
on long-lived connections could exhaust the table that holds pre-response credit. - A stream reset by the body deadline now accepts late trailers without a connection-wide GOAWAY.
- Receive-side WINDOW_UPDATEs are batched at half the window instead of sent twice per DATA frame.
- WINDOW_UPDATE credit that arrived for an already-finished stream was parked indefinitely, and
- Stale-if-error during a PHP outage. When the PHP pool stopped answering, a failed request first
rendered a PHP-handledErrorDocumentthrough the same failed pool, and only then served the
retained stale copy. That doubled the wait: in our deployment 120 s, past a typical CDN origin
timeout. A generated 502/503/504 from a PHP dispatch now skips PHP-handled error documents, so the
stale copy (or the built-in error page) arrives after one backend timeout. - Page-cache admission counted twice. A cookieless request probed the cache on the on-core fast
path and again in dispatch, and both recorded an admission sighting. As a result
--page-cache-admit-threshold 2behaved like 1, and miss counts were doubled. One request is now
one sighting.
Page cache: less CPU for the same content
- Store-time dictionary compression defaults to zstd level 12 instead of 15. In our A/B that
was 2.7-2.9x faster for 1-2% larger stored bodies.- New
--page-cache-dict-level vhost=level,...sets a level per vhost. - Encoders are pooled per dictionary.
- In our deployment, dictionary encoding had been the largest share of httpjet's own CPU.
- New
- Fewer encodes.
- A capsule render stored under two keys is encoded once.
- Incompressible content types and tiny bodies are never encoded.
- The skip reasons are exported as
httpjet_pagecache_dict_recompress_skipped_total{reason}.
- Cheaper bookkeeping.
- Entry metadata is compressed at zstd level 6 instead of 19.
- Weak ETags are hashed a word at a time.
- A decoded body is built once per hit.
- Store-time finalize paths no longer pin decoded metadata in RAM.
- Body collectors are presized from Content-Length.
- Streaming zstd compression contexts are reused across responses.
- Miss responses carry the stored entry's weak ETag, so a CDN can revalidate them with
If-None-Match. - New observability: precompressed-variant fill/serve counters per codec, and sampled phase
histograms for store and body collection.
New opt-in features
--page-cache-stream-fill-vhosts <vhosts>: progressive miss streaming.- For templates that print without a Content-Length, a cacheable miss is forwarded to the client
as the backend writes it, then stored once the backend finishes cleanly. It used to be
buffered whole before the first byte went out. - The transport sends the head with the first byte, and the streamed compressor flushes its
first block when the backend stalls. - A backend error, a client disconnect, or a body over the object cap stores nothing.
- Single-flight followers still wake to a stored entry.
- In our deployment it moved a progressive site's miss time-to-first-byte from ~9 ms to ~6 ms.
- For templates that print without a Content-Length, a cacheable miss is forwarded to the client
--page-cache-opt-in-status 303,404,410+--page-cache-303-prefixes <prefixes>. Cache these
statuses only on an explicitX-LiteSpeed-Cache-Control: public, with the TTL capped at 300 s and
no stale windows. A 303 is cached only under a listed path prefix.--trusted-proxy-idle-secs N+--trusted-proxy-idle-max-conns N. A longer idle window for
connections whose peer presented a verified client certificate, such as a CDN's authenticated
origin pulls, which never resume TLS. At 600 s this cut full TLS handshakes per request by ~55%
in our deployment. It falls back to the normal floor above the connection cap.--tls-session-tickets N(default 2, unchanged behavior).0stops issuing TLS 1.3 tickets
and shrinks the session cache to TLS 1.2 session IDs, for deployments whose peers never resume.
It measured ~5% less server CPU per handshake.HTTPJET_LOG_DIR=<dir>redirects every log file an instance opens (unified and per-vhost). It
is meant for test instances that load a production config.
Lower per-connection cost
- Idle TLS connections hold no write buffer. A flushed 64 KiB ciphertext buffer returns to a
per-thread pool (cap 32) and the next write takes a warm one, so a response costs no allocation or
page fault. In a 2,000-connection A/B this used 19-30% less memory per idle TLS connection, with
throughput unchanged within noise. - HTTP/2 per-connection stream maps start empty instead of being pre-sized at accept.
- TLS parameters for a repeat client certificate are memoized instead of re-parsed on every
handshake.
Behavior changes to check when upgrading
--page-cache-admit-thresholdnow means what it says (see the admission fix above). If you
set 2 while it behaved like 1, set 1 to keep the previous store rate.- The connection-close mimalloc collect uses
--mimalloc-trim-threshold-mib(default 768)
instead of a hard 384 MiB cap. A large page cache keeps a healthy process above 384 MiB, which
made every thread force-collect every 30 s. Size the flag above your healthy peak RSS; in our
deployment minor page faults fell from ~6,600/s to ~400/s. - Default store-time dictionary level is 12 (was 15). Bodies stored at 15 still decode.
- HTTP/2 receive-side WINDOW_UPDATE batching changes wire behavior (h2spec 2.6.0: 146/146 over TLS).
Public repository and CI
- CI and the interoperability workflow now install Rust 1.98.1. They had still pinned 1.97.0, below
the workspace floor, so scheduled runs stopped at the toolchain check. - The helper scripts the workflows call are now published: ACME and OCSP acceptance, h2spec,
bounded fuzzing, the OCI smoke test, and two HTTP/3 test clients. fuzz/Cargo.lockis current, and the OCI example builds onrust:1.98-bookworm.- The OCI smoke test works on non-root runners.
Dependencies
- opentelemetry 0.33 (optional
otelfeature), plus cc, hyper-rustls, rand, and unicode-ident. - Built and tested with Rust 1.98.1.