Benchmarks

2026-10-05 — M2 runtime benchmarks (Docker Desktop smoke run)

Not a performance claim. Docker Desktop on Windows (WSL2 VM, Ryzen 7 7800X3D), host shared with ~25 other running containers. Run-to-run variation on this host is ±15–20 % (e.g. scb static-small measured 71.7k and 58.6k req/s in two runs of the same build); compare servers only within one run.

Command (bench/README.md):

docker run --rm --cpuset-cpus 0-11 --cap-add SYS_NICE -e DURATION=10 \
  -v scb-bench-www:/bench/www -v "$PWD/bench/results:/results" scb-bench
  • Server on CPUs 0-3 (4 workers/threads), application servers and the bench upstream on 4-5, oha 1.16.0 on 6-11; 64 connections; 3 s warm-up, 10 s measured.
  • Same application servers for every web server: PHP-FPM 8.2 (pm = static, 4 children, workers as nobody), Node.js 18 (one process), uvicorn 0.37 (2 workers), all on Unix sockets. scb starts and supervises its own copies; nginx 1.22 and Caddy 2.6 are put in front of identical externally started ones.
  • cpu_s counts only the web server’s own processes (nginx: master + workers; scb: scb-webd without the application processes it supervises).

Results

server  scenario                     rps   p50_ms   p95_ms   p99_ms   cpu_s  peak_rss_mib
scb     static-small               58607     0.97     2.16      3.5   29.15       13
scb     static-1m                   9501      6.4    11.72    16.59   35.09       14
scb     respond                    84335     0.58     1.88     3.25   21.14       14
scb     proxy                      42407     1.37     2.69     4.11   28.97       19
scb     tls-h2-static-small        45750     1.27     2.62      4.1   30.61       20
scb     php                        14933     3.76     7.55    10.53   21.91       21
scb     node                       23986     2.41     5.15     7.09   18.79       22
scb     python                     11919     5.18     9.55    11.88   15.02       22
nginx   static-small               98381     0.62     1.04     1.53   36.69       56
nginx   static-1m                  23076     2.27      6.9    10.19   35.48       56
nginx   respond                   241763     0.22     0.55      1.1   34.72       56
nginx   proxy                      62945     0.76     2.43     4.73   27.61       56
nginx   tls-h2-static-small        82288     0.65     1.96     2.98   36.24       70
nginx   php                        21224     2.85     4.28     5.98   19.33       71
nginx   node                       24004      2.5     4.95     6.54   12.67       71
nginx   python                     10972      5.4     9.66    12.89    9.16       70
caddy   static-small               31382     1.27     6.65    11.87    31.4       48
caddy   static-1m                  12819     3.33    15.29    23.81    30.2       48
caddy   respond                    52092     0.75     3.92     7.83   31.28       48
caddy   proxy                      23465     2.06     7.22    10.83   32.11       51
caddy   tls-h2-static-small        22652     2.18     7.35    11.72   33.25       53
caddy   php                        12868     4.65     7.57    10.12   30.19       58
caddy   node                       25267     2.07     6.07     9.11   29.86       58
caddy   python                     11712     5.08    10.13    15.55   21.37       58

All runs: 100 % success, 0 non-2xx responses.

Findings

  1. Node and Python are application-bound for all three servers: scb ≈ nginx ≈ Caddy (node 24.0k / 24.0k / 25.3k; python 11.9k / 11.0k / 11.7k). scb uses more CPU of its own than nginx for the same throughput (node 18.8 vs 12.7 CPU-s).
  2. PHP: scb 0.70× nginx (14.9k vs 21.2k), above Caddy (12.9k). At concurrency 1 and 4 scb’s latency equals nginx’s (0.38 ms vs 0.50 ms at c=1); the gap appears when 64 connections queue for 4 FPM workers. scb spends ~1.9× nginx’s CPU per PHP request; the profile shows no single hotspot. Recorded as an M2 follow-up (FastCGI connection reuse, params building).
  3. Static and proxy results reproduce 2026-10-05-static-profile.md within host noise.
  4. Harness bug found and fixed during this run: PHP-FPM workers run as nobody and could not be moved to the application CPUs without CAP_SYS_NICE, so in an earlier run they competed with scb for the server CPUs (PHP 12.2k). The harness now warns when re-pinning fails and the documented command adds --cap-add SYS_NICE.