Benchmarks

2026-10-08 — Security fixes vs RC1 vs nginx 1.24

Host: development VM (8 vCPU EPYC 7642, Ubuntu 24.04, 31 GiB). Server on CPUs 0-3, upstreams and applications on 4-5, oha on 6-7. Raw results (every run’s oha JSON, summary.csv, environment.txt) are in results/2026-10-08-audit-vs-nginx124/.

Builds:

  • fixed — scalwsd at the end of the security-audit fixes (release, --features http3)
  • rc1 — scalwsd 1.0.0-rc.1 (before the fixes)
  • nginx — Ubuntu package 1.24.0, same CPUs, workers = CPUs, access log off

A console demo instance (scalwsd, one worker thread, idle) was running on the host for all variants alike.

Saturated throughput (MODE=release bench/multi.sh, 5 × 30 s, order rotating)

Scenariofixed req/src1 req/snginx req/sfixed / nginxfixed p99 msnginx p99 msfixed CPU µs/reqnginx CPU µs/reqRSS MiB fixed / nginx
static-small98 03597 842101 3170.97×4.289.5535.829.839 / 74
static-1m8 7848 8619 3230.94×14.5714.46320.8309.739 / 69
respond104 480102 955104 8261.00×9.2010.3122.222.539 / 69
proxy80 39081 93795 1670.84×1.601.4249.437.644 / 72
cache-hit103 561102 091101 4881.02×9.709.3226.632.944 / 72
TLS + HTTP/2 static64 20165 68264 2891.00×2.4110.2443.543.448 / 82
PHP-FPM31 97932 80429 4761.08×3.223.2784.783.649 / 82
Node.js21 38022 05326 4070.81× (see below)5.724.6371.667.052 / 82
Python (uvicorn)12 05111 93212 1330.99×9.658.3797.089.552 / 82

Cost of the security fixes (fixed vs rc1): within the run-to-run spread (CV 1–4 %) everywhere; proxy −1.9 %, static-1m −0.9 %, PHP −2.5 % (CV 9.5 %). Two avoidable costs were found by profiling and removed afterwards (see “Follow-up”): the cache stage copied the whole request header map for its variant, and the proxy wrapped bodyless request bodies.

The Node.js gap is a measurement artefact

The application process uses the same CPU with either server (30.4 s), yet nginx got 24 % more requests out of it — which pointed at the requests scalws sends. They are the same (captured byte-for-byte: same headers, keep-alive, 64 upstream connections, no connection churn: zero accept calls during load). The difference comes from the state of the long-lived Node.js process: in run.sh without FRESH_SERVER the managed Node.js process has been idle through six scenarios and the PHP scenario when it is measured. With a fresh server per scenario (FRESH_SERVER=1, raw results in results/2026-10-08-node-fresh-check/):

scalws req/snginx req/s
after the PHP scenario (as above)21 05727 696
fresh server30 13926 696

Comparisons from now on use FRESH_SERVER=1.

The proxy gap is real CPU per request

49.4 vs 37.6 µs per request. Profile (perf record -g, proxy load): 63 % kernel (TCP), 33 % scalwsd. In scalwsd the largest single item was CancellationToken::is_cancelled (1.2 %): every request cloned, checked and dropped the server’s shutdown token, each a mutex shared by all connections — plus the two copies above.

Follow-up

Fixes being measured: shutdown state read from a per-connection atomic flag, no header copy in the cache stage, no body wrapper for bodyless proxied requests. Then: nginx 1.30.5 (stable) and 1.31.6 (mainline) built from source, and real applications (WordPress, Laravel, Express, Django; bench/real/setup.sh).