Skip to content

Latest commit

 

History

History
1806 lines (1567 loc) · 91.4 KB

benchmark.md

File metadata and controls

1806 lines (1567 loc) · 91.4 KB

Benchmarks

Reproducible measurements with pinned toolchains, integrity-gated baselines, and a 5-run median with stdev gate. The single-worker Linux plaintext table is the closest single number to a production-shape headline.

Read the worker count. Every row in every table on this page labels its server-side worker count explicitly. Apples-to- apples comparisons (4-worker flare_mc vs 4-worker hyper / axum / actix_web) and per-core comparisons (1-worker flare vs 1-worker nginx vs 1-worker Go) are spelled out side-by-side rather than hidden in section headers; the vs <baseline> ratio is only computed when the worker counts on both sides match.


Workload + harness shape

The headline harness is wrk2 in calibrated-peak mode against a 13-byte plaintext body. Calibrated-peak means a four-phase run: (1) a 5 s settle phase at a low fixed rate so JIT, branch caches, and TCP slow-start are out of the way; (2) a brief overdrive probe (-R 10000000) that establishes a ceiling; (3) a five-step binary search for the highest fixed rate where p99 ≤ a configurable budget (default 50 ms) AND wrk2's achieved rate is at least 90 % of the requested rate — that "achieved ≥ 90 %" rule rejects the case where the load gen has piled up requests at its own queue while the server falls behind (which is what overdrive-only peak-finders silently report as peak); (4) five 30 s measurement rounds at 90 % of the calibrated peak that report the latency distribution.

Calibrated-peak replaces the earlier "two-phase" harness. The two-phase harness measured peak with one wrk2 overdrive run and sustained at 90 % of that number; on a single-worker server the overdrive-vs-sustainable gap was small (~10 %), but on a multi-worker server the overdrive number over-reports the sustainable peak by 30–60 % (the kernel's accept queue absorbs the extra load briefly, then the server falls behind and tail explodes). The calibrated-peak path closes that gap and is what every multi-worker number in this document publishes.

wrk2 (rather than wrk) closes the coordinated-omission hole that makes wrk's default mode silently inflate p99 and hide p99.9 / p99.99 once the server is anywhere near capacity: wrk2 sends at constant throughput so queue time at the gen is counted, which is what production clients actually observe under load.

  1. wrk2 + tail percentiles. Every measurement run captures p50 / p75 / p90 / p99 / p99.9 / p99.99 / p99.999 via wrk2 --latency. The summary headline req/s is peak capacity from the find-peak phase; the latency columns reflect tail behaviour at 90 %-of-peak sustained load. The _install_wrk2.sh step builds a pinned wrk2 commit when the platform has no conda-forge package (linux-64 today). Tail numbers are reproducible across machines because the toolchain is pinned in [feature.bench.dependencies].

  2. Multiple workloads, not one:

    • micro-static (throughput.yaml) — the per-core plaintext parity gate. The headline tables below.
    • mixed-keepalive (mixed_keepalive.yaml
      • wrk_mixed_keepalive.lua) — 80 % keep-alive, 20 % Connection: close. Catches regressions in flare's keep-alive book-keeping and close-after disposition that pure keep-alive loads can't exercise. pixi run --environment bench bench-mixed-keepalive.
    • uploads (uploads.yaml) — POSTs of 4 KB / 64 KB / 1 MB / 16 MB. The 1 MB and 16 MB cases drive the zero-copy reactor adoption.
    • downloads (downloads.yaml) — GETs returning 4 KB / 64 KB / 1 MB / 16 MB streamed bodies. Headline target for the streaming-body reactor adoption: "no per-client allocation proportional to body size." The Go baseline serves matching /4kb / /64kb / /1mb / /16mb routes so the comparison is apples-to-apples.
    • slow-clients (slow_clients.yaml) — 256 connections, each trickling 1 byte / 100 ms. Validates that the read_body_timeout_ms deadline reclaims worker slots from slow-body DoS attempts.
    • churn (churn.yaml) — 10 K open / send / close cycles per second. Stresses accept() throughput, the Pool[ConnHandle] allocator, and the kernel's ephemeral-port + TIME_WAIT bookkeeping.

Server throughput (TFB plaintext)

The workload spec is GET /plaintext returning the 13-byte body Hello, World! with Content-Type: text/plain, HTTP/1.1 keep-alive on, no gzip, no logging. Mojo nightly is pinned per the [dependencies] block in pixi.toml. Workload definitions live in benchmark/configs/throughput.yaml.

The published headline numbers below are taken on the boxes flare's release process targets (Apple M-series for the macOS column, AWS EPYC 7R32 for the Linux column). Per-tag refreshes land in the GitHub release notes for the matching tag; this page tracks the methodology + most recent dev-box smoke.

Single-worker, macOS Apple M-series

All rows are single-worker (flare 1 reactor, Go GOMAXPROCS=1). This is the per-core request-processing comparison; multi-worker numbers live in the next section.

Server Workers Peak req/s p50 p99 vs Go net/http
flare (reactor) 1 157,459 0.39 ms 0.80 ms 1.10x
Go net/http (GOMAXPROCS=1) 1 143,500 0.44 ms 0.86 ms 1.00x

flare is ~1.10x Go's stdlib net/http at the same worker count.

Single-worker, Linux AWS EPYC 7R32

Linux 6.8.0-1027-aws, Mojo nightly, Go 1.24.13, nginx 1.25.3. Different machine — absolute req/s is not comparable across the macOS and Linux tables (different OS, scheduler, CPU); only the intra-platform ratios are.

All rows are single-worker (flare 1 reactor, nginx worker_processes 1, Go GOMAXPROCS=1).

Server Workers Peak req/s p50 p99 vs Go net/http
nginx 1 81,612 0.40 ms 0.79 ms 2.00x
flare (reactor) 1 79,965 0.78 ms 1.53 ms 1.96x
Go net/http (GOMAXPROCS=1) 1 40,739 1.59 ms 3.10 ms 1.00x

flare sits within 2 % of nginx's single-worker throughput and is about 1.96x Go net/http per core. The flare-vs-Go ratio is wider on Linux (1.96x vs 1.10x) because Go's scheduler and netpoll overhead is a larger share of each request on the slower EPYC core than on an Apple M-series P-core. Absolute req/s is lower on EPYC for reasons independent of flare; see the platform footnote.

Tail latencies under sustained load (dev-box smoke)

The wrk2 calibrated-peak harness produces full p50 / p99 / p99.9 / p99.99 columns at 90 %-of-peak sustained load. Numbers below are smoke-quality — taken on the maintainer's AWS Ubuntu 22.04 dev box (6 vCPU, glibc 2.35) at commit 9025444, not the EPYC headline machine — but they prove the harness works and demonstrate the tail-discipline shape flare's release-tag numbers will carry.

Workload Server Workers Peak req/s p50 (ms) p99 (ms) p99.9 (ms) p99.99 (ms) stdev%
throughput flare 1 76,710 1.22 3.06 3.35 3.76 1.27
throughput Go net/http 1 40,896 1.37 3.21 3.72 4.53 1.57
mixed_keepalive flare 1 75,958 1.23 3.09 3.34 3.46 1.27
mixed_keepalive Go net/http 1 41,027 1.38 3.22 3.77 4.57 1.57

Same single-worker discipline as the prior tables. flare holds 1.87x Go's peak req/s; the tail stays disciplined out to p99.99 (3.76 ms vs Go's 4.53 ms on throughput, 3.46 ms vs 4.57 ms on mixed_keepalive). The mixed_keepalive configuration adds 20 % Connection: close to the load — flare's close-after-disposition handling doesn't introduce tail bumps. Both servers are stable under the 3 % stdev gate across the 5-run measurement phase.

No-regression check across the QUIC + H3 + gRPC codec additions

Codec-layer additions land outside the HTTP/1.1 plaintext hot path (the wire that drives every row in the tables above), but the policy is "verify, don't assume". A re-run of the quick harness on the same dev box right after the codec cycle showed the baseline held:

Workload Server Workers Peak req/s p50 (ms) p99 (ms) p99.9 (ms) p99.99 (ms) stdev%
throughput flare 1 76,148 1.24 3.10 3.34 3.52 1.27
throughput Go net/http 1 40,444 1.38 3.22 3.81 4.53 1.57
throughput_mc flare_mc 4 234,179 1.22 2.72 3.07 5.56 0.18

The single-worker throughput row sits at −0.7 % of the 9025444 smoke baseline above (76,148 vs 76,710 req/s) — well within the 1.27 % run σ, p99 and p99.99 within 0.04 ms / −0.24 ms respectively, and the flare-vs-Go ratio holds at 1.88×. The throughput_mc row at 234,179 req/s is at +0.8 % vs the most recent stable dev-box flare_mc measurement (232,406 req/s on commit aea7bdb) and well above every intermediate run across the cycle. Conclusion: the codec additions don't change the steady-state hot-path shape, and the headline tables above remain the correct numbers to publish.

No-regression check after the public-surface refresh

The follow-on pass that tightens the public surface around the codec primitives (the FrameHandler / parse_frame_into trait-driven QUIC dispatcher, the Http3RequestEventHandler / feed_into HTTP/3 reader, the mut out: List[UInt8] encoder buffer-reuse contract across QPACK / H3 / gRPC LPM, and the typed GrpcRequestHeaders / GrpcUnaryReply / never-raises run_unary_call adapter) is again outside the HTTP/1.1 hot path, but a fresh run of the quick harness on the same dev box recorded:

Workload Server Workers Peak req/s p50 (ms) p99 (ms) p99.9 (ms) p99.99 (ms) stdev%
throughput flare 1 71,444 1.18 3.05 3.39 3.66 0.00
throughput Go net/http 1 39,967 1.38 3.25 4.16 5.34 1.57
throughput_mc flare_mc 4 218,939 1.25 2.68 3.04 11.45 0.21

flare single-worker p99 holds at 3.05 ms (vs the prior 3.10 ms check), flare_mc multi-worker p99 holds at 2.68 ms (vs 2.72 ms). p50 is tighter on both shapes. Median req/s on single-worker flare moved from 76,148 to 71,444 (−6.2 %) and on flare_mc from 234,179 to 218,939 (−6.5 %); both deltas sit within the normal cross-day spread the shared dev box produces (the run-σ is 0.00 % / 0.21 %, so the rounds were internally consistent — the drift is the box, not the code). The flare-vs-Go ratio on the single-worker plaintext path holds at 1.79× (71,444 / 39,967), and Go itself moved −1.2 % across the same window. The flare_mc p99.99 median of 11.45 ms is dominated by two warmup-phase rounds with one-off 74.4 / 20.7 ms spikes; rounds 3–5 settled to p99.99 ≤ 4.81 ms, matching the prior check. The encoder buffer-reuse path and the trait-driven dispatchers are HTTP/3 / QUIC / gRPC surfaces — they do not touch the HTTP/1.1 ConnHandle read path that drives these rows, so the held p99 / p50 numbers are the load-bearing signal.

No-regression check after the Mojo v1.0.0b2 migration

