Benchmarks

Experimental upstream client vs hyper (2026-10-09)

ADR-0045. One scalwsd build (commit d7dcbf5, --features http3), run with server.upstream_client: hyper (default) and experimental. Development VM: 4 server CPUs (0-3), upstreams on 4-5, load generator on 6-11. bench/multi.sh, MODE=quick (3 s warm-up, 10 s measurement), 64 connections, variants alternate in every run.

Round 1 — 5 runs (results/2026-10-09-own-client-ab)

Scenariohyper req/sexperimental req/sΔCPU µs/req hyper → exp.p99 ms
proxy (synthetic upstream)75 14283 653+11.3 %52.2 → 46.41.83 → 1.60
node (managed)27 04526 725−1.2 %68.0 → 62.85.12 → 4.64
python (ASGI, uvicorn)11 37510 665−6.2 % ¹104.2 → 100.910.5 → 12.5
Express3 6454 096+12.4 % ²121.2 → 110.732.3 → 30.7
Express API3 1033 401+9.6 %123.4 → 113.840.1 → 35.5
Django (gunicorn)1 8911 867−1.3 %179.5 → 164.459.8 → 60.3
Laravel (FastCGI — control, not affected)521511−1.9 %328 → 313—

¹ Re-measured, see round 2. ² CV 12–18 %.

Round 2 — 10 runs, python and Django (results/2026-10-09-own-client-py)

Scenariohyper req/sexperimental req/sΔCPU µs/req hyper → exp.
python (ASGI)11 24411 332+0.8 %103.8 → 92.3
Django1 9131 900−0.7 %177.3 → 158.3

The round-1 python difference was noise (CV 3–4 % with 5 runs; the Laravel control moved by 2 % as well). Syscalls per request are the same for both clients (strace, 20 000 requests: 2 reads + 2 writes per request each).

Conclusion

  • The experimental client lowers scalwsd’s CPU per proxied request by 7–11 % on every upstream measured, and raises synthetic proxy throughput by 11 %, where the upstream is not the bottleneck.
  • Real applications are bound by the application (Node, Python, PHP); their throughput is unchanged within noise, CPU left for them grows.
  • No real-application regression was measured; it stays opt-in (ADR-0045): message framing is our own code, and the hyper path is the conservative default. A long soak (roadmap: 6–12 h real-app tests) is the next gate before it could become the default.

Remaining work inside hyper’s path

  • Response cache on proxied routes. The benchmark’s proxy route passes through the response cache (nginx’s location /api/ has none). Excluding it with bypass_paths: [/api/x] changes nothing measurable: 75 106 vs 74 141 req/s, 52.2 vs 52.6 µs CPU per request (5 runs, results/2026-10-09-proxy-cache-bypass). Bypass rules are now checked before the cache key is built (19126c2), so bypassed requests no longer pay for it.
  • Allocations. A proxied request makes 21 allocations (32 on 2026-10-08). Removing the few left on the request path is worth about 1–2 % — below what ADR-0038 accepts as a measurable gain, so not pursued.

On hyper’s path the proxy is at diminishing returns; the experimental client is the measured lever (+11 %).