Benchmarks

2026-10-09 — Final comparison: scalws vs nginx 1.24 / 1.30.5 / 1.31.6

Overnight work (2026-10-08/09): Stage 1 (upstream path), Stage 2 (upstream telemetry), the open items, then this comparison. Host and method as in 2026-10-08-nginx-versions-real-apps.md: 8 vCPU EPYC 7642 VM, server on CPUs 0-3, applications/upstreams/MariaDB on 4-5, oha on 6-7, every scenario on a freshly started server (FRESH_SERVER=1), unit order rotating.

  • scalws — commit bfab683 (security fixes, per-request savings, upstream telemetry), release, --features http3
  • nginx — 1.24.0 (Ubuntu package), 1.30.5 stable and 1.31.6 mainline (signed nginx.org sources, -O2)

Raw results: results/2026-10-09-final-vs-nginx/ (3 × 30 s, all scenarios) and results/2026-10-09-final-apps8/ (8 × 10 s, the application scenarios whose run-to-run spread is 5–15 %).

Requests/s (median)

Scenarioscalwsnginx 1.24nginx 1.30.5nginx 1.31.6vs 1.24vs 1.30.5vs 1.31.6
static-small97 59699 363101 401102 3960.98×0.96×0.95×
static-1m8 8949 3629 3419 3810.95×0.95×0.95×
respond104 303102 140102 39597 9631.02×1.02×1.06×
proxy80 39892 62294 39698 9350.87×0.85×0.81×
cache-hit102 443100 59398 105101 3461.02×1.04×1.01×
TLS + HTTP/2 static61 79964 37963 84063 6510.96×0.97×0.97×
PHP-FPM27 46225 26924 36126 2171.09×1.13×1.05×
WordPress373637361.03×1.00×1.03×
Laravel3853713923571.04×0.98×1.08×
Node.js (8 runs)26 80726 02025 61626 2631.03×1.05×1.02×
Python uvicorn (8 runs)11 31811 18911 02211 1771.01×1.03×1.01×
Express HTML (8 runs)3 6973 8293 7023 3730.97×1.00×1.10×
Express JSON (8 runs)3 4413 6563 4313 5500.94×1.00×0.97×
Django (8 runs)1 9111 9311 9291 9200.99×0.99×1.00×

Differences below about 3 % (5–10 % for Express) are within the run-to-run spread.

p99 latency, server CPU per request, memory

Scenariop99 ms scalws / nginx 1.24–1.31.6CPU µs/req scalws / nginxRSS MiB scalws / nginx
static-small1.9 / 6.9–9.434.5 / 29.0–31.546 / 77–78
static-1m17.2 / 14.9–15.9298.7 / 308–31745 / 77–78
respond9.4 / 10.2–10.521.3 / 21.8–23.245 / 76–78
proxy1.68 / 1.47–1.5249.0 / 36.1–38.552 / 78–79
cache-hit9.2 / 3.3–6.826.0 / 34.2–35.046 / 78–79
TLS + HTTP/22.5 / 10.4–10.543.0 / 43.7–44.253 / 89–90
PHP-FPM3.4 / 4.3–4.482.8 / 92.5–93.248 / 78
WordPress2 285 / 2 479–2 980747 / 701–74052 / 79–80
Laravel459 / 423–491330 / 496–50161 / 79–80
Express JSON33.6 / 30.1–32.4123 / 90–9850 / 78–79
Django58.8 / 49.9–57.4176 / 128–13850 / 78–79

Summary

  • Ahead or level: PHP-FPM (+5–13 %), respond, cache hits, Node.js, Python, WordPress, Laravel, Django, and TLS + HTTP/2 latency (p99 2.5 ms vs 10.5 ms). PHP, Laravel, cache hits and 1 MiB files cost less server CPU per request than with nginx.
  • Behind: the bare reverse proxy (0.81–0.87×) and, by 2–5 %, small and 1 MiB static files. Newer nginx versions are not faster than 1.24 except on the bare proxy.
  • Memory: scalws 45–61 MiB vs nginx 76–90 MiB in every scenario.

What the night changed and what is left

Stage 1 — upstream path. Allocation work, measured per request with uprobes on the allocator: static file 40 → 21 allocations (derived headers memoised per thread and validated against each fstat), respond 16 → 13, proxy 34 → 30; CPU per request static 39.8 → 37.2 µs, respond 25.4 → 23.2 µs (A/B in results/2026-10-09-alloc-ab/). An apparent Express regression in that A/B was noise: 10 alternating runs show none (results/2026-10-09-express-ab10/). Two upstream correctness bugs found by new framing tests were fixed (Transfer-Encoding + Content-Length responses). The own HTTP/1 client is kept out of the build (docs/experimental/own-h1-client.patch): it framed messages itself.

Stage 2 — upstream telemetry (ADR-0043): connections opened/reused, retries, cancellations, wait and time-to-first-byte per application and protocol (HTTP/1 and FastCGI), at no measurable cost (results/2026-10-09-telemetry-ab/). First readings: proxy 32 connections for 20 000 requests; PHP-FPM 4 connections for 5 000 requests, 0.49 ms average queue wait at 16 concurrent requests on 4 workers.

Open items, resolved as one cause. The higher CPU per request of the low-rate proxied applications (Express, Django, ~3.5k req/s) is not wake-ups: at a fixed 3 500 req/s scalws has fewer context switches per request than nginx (1.94 vs 2.27) and still about 13 µs more CPU (112 vs 99 µs). That is the same constant user-space cost as the proxy gap at saturation (17 µs per request in scalwsd vs 5.8 µs in nginx; kernel time is equal), spread over hyper’s HTTP/1 server and client machinery, tokio scheduling and the pipeline. hyper’s header-read timer stays (Slowloris protection). Closing the gap means a leaner HTTP/1 path than hyper’s — the own client was the attempt, deliberately not built.