Re-running the harness on the same dev box after the mojo ==1.0.0b2 toolchain migration (the reflect[T] alias, non-nullable UnsafePointer, and the b2 mozz dependency) recorded:

Workload Server Workers Peak req/s p50 (ms) p99 (ms) p99.9 (ms) p99.99 (ms) stdev%
throughput flare 1 70,939 1.14 3.09 3.40 3.65 0.00
throughput Go net/http 1 39,219 1.40 3.22 3.80 4.54 1.27
throughput nginx 1 78,320 1.16 3.33 3.93 4.48 0.00
throughput_mc flare_mc 4 221,547 1.25 2.69 3.01 3.24 0.33
throughput_mc flare_mc (static) 4 218,130 1.28 2.70 3.01 3.22 0.33
throughput_mc hyper 4 197,423 1.30 2.83 3.33 13.66 0.21
throughput_mc axum 4 182,368 1.31 2.81 3.26 3.89 0.21
throughput_mc actix-web 4 249,922 1.22 2.77 5.34 9.81 0.39

Single-worker flare holds flat: 71,444 -> 70,939 req/s (-0.7 %), with p99 3.05 -> 3.09 ms and p50 1.18 -> 1.14 ms. The flare-vs-Go ratio is unchanged at 1.81x (70,939 / 39,219), and flare lands at 90.5 % of nginx's single-worker peak.

On the multi-worker shape the absolute numbers moved down across the board this session -- axum -9.4 %, hyper -9.0 %, flare_mc -6.8 %, flare_mc (static) -10.0 % -- while actix-web rose +4.5 %. Three of the four independent implementations dropping ~9 % in lockstep is the shared dev box drifting, not a flare change: the load-bearing signal is flare's standing relative to the Rust libraries, which held or improved. flare_mc moved from 1.10x to 1.12x hyper and from 1.18x to 1.22x axum, and its tail discipline is markedly better under b2 -- flare_mc p99.99 settled at 3.24 ms (run stdev 0.06 ms) against the prior run's 64.86 ms p99.99 with a 351 ms stdev. No b2 regression on either shape.

No-regression check after the v0.9 streaming surface

Re-running the harness on the same dev box after the v0.9 streaming-proxy surface landed (the typed streaming server, reactor-integrated external sources, and the multiplexed framed transport are all additive modules; none touch the plaintext hot path) recorded:

Workload Server Workers Peak req/s p50 (ms) p99 (ms) p99.9 (ms) p99.99 (ms) stdev%
throughput flare 1 76,634 1.13 3.31 3.73 4.07 1.27
throughput Go net/http 1 39,758 1.39 3.23 4.18 6.06 1.27
throughput nginx 1 76,024 1.13 3.24 3.57 3.86 0.00
throughput_mc flare_mc 4 224,920 1.26 2.69 3.00 3.22 0.36
throughput_mc actix-web 4 234,774 1.27 2.71 3.05 3.27 0.21
throughput_mc hyper 4 217,001 1.25 2.85 3.27 3.62 0.28
throughput_mc axum 4 203,306 1.29 2.88 4.30 6.88 0.17

Single-worker flare moved up from 70,939 to 76,634 req/s (+8.0 %), landing at 1.93x Go net/http (76,634 / 39,758) and at parity with nginx (100.8 % of its single-worker peak, up from 90.5 %). On the multi-worker shape flare_mc held its standing relative to the Rust libraries: 1.04x hyper (224,920 / 217,001) and 1.11x axum (224,920 / 203,306), within 4 % of actix-web. No regression from the v0.9 surface on either shape (all v0.9 modules are additive and never touch this plaintext path).

The flare_mc row reports a CPU-contention control run. The default pinned baseline (FLARE_BENCH_PIN=1) on this shared dev box showed bimodal multi-worker tails -- whole 30 s rounds either clean (p99.99 ~3.2 ms) or stalled (p90 in the hundreds of ms) at steady throughput, the signature of pinned workers being preempted by co-tenants and the same-box load generator rather than an algorithmic cost (an algorithmic tail regression elevates every round, not an all-or-nothing subset). Re-running with pinning disabled (FLARE_BENCH_PIN=0) removes the contention and yields clean, consistent tails across all five rounds (p99.99 median 3.22 ms, stdev 6.17 ms), confirming flare's own tail behaviour is intact; throughput is pinning-insensitive (224,920 unpinned vs 232,555 pinned, both ahead of hyper and axum). On a dedicated machine the pinned shape is the faster one; the numbers above use the unpinned control purely to isolate flare from shared-box scheduler noise.

Bimodality gate (harness). benchmark/scripts/_stat.py now marks a run stable only when BOTH the throughput sigma is tight (stdev_pct < 3.0) AND the per-round p99 did not cliff: it computes p99_bimodality_ratio = max(round p99) / min(round p99) and requires it at or below FLARE_BENCH_BIMODAL_MAX (default 10x). This closes the prior gap where a 393 ms-median-p99 round could ship stable: true because req/s held steady -- the standing-queue cliff above is exactly that shape and is now caught. When the ratio trips, the summary carries a note telling the operator to pin server and load generator to disjoint cores (or set FLARE_BENCH_PIN=0 on a shared box) and rerun.

Pinning guidance. On a shared box, either run the unpinned control (FLARE_BENCH_PIN=0) or pin the server workers and the load generator to disjoint core sets so wrk2 never preempts a pinned reactor worker: give the server cores 0..N-1 (its default worker % num_cpus pinning) and launch wrk2 under taskset -c N..M on the remaining cores, steering NIC IRQs away from the server cores. Co-locating the load generator on a server core is the single largest source of the bimodal tail above.

Multi-worker scaling, Linux EPYC

Worker-count discipline: the tables below show two things, kept in the same matrix so the reader can see the per-core baseline (1-worker flare, 1-worker nginx, 1-worker Go) next to the matched multicore comparison (4-worker flare_mc vs 4-worker hyper, axum, actix_web). Every row's worker count is in the column header; we do not compute a vs <X> ratio between rows whose worker counts differ. The Go baseline (benchmark/baselines/go_nethttp/main.go) is hard-coded to runtime.GOMAXPROCS(1) and the nginx config to worker_processes 1 so those rows land in the per-core column; the Rust baselines all run on a 4-worker tokio runtime / 4-worker actix system, matching flare_mc.

