Benchmarks

2026-10-06 — Development server: release-mode comparison and request-path work

Host: VMware VM, AMD EPYC 7642, 8 vCPU, 31 GiB, Ubuntu 24.04, kernel 6.8 (no WSL or Docker). Server CPUs 0-1, applications 2-3, oha 4-7; 64 connections. Release mode (MODE=release bench/multi.sh): 10 s warm-up, 30 s measurement, 5 runs, medians; every process with ulimit -n 65535; open file cache with a check per hit and kTLS on both servers; nginx sends the same forwarding headers as scb.

Result (requests/s, median of 5)

scenario               scb      nginx    scb/nginx   CV% scb   scb cpu_us/req   nginx
static-small         73.7k     74.5k      0.99×       1.6        27.1            26.8
static-1m             7.9k      9.0k      0.88×       3.4       252.2           223.1
respond              97.4k    111.8k      0.87×       3.8        20.5            17.9
proxy                47.4k     52.8k      0.90×       2.2        42.1            37.9
cache-hit            82.4k     69.0k      1.19×       7.9        24.3            28.9
tls-h2-static-small  64.2k     44.6k      1.44×       3.4        31.1            44.8
tls-static-1m         1.57k     1.56k     1.00×       1.4      1272.8          1277.4
php                  29.7k     26.8k      1.11×       5.6        59.8            68.3
node                 21.8k     26.0k      0.84×       3.4        59.4            52.4
python               11.6k     12.2k      0.96×       3.8        92.3            80.7

Peak RSS: scb 33–42 MiB, nginx 42–50 MiB. After this run: MSG_MORE for the response head before sendfile (below) moved static-1m to ~0.91×.

Findings and changes (each verified by an independent A/B)

ChangeA/B result
Node “regression”none: same build pair 8 × 30 s, 29.9k vs 29.8k. Node is single-threaded and saturated; both builds deliver identical requests; upstream reuse 99.99 %, no pool starvation
FastCGI connection drivers (ADR-0032)PHP 17.2–20.4k → 23.6–26.9k; FPM workers busy 1.2–1.3 → 1.4–1.5 CPUs
Drivers on a dedicated runtimeworse in 4 alternating runs — reverted
Precomputed static file headers, shared cache keyuser CPU 9.3 → 7.6 µs/req, 67k → 78k
Proxy: no URI re-parse, single-pass header stripping, replay copy only after ≥1 s idleuser CPU 13.5 → 12.4 µs/req; allocations 47 → 36/req
Response head with MSG_MORE before sendfilesegments 23.3 → 21.7 per 1 MiB response (nginx 20.8); 7.7k → 8.2k, CPU 259 → 243 µs/req
Cache-hit flagged by compare.py (−9.9 % against the morning’s 3 short runs)not a regression: old vs new build 6 × 30 s, 86.4k vs 85.3k, CPU 23.1 vs 23.4 µs/req

Where the remaining gaps are:

  • respond and proxy: entirely user space (kernel time equal or lower than nginx’s; same syscalls and TCP segments per request). Proxy user CPU 12.4 vs ~8.7 µs.
  • static-1m: ~1 more TCP segment per response than nginx and ~6 µs more user time.
  • node: Node.js itself is the bottleneck; results vary by ±10 % run to run.