HttpServer.serve(handler, num_workers=N) with N >= 2 binds per-worker SO_REUSEPORT listeners by default (each worker owns its own listener fd; the kernel hashes new 4-tuples to one of N listeners, matching actix_web's listener strategy and giving the highest steady-state throughput on dev-box workloads). Set FLARE_REUSEPORT_WORKERS=0 to opt into the alternative: a single shared listener registered in every worker's reactor with EPOLLEXCLUSIVE (Linux >= 4.5). Under the shared-listener mode the kernel wakes exactly one waiter per accept event in FIFO order across workers blocked in epoll_wait, so a worker actively running a handler is not woken -- fair-by-construction across idle workers. That mode trades 7-22% req/s (handler vs static fast path) for a uniformly tighter p99.99 σ under sustained load; useful when the workload has bursty arrival that can stack on one reuseport listener. See the Listener-mode A/B section for the head-to-head numbers in both modes.

The load generator is throughput_mc.yaml (wrk2 -t8 -c256 -d30s --latency) with the calibrated sustainable-peak harness from bench_vs_baseline.sh. The single-threaded throughput.yaml pins wrk to one thread and 64 connections, which cannot drive enough concurrent load to show worker scaling no matter what the server does.

flare own worker scaling, EPYC 7R32, throughput_mc

Server Workers Req/s (median) stdev% p50 p99 vs flare 1w
flare (single-threaded) 1 56,086 0.32 1.21 ms 2.70 ms 1.00x
flare_mc (per-worker SO_REUSEPORT, default) 4 170,305 0.17 1.13 ms 2.38 ms 3.04x

flare_mc scales to 3.04x of flare 1w on 4 workers — slightly super-linear once the single-worker reactor's serialization / ack-batching overhead is amortised across cores.

Cross-framework comparison, EPYC 7R32, throughput_mc (8 wrk2 threads, 256 conns, --latency)

Every row labels its server-side worker count explicitly. The single-worker rows (flare, nginx, Go) and the four-worker rows (flare_mc, hyper, axum, actix_web) are kept in the same table so the reader can see the per-core baseline next to the matched multicore comparison. Median req/s is the wrk2 calibrated sustainable-peak (p99 ≤ 50 ms gate, then 90 % of peak across 5x 30 s runs).

Target Workers Req/s (median) stdev% p50 (ms) p99 (ms) p99.9 (ms) p99.99 (ms) stable
actix_web (tokio, workers=4) 4 264,691 0.17 1.19 2.80 11.44 21.61 true
hyper (tokio multi-thread) 4 221,349 0.17 1.24 2.82 3.28 3.67 true
axum (tokio multi-thread) 4 201,042 0.21 1.29 2.82 3.27 3.65 true
flare_mc (per-worker SO_REUSEPORT, default) 4 170,305 0.17 1.13 2.38 2.73 3.11 true
nginx (worker_processes 1) 1 63,764 0.39 1.06 2.29 2.70 3.03 true
flare (reactor) 1 56,086 0.32 1.21 2.70 3.16 3.54 true
Go net/http (GOMAXPROCS=1) 1 35,940 0.21 1.12 2.92 4.29 5.47 true

Worker discipline:

  • flare_mc, hyper, axum, actix_web all run 4 OS worker threads. flare_mc and actix_web both default to per-worker SO_REUSEPORT listeners; hyper / axum share a listener via tokio's multi-thread runtime. flare_mc can be flipped to the shared-listener / EPOLLEXCLUSIVE shape via FLARE_REUSEPORT_WORKERS=0 -- see the Listener-mode A/B section.
  • flare, nginx, go_nethttp all run 1 OS worker thread. nginx via worker_processes 1, Go via runtime.GOMAXPROCS(1), flare via the default single-reactor serve(handler) path.
  • All targets are stress-tested by the same wrk2 -t8 -c256 -d30s --latency client against 127.0.0.1:8080/plaintext over HTTP/1.1 keep-alive on the same EPYC 7R32 box.

Reading order:

  1. flare_mc tail latency is the best of the four 4-worker frameworks. p99 = 2.38 ms vs hyper 2.82 / axum 2.82 / actix_web 2.80; p99.99 = 3.11 ms vs hyper 3.67 / axum 3.65 / actix_web 21.61. This flare_mc row was measured in the shared-listener / EPOLLEXCLUSIVE mode (FLARE_REUSEPORT_WORKERS=0), which trades 7-22 % req/s for the tightest p99.99 σ -- see the Listener-mode A/B section for both shapes against the same workload. actix_web's p99.99 spike is the per-worker SO_REUSEPORT listener- distribution variance wrk2's coordinated-omission correction picks up.
  2. Throughput: flare_mc lands at 64 % of the throughput leader (170 K vs actix_web 265 K), 77 % of hyper 221 K, 85 % of axum 201 K. The remaining gap is per-request handler / serializer constant overhead — Mojo nightly's allocator and Mojo's String ref-count discipline are still measurably heavier than Rust's Bytes::from_static + &'static [u8] path. The 0.17 % stdev shows the gap is a steady cost, not tail variance.
  3. flare 1w vs nginx 1w: 88 % parity. 56 K vs 64 K req/s on matched single-worker setups. nginx wins absolute throughput (it's a static-text fast path); flare's tail at p99 (2.70 ms vs 2.29 ms) and p99.99 (3.54 ms vs 3.03 ms) is in the same shape. flare runs a real Mojo handler (req.url == "/plaintext" branch + ok("Hello, World!") + Response serialization), not a hard-coded return 200 "Hello, World!".
  4. flare 1w vs Go 1w: 1.56x throughput, comparable tail. The Go scheduler / netpoll overhead is a meaningful fraction of each request on EPYC.
  5. stdev across 5 measurement runs is at or below 0.39 % for every published row — well under the harness's 5 % stability gate. Numbers are coordinated-omission corrected.

On macOS loopback flare_mc saturates at ~140 K req/s regardless of worker count because wrk and the server compete for the same single-client CPU. That ceiling is the testbed, not flare. The 4-worker flare_mc row is within noise of the 1-worker flare row on macOS — exactly why the Linux table above is the headline.

Why actix_web's p99.99 spikes (and flare_mc's doesn't)

actix_web ships with per-worker SO_REUSEPORT listeners by default — every worker binds its own listener at the same port, and the kernel hashes each new 4-tuple to one of N listeners. Under bursty arrival (a wrk2 ramp opening 256 connections in the first measurement window), the hash can cluster 80+ connections onto one worker and 30 onto another. The overloaded worker head-of-line-blocks its share of the connections while the other three sit idle; wrk2's coordinated-omission correction surfaces that as p99.99 amplification.

hyper and axum share a single listener across the four workers via tokio's multi-thread runtime; the flare_mc row in the table above was measured in flare's shared-listener shape:

  • flare_mc (FLARE_REUSEPORT_WORKERS=0) uses EPOLLEXCLUSIVE (Linux ≥ 4.5) so the kernel wakes exactly one worker per accept event — whichever worker is currently parked in epoll_wait. Idle workers absorb spikes; busy workers aren't burdened with extra accepts.
  • hyper + axum use tokio's multi-thread runtime which shares one accept future across worker tasks.

The shared-listener shape is what gives flare_mc / hyper / axum their tight (3.11 / 3.67 / 3.65 ms) p99.99. actix_web's 21.61 ms is the reuseport-distribution cost; switching actix_web to a shared listener (it's a config knob) would close the gap. flare's own default (per-worker SO_REUSEPORT) sits between the two shapes -- see the next section for the head-to-head numbers.

Same-host head-to-head (dev-box, current code)

The cross-framework table above is the historical CPU-pinned reference run (raw run data not tracked in the repo; reproduce locally via the harness below). The numbers in the README headline come from a fresh same-host head-to-head on the dev-box (also EPYC 7R32, separate AWS instance, no CPU pinning) against the current code. Each row is measured at its peak-sustainable rate -- the harness's calibrated- peak finder picks the highest R= that holds p99 ≤ 50 ms, runs five 30s rounds at 90% of that, reports the median. The flare baselines build via mojo build -D ASSERT=none (see Production / bench build flags); the Rust baselines build via cargo build --release --locked.

4-worker frameworks (each row uses each framework's default-recommended listener strategy: actix_web ships per-worker SO_REUSEPORT; hyper and axum use tokio's multi-thread runtime sharing one accept future; flare also defaults to per-worker SO_REUSEPORT for num_workers >= 2):

Each latency cell is reported as median ± σ over the 5 measurement runs (σ = sample stdev across runs, ms; Bessel-corrected). The σ% column is the relative stdev of req/s across the same 5 runs (the harness's stability gate fires at 3-5 % depending on config).

The σ on the tail percentiles is the honesty meter for these numbers. A small σ (sub-ms on p99.9 / p99.99) means all 5 runs landed inside the steady-state working envelope. A large σ (tens or hundreds of ms) means at least one of the 5 runs brushed against the saturation cliff and the headline rate is sitting at the limit, not comfortably inside it. The harness's calibration pass (probe duration 20 s, cliff-fanout gate on p99.9 / p99.99, transient-blip retry, absolute-p99 growth gate, post-search 0.92× validation back-off) catches the cliff at calibration time; what remains in the σ column across these 4-worker rows is whatever residual variance each framework has at its calibrated rate.

Server Workers Req/s σ% p50 (ms) p99 (ms) p99.9 (ms) p99.99 (ms)
flare_mc_static (REUSEPORT) 4 242,384 0.21 1.18 ± 0.02 2.67 ± 0.02 3.03 ± 0.02 3.34 ± 0.16
actix_web (tokio) 4 239,108 0.33 1.26 ± 0.00 2.73 ± 0.04 3.25 ± 11.28 5.21 ± 12.05
flare_mc handler (REUSEPORT) 4 237,761 0.34 1.20 ± 0.02 2.74 ± 320.65 5.99 ± 359.05 64.86 ± 350.97
hyper (tokio multi-thread) 4 217,036 0.21 1.24 ± 0.01 2.83 ± 0.02 3.28 ± 0.08 3.66 ± 2.72
axum (tokio multi-thread) 4 201,216 0.35 1.29 ± 0.00 2.82 ± 0.03 3.25 ± 2.50 3.64 ± 29.52

Source data: benchmark/results/2026-06-18T0322-ehsan-dev-6439147/ (all 4-worker rows, single multi-worker run with the calibration harness, hot-path UTF-8 validation bypass included; see Hot-path measurement notes below).

What jumps out:

  • flare_mc (handler path) posts the best median p99 of the 4-worker pack at 2.63 ms, edging hyper (2.83 ms) and matching axum (2.80 ms). The σ on flare_mc's tail (17–50 ms across the 5×30 s runs at p99 / p99.9 / p99.99) is larger than axum's flat σ but smaller than hyper's at p99.99 — one of the five measurement runs brushed the working-envelope edge while the other four landed clean. The headline tightened by +1.1 % over the prior baseline (212,246 → 214,567 req/s) after eliminating a redundant UTF-8 validation pass on the H1 parser's ASCII artifacts (Method / Path / Version / header names + values are already RFC 7230-validated by the byte-level parser before string materialisation). This is the row we hand operators when steady-state tail predictability matters more than headline throughput.
  • flare_mc_static still leads on req/s of the multi-worker fixed-response fast paths (247k req/s, ~13 % under actix_web's new headline). Its p99 median is 2.68 ms — tight when the harness lands inside the working envelope. The ~660 ms σ at every tail percentile is the honesty meter firing: at this rate the fixed-response path occasionally tips off the saturation cliff, and the σ tells you that's where the next 10 % of throughput goes. Use this row when the headline matters and the workload tolerates occasional tail expansion; use flare_mc when you want a uniformly tight tail under sustained load.
  • actix_web posts the highest headline (253k req/s) but its p99 median is 10.04 ms and p99.99 is 37.41 ms — the same cliff dynamic flare_mc_static shows, just at a higher rate. The σ on actix's p99 / p99.9 (4.55 ms / 4.31 ms) is tight enough that this isn't measurement noise; it's a steady-state shape at that rate. The harness's calibration gate accepted the rate (the 20 s probe landed below the absolute-p99 limit the gate enforces) but the 5×30 s measurement rounds caught the steady-state shape underneath.
  • axum is the steadiest of the pack: 195k req/s with σ ≤ 0.02 ms at every tail percentile — flat at the cost of being the lowest headline of the four. Use it as the reference for what an in-envelope p99 distribution looks like at this load.
  • hyper is the reference baseline — its current numbers (216k req/s, 2.83 ms p99 median) move within ±0.5 % of the prior measurement, so the same Rust binary under the same Linux kernel returns the same throughput run-over-run.
  • The harness's σ% column shows req/s itself is rock-steady at the 90 %-of-peak sustain rate (0.17-0.69 % across the 5 runs for every framework), so the tail numbers are measuring real latency variance, not load-gen drift.

Hot-path measurement notes

The +1.1 % flare_mc tightening between the prior baseline and the HEAD numbers above comes from a targeted hot-path optimisation surfaced by the repeatable allocation + CPU-profile harness shipped under pixi run -e dev perf-server-alloc (Linux only; benchmark/scripts/perf_profile_server.sh).

The harness composes three measurements on the same bench_server build (always built with -D ASSERT=none):

  1. heaptrack — LD_PRELOAD malloc tracer. The healthy posture is every top entry coming from Mojo runtime startup (M::MLRT::getOrCreateRuntime) and the per-request hot path adding zero new allocation sites.
  2. strace -c on brk / mmap / munmap / mremap — syscall-level allocator counts. The healthy posture is total_syscalls / total_requests << 1.0 — measured posture is 171 syscalls / 600K requests = 0.00029 syscalls/req, all during startup, none on the per-request hot path.
  3. perf record -F 999 --call-graph dwarf — sampling CPU profile of the live server. The healthy posture has the top user-space symbol as the parser body (_parse_http_request_bytes) at ~2-3 % of CPU. The prior profile flagged _is_valid_utf8_runtime at ~5 % — a Mojo-stdlib UTF-8 validator called by the String(unsafe_from_utf8=Span) constructor despite its name. Replacing the seven hot-path call sites with a non-validating helper that uses String(unsafe_uninit_length=N) + memcpy over pre-validated ASCII bytes recovers that 5 %, and the +1.1 % throughput delta is what's left after the reactor / syscall layer absorbs most of the win.

Both valgrind (callgrind / massif / dhat) and heaptrack are pixi-managed via conda-forge on Linux-64 through [feature.dev.target.linux-64.dependencies], so the profile reproduces from a clean clone with pixi install -e dev.

Single-worker (per-core request-processing cost, same harness as the 4-worker rows above):

Server Workers Req/s σ% p50 (ms) p99 (ms) p99.9 (ms) p99.99 (ms)
flare (reactor) 1 79,028 1.57 1.13 ± 0.03 3.23 ± 0.12 3.84 ± 0.37 4.30 ± 0.51
nginx (worker_processes 1) 1 76,883 1.27 1.12 ± 0.01 3.23 ± 0.09 3.62 ± 0.15 4.05 ± 0.48
Go net/http (GOMAXPROCS=1) 1 40,343 0.00 1.35 ± 0.01 3.21 ± 0.01 3.60 ± 0.04 4.40 ± 0.17

flare 1w is on par with nginx 1w at this measurement point: both at ~77-79k req/s, identical median p99 of 3.23 ms, and identical median p50 of 1.12-1.13 ms. The 2,145 req/s gap between them (79,028 - 76,883) sits inside the combined 1σ envelope of the two runs (√(1241² + 976²) ≈ 1,579 req/s, so the gap is ~1.36σ -- inside 2σ, outside 1σ). nginx 1w itself drifted by 3,356 req/s between the prior and current measurement runs of the same binary (80,239 → 76,883, a -4.2 % move that's 2.66σ apart from itself), which is larger than the current flare-vs-nginx gap in either direction. Statistically indistinguishable at single-core plaintext load.

What did move decisively is the flare-side measurement: 71,619 → 79,028 req/s is a +10.3 % headline jump on the same hardware and same harness. The multi-worker shape only saw +1.1 % from the same H1 parser tightening because that workload is split four ways and the parser is a smaller fraction of per-CPU time; single-worker runs every request on one core where the ~5 % CPU reclaim from the UTF-8 validation bypass translates directly into throughput. The p99.9 / p99.99 medians moved from 3.30 / 3.43 ms to 3.84 / 4.30 ms, but the σ at p99.99 tightened from 5.67 ms to 0.51 ms -- the previous run had one of 5 measurement rounds brush the cliff (matching what the prior 5.67 ms σ already flagged); this run's σ says all 5 rounds landed clean inside the working envelope. vs Go net/http at the same worker count: 1.96× the throughput (was 1.78× in the prior measurement) with comparable tail medians (Go's 3.21 / 3.60 / 4.40 ms at p99 / p99.9 / p99.99 vs flare's 3.23 / 3.84 / 4.30 ms).

Source data: benchmark/results/2026-05-27T2256-ehsan-dev-03e55f2/. Prior baseline (earlier numbers carried through until this HEAD): benchmark/results/2026-05-11T1821-ehsan-dev-944de73/.

Feature-pass no-regression check (HTTP/3 + QUIC additive surfaces)

The HTTP/3 + QUIC feature pass (QUIC reactor + H3 server + rustls QUIC binding + CC trait + crypto key schedule + ALPN dispatcher + WS auto-dispatcher + http<->http2 cycle break) is additive to the request-side hot path: every new module either runs only when the caller opts into it (ALPN dispatcher, WS auto-dispatcher) or sits on the QUIC sub-tree that the throughput harness doesn't exercise (QUIC reactor, H3 server, rustls binding, AEAD). The StreamSlab[Stream] swap (HTTP/2 stream map) is on the h2 hot path but the throughput harness uses h1 only.

The full bench-vs-baseline gate must run on the EPYC dev-box before the next h1 floors are quoted; the in-session gates that did run all hold:

  • pixi run format-check -- 423 files clean.
  • pixi run check-sans-io -- 28 files clean.
  • pixi run check-no-http-http2-cycle -- clean.
  • New tests this cycle (9 files / 88 cases): all pass.
  • pixi run bench-h3 flare -- prints the "infra ready, wiring pending" banner + exits 0 as the gate now expects.

The full pixi run tests aggregate + fuzz-all + the sanitizer trio + bench-vs-baseline cycle (each multi-hour) runs on the dev-box at the cycle handoff; the new test files are already wired into the tests aggregate so they run alongside the existing 600+ cases.

Wire-paths no-regression check

The wire-paths cycle wired every QUIC + H3 surface that the feature pass had stubbed: OpenSslQuicCrypto replaces StubQuicCrypto, rustls_wrapper links the QUIC TLS handshake through a real C ABI crate, QuicListener.run drives the UDP reactor (non-blocking recv_from drain + ConnectionIdTable + AEAD + frame parse + Connection.handle_frame + PTO/idle/ack-delay timers on the shared TimerWheel + CC + pacing budget gating the egress path), Http3Connection.feed_stream_chunk drives Http3RequestReader -> Handler -> response writer, the unified HttpServer.bind_with_http3 routes ALPN-negotiated h3 alongside h1/h2c/h2, and WsAutoClient.connect drives TLS handshake + ALPN inspection -> WsClient or WsOverH2Stream end-to-end. Same hot-path discipline as the feature pass: every new wire path is opt-in or sits on its own sub-tree, so the existing throughput floors are preserved by construction.

In-session gates that ran on the EPYC 7R32 dev-box at cycle handoff:

  • pixi run tests aggregate -- green; 700+ unit + integration + conformance + example cases pass (the 100+ new tests this cycle for QUIC AEAD + header protection + UDP reactor + H3 dispatch + h3 conformance + WsAutoClient runtime are wired into the aggregate).
  • pixi run fuzz-quic-packet-decrypt -- 200K runs, 0 crashes.
  • pixi run fuzz-quic-initial-handshake -- 200K runs, 0 crashes.
  • pixi run fuzz-quic-connection-id -- 200K runs, 0 crashes.
  • pixi run fuzz-h3-server -- 200K runs, 0 crashes.
  • pixi run test-safety-asserts -- 15 tests, all pass.
  • pixi run check-sans-io + pixi run check-no-http-http2-cycle + pixi run check-reactor-size -- all clean.

The H3 throughput row in HTTP/3 throughput vs Rust libs (EPYC 7R32 dev-box) stays at "pending h2load h3 build" until the nghttp2 + ngtcp2 + nghttp3 build documented there lands on the dev-box; the flare side of the bench gate (server, baselines, harness, statistics script) is wired and passes its unit + conformance + fuzz + loopback integration suites.

The full bench-vs-baseline regression sweep (h1 + h2 floors) runs on the dev-box at cycle handoff alongside the bench-h3 suite once the h2load h3 client lands; the floors carried through from the prior pass (flare 1w >= 79,028 req/s / p99 <= 3.33 ms; flare_mc 4w handler >= 214,567 req/s / p99 <= 2.71 ms; flare_mc_static 4w >= 246,942 req/s) are unchanged by this cycle's additive surfaces.

Best-perf refresh at HEAD 65b3282

Full pixi run -e bench bench-vs-baseline sweep on the EPYC 7R32 dev-box, 5x30s runs per target, mojo build -D ASSERT=none, cargo build --release --locked. Source: benchmark/results/2026-06-03T0331-ehsan-dev-65b3282/.

Single-worker plaintext (throughput config, flare vs nginx 1-worker vs go_nethttp GOMAXPROCS=1):

Workload Server Workers Median req/s p99 (ms) p99.9 (ms) p99.99 (ms) sigma%
throughput flare 1 76,287 3.18 3.56 3.78 1.27
throughput nginx (worker_processes 1) 1 73,828 3.14 3.46 3.66 1.27
throughput Go net/http (GOMAXPROCS=1) 1 39,800 3.47 4.99 6.73 1.27

Multi-worker plaintext (throughput_mc config, 4 workers, flare_mc handler path vs flare_mc_static fast path vs the Rust pack):

Workload Server Workers Median req/s p99 (ms) p99.9 (ms) p99.99 (ms) sigma%
throughput_mc flare_mc_static (peak) 4 279,541 n/a (saturation) n/a n/a 1.65
throughput_mc flare_mc_static (sustain) 4 249,293 2.68 (typ.) n/a n/a n/a
throughput_mc actix_web 4 227,528 2.71 3.04 4.37 0.17
throughput_mc flare_mc (handler) 4 221,299 2.65 2.98 3.26 0.17
throughput_mc axum 4 203,673 2.80 3.26 8.44 0.21
throughput_mc hyper 4 142,970 2.82 3.22 3.77 0.21

Floor-hold check vs the wire-paths check just above:

  • flare 1w: 76,287 req/s (vs 79,028 ceiling, 71,444 refresh floor) -- inside the cross-day spread; p99 3.18 ms <= 3.33 ms floor. HOLD.
  • flare_mc 4w handler: 221,299 req/s >= 214,567 floor; p99 2.65 ms <= 2.71 ms floor. HOLD; p99.99 3.26 ms is the tightest reading on file for this dev-box (vs 11.45 ms in the prior refresh, which carried two warmup-phase spikes).
  • flare_mc_static 4w: 249,293 req/s sustained (median peak 279,541) >= 246,942 floor. HOLD.

Microbenches at the same HEAD (pixi run bench-http / bench-parse / bench-huffman / bench-ws-mask) match the headline numbers in HTTP parsing microbenchmark and WebSocket SIMD masking below (Huffman SIMD 4x scalar; WS-mask SIMD 55x scalar at 1 KB, 33x at 64 KB, 21x at 1 MB; HeaderMap 1.5 us / Response 0.35 us / URL parse 0.7-0.8 us); zero drift from the prior checks.

The cross-framework HTTP/3 row now has the h2load+H3 client landed on the dev-box -- see the HTTP/3 throughput table below for the quiche / flare_h3 reading at this HEAD (the quinn baseline is miscalibrated at this workload shape and is excluded -- see the table caveats). An earlier reading had flare_h3 at 0 req/s because the inbound post-Initial decrypt path had not landed; a later pass (Jun 4, 2026) closed the decrypt path, and a follow-on reactor rewrite (copy elimination, cached-table QPACK decode, coalesced 1-RTT egress) closed the gate -- the table now reads a stable 74,653 req/s, +2.9 % over the quiche floor (see the table notes below).

Refresh check (Jun 3, 2026)

This pass (6 atomic commits on top of 65b3282) joined the wire-paths primitives into a live QUIC reactor + H3 dispatch loop: handshake bridge, post-Initial AEAD + handle_packet dispatch, UDP egress + full I/O cycle, H3 attach + Handler dispatch, bench baseline rewritten through serve_http3, and this docs sweep.

h1 / h2 floor reverify at this HEAD (pixi run -e bench bench-vs-baseline-quick on the same EPYC 7R32 dev-box, 5x30s plaintext runs):

Workload Server Workers Median req/s p99 (ms) sigma%
throughput flare 1 75,294 3.17 1.57
throughput go_nethttp (GOMAXPROCS=1) 1 39,232 3.22 0.00

vs the Best-perf refresh row just above (76,287 req/s p99 3.18 ms at HEAD 65b3282): 75,294 is inside the cross-day spread (both above the 71,444 refresh floor; p99 3.17 ms below the 3.33 ms floor). The QUIC + H3 surfaces added in this pass sit on their own sub-tree, so the TCP hot path's floors are preserved by construction. HOLD.

The bench-h3 cross-framework gate flare_h3 >= 72,571 req/s (quiche reading at HEAD 65b3282) did NOT close in that pass. The QUIC reactor I/O loop is live and the Handler dispatch chain reaches the Handler; the open gap is the rustls FFI wrapper at flare/tls/ffi/rustls_wrapper/src/lib.rs:447 discarding Option<KeyChange> so per-level Handshake / 1-RTT keys never reach the AEAD layer. h2load drives the boot-up + bind + Initial-decrypt path (5 datagrams sent, 0 received). Once the FFI bridge lands, pixi run -e bench bench-h3 all re-runs the cross-framework table and this section gains a flare_h3 row alongside the existing quiche + quinn rows above.

Update (Jun 4, 2026): decrypt gap closed, gate egress-bound

A later pass landed the rustls KeyChange bridge (Rust wrapper + Mojo bindings + per-level key install), the listener-level Handshake + 1-RTT header-protection + AEAD decrypt, a packet-number-space split (an inbound ACK advances largest_acked_by_peer, never the inbound pn-decode base), ACK ranges built from actually-received pns, and inbound datagram batching. The handshake now completes and h2load sustains a stable 351 req/s (sigma 3.19 %, p50 255 ms) at 1 client x 100 streams. The >= 72,571 req/s gate is still unmet, but the bottleneck has moved off the handshake: the single-stream path runs at 2,145 req/s with 105 us RTT, while the 100-stream gate workload was egress-bound -- one un-coalesced UDP datagram per H3 response, each built byte-by-byte through an AEAD + header-protection FFI crossing. A follow-on reactor rewrite (copy elimination + cached-table QPACK decode + coalesced 1-RTT egress with capacity-reserved builders) closed the gate to 74,653 req/s; egress sendmmsg/GSO were not needed for the gate and remain unwired (built in flare.udp.batch but QUIC egress still uses per-datagram sendto; ingress recvmmsg is default-on). See the HTTP/3 throughput table for the final reading.

Sanitizer + fuzz + lint floors (each gate ran at its introducing commit; the wire-paths floors carry forward intact, no new breakage):

  • pixi run tests aggregate -- green per-commit across the pass; the new test files (test_quic_handshake_bridge.mojo, RFC 9001 Appendix A.4 + A.5 vectors, test_quic_loopback_integration.mojo egress cases, test_h3_end_to_end.mojo) are all in the tests aggregate.
  • pixi run fuzz-quic-initial-handshake -- 200K runs green at introduction.
  • pixi run fuzz-quic-packet-decrypt -- 200K runs green over Initial + Handshake + 1-RTT branches.
  • pixi run fuzz-h3-server -- 200K runs green including the QuicListener dispatch branch.
  • pixi run test-safety-asserts -- green at each commit.
  • pixi run check-sans-io + pixi run check-no-http-http2-cycle
    • pixi run check-reactor-size -- clean at each commit.

The full sanitizer trio (pixi run tests-asan / pixi run tests-tsan) re-runs once the rustls KeyChange FFI bridge lands -- the reverify confirmed the existing 2 pre-existing ASan failures + the pre-existing TSan linker errors carry forward without new regressions on the touched surfaces.

Listener-mode A/B (flare-only)

flare exposes both listener strategies so the operator can choose between throughput and tail latency:

flare path Listener mode Peak rps p99.99 (ms)
flare_mc_static (default) per-worker SO_REUSEPORT 246,942 17.50 ± 678.01
flare_mc_static + FLARE_REUSEPORT_WORKERS=0 shared listener + EPOLLEXCLUSIVE 214,306 (-13 %) 3.26 (median)
flare_mc handler (default) per-worker SO_REUSEPORT 214,567 3.26 ± 50.52
flare_mc handler + FLARE_REUSEPORT_WORKERS=0 shared listener + EPOLLEXCLUSIVE 196,757 (-8 %) 3.23 (median)

(The default rows are the HEAD numbers from the 4-worker table above. The shared-listener rows are from the prior historical reference at the same dev-box and are kept as a within-noise comparison point; refresh against HEAD with FLARE_REUSEPORT_WORKERS=0 pixi run -e bench bench-vs-baseline --only flare_mc,flare_mc_static --configs throughput_mc if you need to re-validate the listener-mode delta on your own dev-box.)

Picking a mode:

  • per-worker SO_REUSEPORT (default) — each worker accept(2)s on its own fd, kernel hashes new 4-tuples to one of N listeners. Higher throughput on unpinned dev-boxes / high-core-count instances because each worker's accept loop is fully independent (no cross-worker EPOLLEXCLUSIVE wakeup overhead). Matches actix_web's listener strategy. This is what the headline "vs Rust libs" numbers above use.
  • EPOLLEXCLUSIVE shared listener (FLARE_REUSEPORT_WORKERS=0) — the kernel offers each accept event to whichever worker is currently parked in epoll_wait. Idle workers absorb spikes; busy workers aren't burdened with extra accepts. Strictly tighter p99.99 σ under sustained load (3.23 ms median in the historical CPU-pinned reference, where actix_web's per-worker reuseport hits 21.61 ms p99.99 from listener-distribution variance). Trades 7-22 % req/s (handler vs static fast path) for that tighter tail.

1-worker frameworks (single-thread reactor / event loop):

Server Workers Peak rps p99 (ms) p99.99 (ms) R= used
nginx (worker_processes 1) 1 68,875 2.95 3.59 70,000
flare (reactor) 1 52,147 2.79 3.46 53,000
Go net/http (GOMAXPROCS=1) 1 34,439 2.98 4.70 35,000

Single-worker comparison: flare 1w sustains 76 % of nginx 1w throughput (was 88 % on the historical CPU-pinned reference) and 1.51x of Go 1w (was 1.56x). nginx's tight C event loop loses less to dev-box scheduler noise than flare's reactor does, so the gap widens on the unpinned dev-box. flare keeps the tightest p99 / p99.99 in the single-worker pack: 3.46 ms vs nginx 3.59 / Go 4.70.

Reading order:

  1. Tail latency: flare wins at both p99 and p99.99 of the five 4-worker frameworks. flare_mc handler is 3.23 ms p99.99 vs actix_web 3.43 / hyper 3.70 / axum 3.74. The ~3 ms p99.99 floor is the dev-box scheduler-noise floor; inside that floor, flare's per-request hot path is at least as quiet as the rust frameworks' equivalents.
  2. Throughput: actix_web leads the dev-box. 248K vs flare_mc_static 214K (-14 %) vs flare_mc handler 197K (-21 %). actix_web's per-worker SO_REUSEPORT listeners (which inflated p99.99 on the historical CPU-pinned run) actually help it on the unpinned dev-box: the kernel spreads load across the four listeners directly without waking idle workers through epoll_wait.
  3. flare is at parity with hyper / axum on throughput. flare_mc_static slightly beats hyper (214K vs 209K, +3 %) and clearly beats axum (214K vs 190K, +13 %). flare_mc handler beats axum (197K vs 190K) and trails hyper slightly (197K vs 209K, -6 %).
  4. The historical CPU-pinned numbers are still the reference for tail latency. flare_mc on the CPU-pinned EPYC reference holds p99.99 = 3.11 ms — even tighter than the 3.23 ms above. The 9-of-flare improvements documented in the next section don't touch the dispatch-loop or accept fairness, so the CPU-pinned tail shape carries forward.

Methodology: peak-sustainable vs saturate-mode

wrk2 -R<rate> is a target-rate generator. If the server keeps up, you get <rate> req/s with bounded tail latency. If the server can't keep up, requests queue inside wrk2's HdrHistogram and the coordinated-omission correction amplifies the tail by the queueing time — so a server pushed past saturation reports req/s near its true peak but p99.99 in the hundreds of ms or seconds, not because of a real tail bug, but because the cliff was crossed.

The table above is peak-sustainable: the cliff was found by sweeping R= upward, and the reported row is the highest R= that holds p99.99 ≤ 5 ms (≈2× the dev-box noise floor). One step above each row's R= and the same server's p99.99 jumps to 100 ms+ — the cliff is sharp, not gradual.

Server Last tight-tail row First post-cliff row
flare_mc_static (REUSEPORT) R=260K → p99.99 = 3.54 ms R=270K → p99.99 = 383 ms
actix_web R=250K → p99.99 = 3.43 ms R=260K → p99.99 = 69.57 ms
flare_mc handler (REUSEPORT) R=240K → p99.99 = 3.47 ms R=245K → p99.99 = 584 ms
hyper R=210K → p99.99 = 3.70 ms R=220K → p99.99 = 35.46 ms
axum R=195K → p99.99 = 3.74 ms R=210K → p99.99 = 451 ms
flare_mc_static (default) R=220K → p99.99 = 3.26 ms R=245K → p99.99 = 1.07 s
flare_mc handler (default) R=200K → p99.99 = 3.23 ms R=205K → p99.99 = 1.49 s
flare 1w R=53K → p99.99 = 3.46 ms R=55K → p99.99 = 186 ms
nginx 1w R=70K → p99.99 = 3.59 ms R=73K → p99.99 = 1.69 s
Go 1w R=35K → p99.99 = 4.70 ms R=40K → p99.99 = 24.99 ms

This is why bench harnesses that auto-calibrate R= from a coarse probe land different rows on different sides of the cliff and produce noisy comparisons. The peak-sustainable operating point is the only honest single-rate number for a given server on a given host.

What changed under the hood

The improvements above are five concrete code changes:

What How Where
Per-request parser cost New _parse_http_request_bytes_minimal skips HeaderMap build entirely when the handler doesn't read headers; opt-in via ServerConfig.skip_header_decode_for_short_requests flare/http/_server/parse.mojo, flare/http/server.mojo (ServerConfig), flare/http/_server_reactor_impl.mojo
Per-request keep-alive policy _connection_is_keepalive / _connection_is_close byte fast-paths replace the per-request _ascii_lower allocation for canonical Connection: keep-alive / close flare/http/_server_reactor_impl.mojo
Read-buffer compaction _compact_read_buf_drop_prefix replaces 5 inlined per-byte append loops with a single memcpy shift flare/http/_server_reactor_impl.mojo
Fixed-response specialisation HttpServer.serve_static_multicore(resp, num_workers=N) runs the static fast path under StaticScheduler — recv -> _scan_content_length -> memcpy(resp.bytes) -> send, no parser / handler / Response alloc flare/http/server.mojo, flare/runtime/scheduler.mojo, flare/http/_server_reactor_impl.mojo
io_uring substrate completeness IORING_REGISTER_PBUF_RING (2.7x kernel-bench faster than PROVIDE_BUFFERS), IORING_RECV_MULTISHOT routing fix (was silently degrading to oneshot), IORING_SETUP_* flag plumbing (COOP_TASKRUN / DEFER_TASKRUN / SINGLE_ISSUER / SUBMIT_ALL), peek-then-block UringReactor.poll, enable_wakeup=False mode for single-issuer rings flare/runtime/io_uring_sqe.mojo, flare/runtime/io_uring_driver.mojo, flare/runtime/uring_reactor.mojo

Reproducibility

hyper, axum, actix_web, nginx, go_nethttp baselines all live under benchmark/baselines/, pinned via Cargo.lock (Rust frameworks), conda-forge nginx 1.25 and go 1.24. Raw run data (env, integrity gate, per-target JSON, raw wrk2 stdout) lands under benchmark/results/<timestamp>-<host>-<commit>/ when you reproduce locally; results aren't tracked in the repo.

Reproduce on a Linux box with ≥ 8 physical cores:

pixi run --environment bench bash benchmark/scripts/bench_vs_baseline.sh \
    --only=flare,flare_mc,hyper,axum,actix_web,nginx,go_nethttp \
    --configs=throughput_mc

Worker counts default to 4 for flare_mc (FLARE_BENCH_WORKERS=4) and to whatever is hard-coded in each baseline's run script (1 for nginx / Go, 4 for the Rust frameworks). Override flare_mc with FLARE_BENCH_WORKERS=N to scale; the bench harness re-runs the peak-finder at the new worker count automatically.

HTTP/2, TLS, and upload/download workload harnesses

Beyond the plaintext throughput matrix, the harness ships the wire + workload variants the assessment called for:

  • HTTP/2 (h2c): benchmark/scripts/bench_h2.sh drives h2load over cleartext HTTP/2 against the flare unified server and the nginx / Go / Rust baselines under benchmark/configs/h2_throughput.yaml, mirroring bench_h3.sh. Run with pixi run --environment bench bench-h2 (or bench-h2-flare for the flare row only).
  • TLS termination, uploads, downloads, mixed keep-alive: the workload Lua scripts already live under benchmark/scripts/ (wrk_tls_handshake.lua, wrk_upload_{4kb,64kb,1mb,16mb}.lua, wrk_download_{4kb,64kb,1mb,16mb}.lua, wrk_mixed_keepalive.lua); bench_vs_baseline.sh --workloads=<name> selects them.

Host blocker (documented): publishing a byte-stable cross-framework h2 / TLS / upload / download table -- and re-calibrating the quinn / quiche HTTP/3 baselines against the current quiche 0.24.5 -- requires a quiet, isolated multi-core Linux box (no co-tenant load) so the 3% variance gate holds. The CI runners and the shared dev sandbox do not meet that bar; the harnesses above produce the tables unchanged once run on such a host. The headline plaintext tables in this doc were captured on the EPYC 7R32 dev-box under those conditions.

Production / bench build flags

The headline numbers above use the same compiler posture as the Rust baselines: full -O3 optimisation with all debug_assert calls compiled out. That's what cargo build --release --locked produces for actix_web / hyper / axum, and what flare's bench harness does via:

mojo build -D ASSERT=none -I . benchmark/baselines/flare_mc/main.mojo \
    -o target/bench_baselines/flare_mc
./target/bench_baselines/flare_mc

(mojo build defaults to -O3; -D ASSERT=none compiles out every debug_assert[assert_mode="safe"] call -- the FFI-boundary safety asserts that the Mojo stdlib default ASSERT=safe keeps in the binary. See docs/build.md for the full assert-mode hierarchy + the sanitizer harness.)

For production deployments, use the same shape: build with mojo build -D ASSERT=none and run the resulting binary. The mojo run my_app.mojo JIT path keeps the safety asserts on, which is fine for development (catches use-after-free, EBADF, EFAULT in the FFI layer before they become silent kernel-mode UB) but adds a measurable per-event tax on the reactor hot path. Bench numbers and production traffic see the same posture only when both build with -D ASSERT=none.

The dev-time flow stays simple: pixi run tests, the sanitizer harnesses, every example -- they all run under the default ASSERT=safe. Only the bench-vs-baseline* tasks and production deployments need the no-asserts build.

Platform footnote

Three things about the Linux column are deliberate, not "flare can only hit 80K/s in production":

  1. GOMAXPROCS=1, worker_processes 1, and single-thread flare. Every baseline runs on one logical core so the comparison is apples-to-apples about per-core request-processing cost. This models the cheapest hosting tier (one vCPU) rather than peak throughput on the box. Production deployments on either platform should scale with worker count (nginx, Go) or with SO_REUSEPORT sharding (flare).
  2. wrk and the server are not CPU-pinned. On a 64-vCPU AWS instance the Linux scheduler migrates both processes across cores between slices, causing L1/L2 misses and occasional SMT-sibling contention. Pinning wrk and the server to two different physical cores on the same NUMA node typically recovers 15 to 30 % for Go on EPYC (a known net/http behaviour on shared-scheduler Linux, which is also where the flare-vs-Go ratio shift between the two platforms comes from). The harness intentionally does not pin so the numbers match an un-tuned deployment.
  3. c5-class EC2 does not turbo like M-series. Single-thread throughput on EPYC 7R32 is roughly half of an Apple M-series P-core for HTTP plaintext. That is microarchitecture, not a scheduler or runtime property. flare and nginx both drop by about 2x between the two tables; Go drops by about 3.5x because its goroutine plus netpoll overhead is a bigger percentage of each request on the slower core.

Soak: long-running operational gates

The throughput tables above answer "is it fast right now". The soak harness answers "is it still alive at 4 a.m. on day 2". Three operational signals microbenchmarks miss:

  • RSS over time — does memory grow linearly, plateau, or spike under churn?
  • File descriptors — are accept-loop / TLS / connection bookkeeping leaking fds under churn?
  • Tail-latency drift — does p99 stay flat at hour 24 or does a slow pathology creep in?

Three tiers, one harness

The driver lives in benchmark/scripts/_run_soak.sh. A single set of scripts runs at three tiers via the SOAK_DURATION_SECS env knob (defaults to 60 s):

Tier Per-workload duration Total wall time When to run
smoke 60 s ~3 min PR / iterative dev (pixi run --environment bench bench-soak-smoke)
extended 300 s ~15 min Before pushing larger changes (pixi run --environment bench bench-soak-extended)
release gate 86 400 s (24 h) ~24 h per workload, ~3 days serial Linux EPYC, manual one-shot pre-tag

Release-gate invocation pattern (one workload per box-day, run serially or in parallel on different EPYC boxes):

SOAK_DURATION_SECS=86400 pixi run --environment bench bench-soak-slow-clients
SOAK_DURATION_SECS=86400 pixi run --environment bench bench-soak-churn
SOAK_DURATION_SECS=86400 pixi run --environment bench bench-soak-mixed

Same _run_soak.sh driver, same summary.json schema, same gate logic — only the duration changes. The 24 h gate is the release-blocking one; smoke and extended are catch-loud-failures filters.

Workloads + gates

All three workloads target /plaintext on the flare bench server boot from benchmark/baselines/flare/main.mojo — the same entry point the bench-vs-baseline throughput harness uses, so soak numbers are directly comparable to the single-worker throughput tables above. The wrk lua scripts live in benchmark/scripts/wrk_soak_*.lua.

slow-client

256 concurrent connections, each issuing a short POST request every ~100 ms (wrk's delay() model is the closest approximation to "1 byte / 100 ms" inside wrk's protocol shape). Gate:

  • pass = errors == 0 && rss_end <= 2 * rss_start
  • 24 h release-gate variant additionally requires rss_end ≈ rss_start after the first hour (RSS-flat).

The lua approximation does not byte-trickle inside a single request body the way a true byte-trickle harness would; the read_body_timeout_ms deadline path is exercised end-to-end in tests/http/test_server_deadlines.mojo instead. Soak covers the resource-exhaustion shape (many connections held under pressure).

churn

64 concurrent connections, every request sets Connection: close so the server closes after each response. wrk reopens for the next request. Effective rate is bounded by ephemeral-port turnover. Gate:

  • pass = errors == 0 && fd_end <= fd_start + 16
  • 16-fd slack covers timer / wakeup / log fds beyond the per-connection fds. The fd_end measurement happens after a 3 s post-wrk drain pause so in-flight connections finish their close handshake before the observer's last sample.

mixed

64 concurrent connections, ~20 % tagged with Connection: close (every 5th request), the remaining 80 % standard HTTP/1.1 keep-alive. Catches regressions in the connection-disposition path that pure keep-alive load doesn't exercise. Gate:

  • pass = errors == 0 && rss_end <= 2 * rss_start

Output schema

Each per-workload run writes build/soak/<workload>/<timestamp>-<host>-<commit>/summary.json with the following fields. The schema is stable so EPYC release- gate runs can be aggregated by per-tag publication tooling without script edits:

{
  "workload":          "slow_clients",
  "tier":              "smoke",
  "duration_secs":     60,
  "wrk_threads":       2,
  "wrk_connections":   256,
  "commit":            "9755049",
  "host":              "ehsan-dev",
  "wrk": {
    "requests_total":           7424,
    "requests_per_sec":         2461.3,
    "duration_secs_actual":     3.02,
    "p50_ms":                   341.0,
    "p75_ms":                   612.0,
    "p90_ms":                   970.0,
    "p99_ms":                   2070.0,
    "socket_errors_connect":    0,
    "socket_errors_read":       0,
    "socket_errors_write":      0,
    "socket_errors_timeout":    0,
    "non_2xx_3xx":              0
  },
  "rss_kb_start":      193552,
  "rss_kb_end":        195088,
  "rss_kb_max":        195088,
  "fd_count_start":    55,
  "fd_count_end":      55,
  "fd_count_max":      311,
  "observe_samples":   8,
  "gates": {
    "rss_within_2x":   true,
    "fd_end_bounded":  true,
    "server_alive":    true,
    "no_non_2xx":      true
  },
  "pass":              true
}

Companion files in the same directory:

  • wrk.txt — raw wrk stdout including the latency distribution.
  • observe.jsonl — per-second (or per-5-s for the 24 h tier) RSS / fd-count samples. One JSON object per line: {"ts_ms": 12345, "rss_kb": ..., "hwm_kb": ..., "peak_kb": ..., "fd_count": ...}.
  • server.{stdout,stderr} — flare bench server output.
  • observer.stderr — observer-side stderr.

Dev-box smoke + extended results (Ubuntu 22.04, 6 vCPU AWS)

These are NOT release-gate numbers. They are smoke artefacts captured on the maintainer's AWS Ubuntu 22.04 dev box (glibc 2.35, x86_64) at commit 9755049. The release-gate p99.9 / p99.99 numbers + 24 h flat-RSS proof are captured on Linux EPYC and live in the per-tag release notes.

What the tables prove on this hardware: the harness fires cleanly, the gates evaluate against real data, and the dev-box server holds steady under all three workloads at the smoke + extended durations (no crashes, no fd leaks, RSS within ~1 % of cold-start across both tiers).

Smoke tier (60 s/workload, ~3 min total)

Workload req/s Total req p50 (ms) p99 (ms) RSS start (KB) RSS end (KB) RSS max (KB) fd start fd end fd max non-2xx Pass
slow-client 2 546.4 152 948 0.18 1.07 192 588 194 124 194 124 55 55 311 0 yes
churn 27 805.5 1 668 403 2.00 2.34 193 488 194 000 194 000 55 55 119 0 yes
mixed 49 544.7 2 972 718 1.01 2.76 193 492 194 004 194 004 55 55 119 0 yes

Extended tier (300 s/workload, ~15 min total)

Workload req/s Total req p50 (ms) p99 (ms) RSS start (KB) RSS end (KB) RSS max (KB) fd start fd end fd max non-2xx Pass
slow-client 2 547.8 764 471 0.19 0.85 193 492 195 028 195 028 55 55 311 0 yes
churn 27 805.0 8 341 534 1.99 2.34 194 196 194 708 194 708 55 55 119 0 yes
mixed 50 072.9 15 021 927 0.99 2.70 192 152 192 664 192 664 55 55 119 0 yes

Two cross-tier observations:

  • RSS deltas are essentially identical between smoke (60 s) and extended (300 s): ~0.5–1.5 MB across all three workloads in both tiers. A 5x duration increase did not produce a 5x RSS increase — per-request allocator churn is bounded rather than leaking. The 24 h gate is what pins this assertion long-term.
  • fd_count returns to baseline in both tiers across all workloads (fd_end == fd_start == 55). The 3 s post-wrk drain pause documented at the top of _run_soak.sh is what makes this measurement honest — without it the observer would race wrk-exit and report ~30–250 in-flight fds as a false-positive "leak".

Limitations

  • Linux only. /proc/<pid>/status is the RSS source; macOS would need ps -o rss= fallback. The release gate runs on Linux EPYC anyway.
  • wrk-driven slow-client is an approximation — see the slow-client section above. The byte-trickle path is exercised by unit tests instead.
  • The smoke and extended tiers cannot prove "RSS flat after 1 hour" — that signal lives only in the 24 h release-gate run. Smoke / extended catch only loud failures (server died, RSS doubled, fds leaked beyond a small constant).

Methodology

The TFB plaintext workload (TechEmpower test #6) is GET /plaintext returning the 13-byte body Hello, World! with Content-Type: text/plain, HTTP/1.1 keep-alive on, no gzip, no logging.

Measurement rules:

  • Response-byte integrity: before any measurement round, each baseline is probed once and its response bytes are diffed against the workload spec. A target producing a different status, body length, or non-whitelisted header is rejected before the measurement starts. Headers allowed to vary per target: Date, Server, Connection, Keep-Alive.
  • Pinned toolchains: Go and nginx pin to [feature.bench. dependencies]; the conda-forge wrk package is on PATH for ad-hoc use; wrk2 is built from a pinned commit by bench-install-wrk2 (the harness drives wrk2 explicitly via build/wrk2/wrk2, not whatever wrk is on PATH).
  • Calibrated-peak wrk2 (see Workload + harness shape above for the full four-phase description): a 5 s settle, an overdrive probe, a five-step binary search for the highest fixed rate that holds p99 ≤ 50 ms AND achieved ≥ 90 % of requested, then five 30 s measurement rounds at 90 % of that calibrated peak. Stability gate fires at 3 % stdev on req/s across the measurement rounds.
  • Load generator: wrk2 with --latency for the headline bench (CO-corrected tail percentiles up to p99.999). The conda-forge wrk package is still on PATH for ad-hoc use. Never ab or h2load, for consistency with published TFB-style numbers. Two configs ship today — throughput.yaml (-t1 -c64 -d30s) for per-core request-processing cost, and throughput_mc.yaml (-t8 -c256 -d30s) for saturating a thread-per-core server. Server and wrk run on the same host over loopback.
  • Per-run provenance: every run writes its own directory under benchmark/results/<yyyy-mm-ddTHHMM>-<host>-<git-sha>/ containing env.json (CPU model, OS, kernel tunables, exact toolchain versions), integrity.md, per-tuple result JSONs, summary.md, and raw wrk stdout under RAW/.

This protocol (integrity check, pinned toolchains, 5-run median with stdev gate) is stricter than TFB's own single 15 s round on shared hardware. It is closer to the reproducibility setups in simdjson and rapidjson.


HTTP parsing microbenchmark

Apple M-series (pixi run bench-compare):

Operation Latency
Parse HTTP request (headers + body) 1.7 us
Parse HTTP response 2.2 us
Encode HTTP request 0.7 us
Encode HTTP response 0.9 us
Header serialization 0.12 us

On EPYC 7R32 (Linux, AVX2) the same ops are about 1.4x slower per-op (e.g. request parse 2.35 us, response parse 6.45 us), consistent with the single-core throughput gap in the server-throughput tables above. Run pixi run bench-compare on either platform to reproduce.

A reminder from the criticism: a parser is not the bottleneck on a 13-byte response. The microbenchmark is useful as a guard against parser regressions, not as a headline.


HTTP/3 throughput

The HTTP/3 cross-framework throughput table is the hard release gate for the QUIC + H3 wire-paths work. The four flare-side wiring follow-ups (AEAD backend, rustls QUIC binding, QUIC reactor, H3 driver dispatch) all landed in this cycle, so the dependency chain in front of the bench harness is now:

  • flare/tls/ffi/openssl_wrapper.cpp -- AEAD seal / open + header-protection mask thunks (tested against RFC 9001 Appendix A vectors + cross-validated with aioquic).
  • flare/tls/ffi/rustls_wrapper/ -- rustls 0.23 ServerConnection over a C ABI; idempotent build via flare/tls/ffi/build_rustls.sh.
  • flare/quic/server.mojo -- UDP listener + per-datagram dispatch + TimerWheel-driven PTO / idle / ack-delay + CC reactor (loopback handshake -> 1-RTT -> close tested vs. a vendored quinn smoke client).
  • flare/http3/server.mojo -- Http3Connection.feed_stream_chunk through Http3RequestReader -> Handler -> response writer, uni-stream type dispatch, SETTINGS + GOAWAY consumption, conformance round-trip vs. aioquic + quiche fixtures.

Infrastructure for the cross-framework bench:

  • benchmark/configs/h3_throughput.yaml -- workload definition (1 client x 100 streams x 100k requests, 30 s duration, 5 runs, 8 % sigma honesty meter -- looser than the h2-over-TCP 3 % gate because QUIC pacing + RTT estimation has wider run-to-run variance).
  • benchmark/baselines/flare_h3/ -- flare HTTP/3 baseline binary (mojo build -D ASSERT=none; mirrors the shape of benchmark/baselines/flare/ so the same harness drives both).
  • benchmark/baselines/quinn/ -- quinn 0.11 + h3 0.0.8 baseline (cargo build --release --locked; serves the same route surface as the hyper baseline).
  • benchmark/baselines/quiche/ -- quiche 0.22 baseline (cargo build --release --locked; boringssl-vendored).
  • benchmark/scripts/bench_h3.sh -- the bench loop runner (build + start + readiness + warmup + 5x measurement + aggregate + stop).
  • benchmark/scripts/_stat_h3.py -- per-percentile aggregator reading h2load --log-file per-request timings + writing benchmark/results/v0.8/h3/${TARGET}.json.
  • pixi run --environment bench bench-h3 {flare,quinn,quiche,all} -- task entry points.

Status: an earlier pass (Jun 3, 2026) joined the wire-paths primitives into a live QUIC reactor + H3 dispatch loop. The bench baseline now drives through HttpServer.bind_with_http3 + serve_http3(handler); the reactor's recv -> dispatch -> handle -> drain -> protect -> sendto cycle is live and the Handler dispatch chain reaches the Handler on completed streams. A later pass (Jun 4, 2026) then closed the inbound post-Initial decrypt path the gate depended on: the rustls FFI wrapper now returns KeyChange, per-level Handshake / 1-RTT keys reach install_handshake_keys / install_1rtt_keys, and the listener strips header protection + AEAD-decrypts Handshake + 1-RTT datagrams through the slot's rustls session. With the packet-number-space split (an inbound ACK advances largest_acked_by_peer, never the inbound pn-decode base) the handshake completes and h2load sustains a stable reading. A follow-on reactor rewrite then closed the cross-framework gate (flare_h3 median req/s >= 72,571 req/s sigma <= 8 %): flare HTTP/3 leads at 74,653 req/s (median, +2.9 % over quiche, sigma 0.50 %). The win came from eliminating per-packet whole-connection deep copies (in-place ref mutation), a cached-table QPACK decode path, and coalesced 1-RTT egress with capacity-reserved packet builders -- not from egress syscall batching (ingress recvmmsg is built and default-on; egress sendmmsg/GSO are built in flare.udp.batch but not yet wired into QUIC egress; the gate closed without them). See the table and reading order below. Stock Ubuntu's nghttp2-client (1.43) predates h3 support; conda-forge's nghttp2 (1.68) ships without h2load. The h2load binary used for the numbers below was built from source on the EPYC 7R32 dev-box against vendored ngtcp2 + nghttp3 + quictls (OpenSSL 3.0 + QUIC fork). The build recipe:

# One-time. Builds quictls, ngtcp2, nghttp3, then nghttp2's
# h2load. Takes ~10-15 min on the EPYC dev-box. Installs
# the four libraries under /usr/local/lib/h3-stack/ and
# registers them with ldconfig so the h2load binary at
# nghttp2/src/.libs/h2load picks them up at runtime.
# Requires: autoconf, automake, libtool, cmake, pkg-config,
#           g++-12 (C++20), libc-ares-dev, libev-dev,
#           libevent-dev, libjansson-dev.
git clone -b openssl-3.0.15+quic https://github.com/quictls/openssl
( cd openssl && ./Configure --prefix=/usr/local/lib/h3-stack \
    enable-tls1_3 && make -j && sudo make install_sw )
git clone --recurse-submodules https://github.com/ngtcp2/ngtcp2
( cd ngtcp2 && autoreconf -i && \
    PKG_CONFIG_PATH=/usr/local/lib/h3-stack/lib/pkgconfig \
    ./configure --with-openssl && make -j && sudo make install )
git clone --recurse-submodules https://github.com/ngtcp2/nghttp3
( cd nghttp3 && autoreconf -i && ./configure && make -j && \
    sudo make install )
git clone --recurse-submodules -b v1.69.0 https://github.com/nghttp2/nghttp2
( cd nghttp2 && autoreconf -i && \
    CXX=g++-12 CC=gcc-12 \
    PKG_CONFIG_PATH=/usr/local/lib/h3-stack/lib/pkgconfig \
    ./configure --enable-http3 --enable-app && make -j )
echo /usr/local/lib/h3-stack/lib | sudo tee /etc/ld.so.conf.d/h3-stack.conf
sudo ldconfig
sudo cp nghttp2/src/.libs/h2load /usr/local/bin/h2load

Once the binary is on PATH, pixi run -e bench bench-h3 all populates the cross-framework table below directly. Current reading at HEAD b9aeeef (source data: benchmark/results/v0.8/h3/, 5x30s runs, 1 client x 100 concurrent streams per run):

Target req/s (median, 5 runs) p99 (ms) p99.9 (ms) p99.99 (ms) notes
quiche 0.22 (boringssl-vendored) 72,571 (per-stream) (per-stream) (per-stream) clean: 0 errors / 0 timeouts across 5 runs, sigma ~1%
quinn 0.11 + h3 0.0.8 N/A* 4.17 4.75 5.48 NOT A VALID COMPARISON -- excluded from the gate. Open-loop 100-stream shape errors 98% (1.2M errored of 1.2M total); the 654 req/s figure is a harness artifact, not a quinn ceiling (the 10s warmup with the same client sustained 104k req/s at 0 errors). Recalibrate benchmark/configs/h3_throughput.yaml before citing quinn.
flare h3 74,653 1.45 2.45 433.45 Gate MET: beats quiche's 72,571 by +2.9% at sigma 0.50% (stable across 5x30s runs). The reactor rewrite removed the per-packet/per-op whole-state deep copies (in-place ref mutation of the connections / http3_connections slabs), routed QPACK decode through the cached-table SIMD Huffman path + a build-once static table + slice-based literal decode, and reserved capacity on the hot List[UInt8] egress builders so the per-datagram assembly fills one allocation instead of growing byte-by-byte. The last lever mattered most: the allocator's thread-local cache lives in a dlopen'd lib, so every spared malloc/free also spares a slow dynamic-TLS lookup, which collapsed both the List._realloc and __tls_get_addr profile peaks. p99.9 fell from 410 ms (first pass) to 2.45 ms; the lone p99.99 outlier (433 ms) is a per-run connection-setup artifact, not a steady-state stall. Egress was already coalesced (ACK + flow-control + multiple H3 STREAM frames per 1-RTT datagram); ingress recvmmsg is built and default-on, while egress sendmmsg/GSO are built in flare.udp.batch but not yet wired into QUIC egress -- the gate closed without egress batching.

Note (Jun 16, 2026 re-measurement): the published comparison above is against the quiche 0.22 baseline. After the baseline crate was bumped to quiche 0.24.5, a re-run on the EPYC dev-box re-confirmed flare HTTP/3 at 74,003 req/s (median, 5x30s, sigma 0.51%, p99 1.46 ms) -- within run-to-run noise of the 74,653 above, so the table stands. The quiche 0.24.5 reference server, however, could not be benched on this host: it serves exactly one request per connection, then quiche::Connection::send returns CryptoFail on every subsequent datagram. The failure is isolated to the AES-GCM cipher path -- quiche selects BoringSSL's stateful EVP_aead_aes_128_gcm_tls13 AEAD for AES-128-GCM (vs the stateless EVP_aead_chacha20_poly1305 for ChaCha20) -- reproduces identically on both the boringssl- vendored and boringssl-boring-crate backends, and is not a harness artifact (the baseline mirrors quiche's own examples/http3-server.rs and fails even at one stream per connection). The openssl backend was not a usable substitute on this host either: stock OpenSSL 3.6 omits the quictls SSL_set_quic_method API that quiche 0.24.5 links, the installed quictls 3.0.15 predates EVP_CIPHER_CTX_dup (OpenSSL 3.2+), and the only quictls +quic branch at 3.2 or newer (openssl-3.3.0+quic) fails to compile. The quiche 0.22 number is therefore retained as the last reproducible cross-framework reading on this hardware.

Reading order:

  • flare HTTP/3 now leads the table at 74,653 req/s -- a +2.9% margin over quiche at a tighter 0.50% run-sigma. The gate (flare_h3 median >= 72,571 req/s, sigma <= 8%) is MET. The win came from the reactor rewrite, not from new syscall batching: eliminating per-packet whole-connection / whole-HTTP/3 deep copies, a cached-table QPACK decode path, and reserving the hot egress buffers so each datagram is assembled in one allocation. Because the Mojo allocator's per-thread cache is reached through a dlopen'd library's dynamic-TLS slot, cutting allocation volume also cut the __tls_get_addr / _dl_update_slotinfo overhead that dominated the profile, so the allocation fix paid off twice. p99/p99.9 are 1.45/2.45 ms; the single 433 ms p99.99 reading is a per-run setup artifact.
  • quiche is the steady-state HTTP/3 reference at this workload shape: 72.5k req/s with 1% run-σ and zero errored streams.
  • quinn is excluded from the gate comparison: its headline 654 req/s number reflects the harness saturating the server's per-connection stream window at the open-loop 100-streams configuration, not a steady-state throughput ceiling. h2load's 10s warmup with the same client ran 104k req/s @ 0 errors before the long-duration shape exposed the per-conn limit. Do not cite 654 req/s as a baseline; the calibration knob lives in benchmark/configs/h3_throughput.yaml (h2load_streams
    • h2load_duration_seconds) and must be re-run before quinn is a valid comparison.
  • flare HTTP/3 at 74,653 req/s reflects the reactor rewrite. The inbound post-Initial decrypt path was already live after the first pass (rustls KeyChange -> per-level keys, Handshake + 1-RTT AEAD/HP decrypt, packet-number-space split); the first pass measured only 351 req/s because per-inbound-packet and per-egress-op work deep-copied the whole QuicConnection / Http3Connection slab entry (each holds a Dict of every open stream), making 100 concurrent streams quadratic. The rewrite bound a mutable ref into the slab slot and mutated in place, routed QPACK decode through the cached-table SIMD Huffman path + a build-once static table + a slice-based literal decoder, and reserved capacity on the hot List[UInt8] egress builders. The allocation reduction was the decisive lever: the Mojo allocator's per-thread cache is reached through a dlopen'd library's dynamic TLS, so every malloc/free spared also spared a slow __tls_get_addr -> _dl_update_slotinfo walk -- the two profile peaks fell together. Egress coalescing (ACK + flow-control + multiple H3 STREAM frames packed per 1-RTT datagram, MTU-bounded) was already in place; the gate closed without needing egress sendmmsg/GSO batching (those helpers are built in flare.udp.batch but not wired into QUIC egress; ingress recvmmsg is built and default-on). UDP SO_RCVBUF/SO_SNDBUF setters + the FLARE_QUIC_RCVBUF / FLARE_QUIC_SNDBUF env knobs were added but default off: raising the receive buffer on this single-reactor loopback shape added queuing delay (bufferbloat) that hurt both throughput and the tail, so the kernel default is kept.

pixi run -e bench bench-h3 all re-runs this table without further script edits; the floor-hold row in "Best-perf refresh at HEAD" gains a matching HTTP/3 entry at the next dev-box sweep.

bench_h3.sh exits 0 with a clear banner when h2load with H3 support isn't on PATH (older dev-boxes / CI runners), so CI continues to pin "infrastructure ready, h3 client install pending" as a known posture rather than a regression on those hosts.


WebSocket SIMD masking

RFC 6455 requires XOR-masking every client-to-server byte. SIMD gives a 14-35x speedup for payloads above 128 bytes.

Apple M-series (NEON, SIMD-32):

Payload Scalar SIMD-32
1 KB 3.2 GB/s 112.6 GB/s
64 KB 3.4 GB/s 47.8 GB/s
1 MB 3.4 GB/s 54.8 GB/s

EPYC 7R32 (Linux, AVX2, SIMD-32) is in the same regime — peak 90.6 GB/s at 1 KB (58x scalar), 52.8 GB/s at 64 KB, 34.7 GB/s at 1 MB — about 20 % under Apple M-series at the L1-resident sizes and within noise at the L2/L3-resident sizes.


Reproduce locally

# Throughput + tail percentiles (the single-worker headline numbers above)
pixi run --environment bench bench-install-wrk2        # one-time build (pinned wrk2 commit)
pixi run --environment bench bench-vs-baseline-quick   # flare vs go_nethttp on throughput, fastest path
pixi run --environment bench bench-vs-baseline         # full sweep: all baselines x configs the script defaults to

# Mixed-keepalive (80% keep-alive, 20% close) — flare vs go_nethttp on the mixed_keepalive config
pixi run --environment bench bench-mixed-keepalive

# Ad-hoc tail percentile probe (no integrity check, no 5-run gate)
pixi run --environment bench bench-tail-quick          # wrk2 --latency at fixed rate

# TLS bench setup (self-signed cert under build/tls-bench-certs/)
pixi run --environment bench bench-tls-setup

The TLS bench configs tls_plaintext.yaml (steady-state TLS throughput, connections kept open) and tls_handshake.yaml (handshake-per-request, Connection: close) are wired into the harness and drive a TLS-terminating flare server on 127.0.0.1:8443. These configs use the blocking handshake_fd(fd) path; a non-blocking reactor-state-machine TLS handshake that ties them into the cancel-aware reactor loop is gated on a Mojo improvement.

Results land under benchmark/results/<timestamp>-<host>-<commit>/.


Continued v0.9.0 work - no regression

The continued v0.9.0 work (client ergonomics, DNS cache, server 0-RTT data path, migration path-validation) is additive and does not touch the measured hot paths in the default configuration (GrpcClient already ships -- unary plus streaming client):

  • The new client-side modules (RedirectPolicy/cookie/retry wiring, GrpcClient, DnsCache) are additive. The HTTP/1.1, h2, and h2c server serve loops are unchanged; the new client features are opt-in (builders / kwargs that default to prior behavior).
  • Server 0-RTT is gated on max_early_data_size > 0 (default 0). With 0-RTT disabled the per-packet path is byte-identical to before: no extra FFI call, only a single UInt32 budget compare before the (skipped) early-key install block.
  • Migration adds a server-initiated PATH_CHALLENGE plus an anti-amplification counter, and these fire only when a 1-RTT packet arrives from a new peer address (a migration event), never on the steady-state single-path request path.
  • Client transport reuse + request streaming is fully opt-in. HTTPS keep-alive pooling only engages after with_pool(); with pooling off the TLS h1 path is byte-identical to before (one _addr == 0 compare, then the prior full-handshake + read-to-EOF path). The shared generic framed reader compiles to the same code the cleartext pool already used. send_chunked is a separate explicit method (no change to send / get / post); it holds one chunk in flight, so a multi-MB upload stays bounded-memory rather than materializing the body.
  • gRPC streaming + async DNS is additive. The unary HttpClient h2/h2c fast path (take_response) is untouched; streaming opens its own long-lived stream and only uses the new additive Http2ClientConnection accessors (send_request_open / drain_body / stream_ended / response_headers), so existing unary and h2-driver suites stay green. resolve_async offloads getaddrinfo to a pool thread (same mechanism as block_in_pool); the sync resolve / DnsCache.resolve hot path is unchanged, and a within-TTL cache hit spawns no thread.
  • 0-RTT replay hardening adds no steady-state cost. The server cross-connection strike set is consulted only on a connection's first 0-RTT packet (gated by not early_guard.any_seen), and 0-RTT is off by default (max_early_data_size == 0), so the 1-RTT request path -- the only path the benchmarks exercise -- runs zero extra work (no Dict touch, no clock read). The strike Dict[String, UInt64] stays empty unless 0-RTT is enabled; its prune is amortized and only fires at capacity. The client fetch_0rtt gate is a pure is_idempotent_method predicate plus two existing accessor reads on the non-eligible path, which falls straight through to the unchanged unary fetch; the eligible emit path is the 0-RTT EarlyData note below.
  • Migration strict egress hold adds no steady-state cost. The per-slot egress drain (_drain_and_send) gains one _drain_migration_probe call whose body returns immediately when the slot has no stashed path-validation frames -- the universal case when no migration is in flight -- so the coalesced 1-RTT response/ACK drain is byte-identical to before. Path frames are no longer mixed into the coalesced datagram; they only ever exist mid-migration. The new per-datagram 3x byte accounting and candidate routing run solely on the migration path (a probing bool check otherwise), and the benchmarks never migrate, so throughput/tail are unaffected. The hold trades one validation round trip of latency on a genuine migration for the guarantee that no 1-RTT egress reflects to an unvalidated address.
  • Client 0-RTT EarlyData emission adds no steady-state cost. The new _build_0rtt / send_stream_early / finish_early_data run only on a resumed connection opened with enable_0rtt=True whose ticket installed early keys (early_data_ready()); a fresh connection -- the only kind the benchmarks open -- never enters the early path, so the 1-RTT send_stream / _build_1rtt / fetch hot path is byte-identical (those functions are untouched). 0-RTT and 1-RTT share the application packet-number space (tx_1rtt_pn), so 1-RTT egress after the handshake needs no special handling. On a server reject, finish_early_data replays the recorded flight once at 1-RTT (a one-time cost on the saved round trip), not on any steady-state path.

Functional parity is covered by the unchanged QUIC/H3 suites (test-quic-loopback-integration, test-quic-post-initial-decrypt, test-quic-post-initial-egress, test-quic-handle-packet, test-quic-resumption, test-quic-migration, test-h3-end-to-end), which stay green after these changes. Run bench-vs-baseline / bench-h3* on a quiet host to confirm throughput/tail parity with the headline numbers above before a release.