Internals: benchmark reports
ScalWS 1.0.0 on bare metal: the installed package with three warm instances (2026-11-12)
The first measurement of a release as it is installed: the 1.0.0 .deb on the Xeon host,
scalws-controller running three scalwsd instances of two CPUs each, started before the
measurement (no autoscaling), with the service sandbox on (what a new install gets). nginx
and OpenLiteSpeed run on the same six CPUs. A second part measures scaling of the
instances themselves against rc.1-11.
Comparison with nginx and OpenLiteSpeed
Host: 2x Xeon E5-2650 v2 (no AVX2), 121 GB, Ubuntu 26.04, kernel 7.0.0-38, performance
governor, turbo off, no VM or container running. Server on CPUs 0-5 (socket 0), the
upstream, PHP-FPM, Node.js and uvicorn on 6, 7, 22, 23, oha 1.16.0 on 8-15 and 24-31
(socket 1), 64 connections. bench/multi.sh MODE=release: 10 s warm-up, 30 s
measurement, 5 runs with rotating order; medians. Every measurement of every server
answered 2xx only; no run was excluded.
- ScalWS:
scalws_1.0.0-1_amd64.deb(the published package) under its systemd unit (SERVERS=scalws-serviceofbench/run.sh):instances: {min: 3, max: 3, cpus_per_instance: 2, cpus: "0-5"},sandbox: on, the bench sites ofbench/configs/scalws.yamlwith the applications as the external processes the other servers use (the controller requires external endpoints). “own HTTP/1” addshttp1_server: experimentalandupstream_client: experimental(ADR-0046, ADR-0045); “defaults” is the package as installed. - nginx 1.31.6 (built from the signed source tarball; every ratio is against it) and
1.28.3 (Ubuntu package),
worker_processes 6; OpenLiteSpeed 1.9.3, 6 workers. - LiteSpeed Web Server Enterprise is not in this run: its trial licence has run out.
- The “defaults” and the nginx and OpenLiteSpeed columns are one session (09:27 to 11:32 UTC); “own HTTP/1” was measured afterwards with the same harness (13:05 to 13:38 UTC). nginx 1.31 measured again in between (diagnosis below) was within 2 % of this table.
Requests per second:
| scenario | ScalWS own HTTP/1 | ScalWS defaults | nginx 1.28 | nginx 1.31 | OpenLiteSpeed 1.9.3 | own / nginx 1.31 | defaults / nginx 1.31 |
|---|---|---|---|---|---|---|---|
| static-small | 130 100 | 123 148 | 124 795 | 126 222 | 94 684 | 1.03 | 0.98 |
| static-1m | 12 202 | 12 176 | 12 352 | 12 360 | 11 772 | 0.99 | 0.99 |
| respond | 232 080 | 213 699 | 228 343 | 248 483 | n/a | 0.93 | 0.86 |
| proxy | 105 037 | 89 220 | 96 424 | 106 907 | 60 016 | 0.98 | 0.83 |
| cache-hit | 190 043 | 172 069 | 110 477 | 111 979 | 52 846 | 1.70 | 1.54 |
| tls-h2-static-small | 96 636 | 98 440 | 75 235 | 76 033 | 77 514 | 1.27 | 1.29 |
| php | 28 979 | 28 903 | 27 167 | 27 277 | 22 001 | 1.06 | 1.06 |
| node | 7 789 | 7 967 | 8 284 | 8 105 | 8 077 | 0.96 | 0.98 |
| python | 5 654 | 6 083 | 6 164 | 6 263 | 5 518 | 0.90 | 0.97 |
OpenLiteSpeed has no fixed-response directive: respond is not measured for it.
p99 latency (ms, median of 5 runs):
| scenario | ScalWS own HTTP/1 | ScalWS defaults | nginx 1.28 | nginx 1.31 | OpenLiteSpeed |
|---|---|---|---|---|---|
| static-small | 0.65 | 0.73 | 0.66 | 0.65 | 3.17 |
| static-1m | 7.92 | 7.92 | 7.11 | 7.48 | 24.01 |
| respond | 0.39 | 0.41 | 0.41 | 0.41 | n/a |
| proxy | 0.90 | 0.97 | 0.88 | 0.90 | 4.03 |
| cache-hit | 0.49 | 0.52 | 0.74 | 0.69 | 5.30 |
| tls-h2-static-small | 1.54 | 1.58 | 1.18 | 1.16 | 1.72 |
| php | 2.51 | 2.53 | 2.65 | 2.64 | 8.11 |
| node | 15.68 | 15.47 | 15.01 | 15.46 | 15.10 |
| python | 17.18 | 15.20 | 16.58 | 15.44 | 13.45 |
Server CPU per request (µs, median of 5 runs; ScalWS: the controller and its instances, the unit’s whole cgroup):
| scenario | ScalWS own HTTP/1 | ScalWS defaults | nginx 1.28 | nginx 1.31 | OpenLiteSpeed |
|---|---|---|---|---|---|
| static-small | 46.31 | 49.13 | 48.07 | 47.51 | 54.44 |
| static-1m | 494.70 | 495.10 | 485.65 | 485.32 | 434.34 |
| respond | 26.03 | 28.27 | 26.26 | 24.08 | n/a |
| proxy | 57.52 | 67.73 | 62.18 | 55.62 | 83.93 |
| cache-hit | 31.80 | 35.12 | 54.28 | 53.56 | 102.06 |
| tls-h2-static-small | 57.99 | 57.08 | 66.47 | 66.62 | 75.83 |
| php | 115.83 | 119.49 | 95.16 | 93.07 | 132.86 |
| node | 82.15 | 95.24 | 80.64 | 74.05 | 94.94 |
| python | 105.75 | 124.47 | 109.25 | 101.39 | 142.94 |
Peak RSS: ScalWS 109 to 120 MiB (controller and three instances together), nginx 103 to 131 MiB, OpenLiteSpeed 55 to 70 MiB.
Findings:
- ScalWS leads on the cached response path (1.54 to 1.70x nginx 1.31), HTTPS/2 (1.27 to 1.29x) and PHP (1.06x), with the lowest CPU per request on the first two; on PHP it uses more CPU per request than nginx (116 against 93 µs) while serving more requests per second.
- With the package defaults,
respond(0.86) andproxy(0.83) are behind nginx; with ScalWS’s own HTTP/1 server and client they are at 0.93 and 0.98, and static-small at 1.03. - With the own client, Python (0.90) and Node.js (0.96) are slower than with the default client (0.97, 0.98): to be understood before the own paths become the defaults (1.1, P10).
Where the default gap comes from
Same host and harness, SCENARIOS="respond proxy static-small", 3 rotating rounds of 10 s
- 30 s, medians (
results/diag-100,diag-100b):
| variant | respond | proxy | static-small |
|---|---|---|---|
| nginx 1.31 | 245 590 | 107 338 | 125 697 |
| scalwsd alone (6 workers, no controller, no sandbox) | 216 700 | 90 270 | 127 947 |
| controller, 1 instance x 6 CPUs, sandbox on | 211 600 | 87 781 | 121 424 |
| controller, 3 x 2, sandbox on | 214 276 | 89 261 | 122 108 |
| controller, 3 x 2, sandbox off | 212 165 | 88 731 | 124 782 |
| controller, 3 x 2, sandbox on, own HTTP/1 (second session) | 233 581 | 105 389 | 129 684 |
The controller, the number of instances and the sandbox move these by 1 to 3 %; scalwsd alone with the default paths has the same gap. The gap is the default HTTP/1 server and client (hyper’s); the own implementations close most of it.
Instances: three against one (bench/instances.sh)
Instance i on CPU i, one event loop each, PHP-FPM and the upstream on CPU 3, oha on
socket 1; 3 runs of 10 s warm-up + 20 s, medians; scalwsd 1.0.0 against rc.1-11 (the
1.0.0 binary of the package; rc.1-11 the published 1.0.0~rc.1-11 package). Zero non-2xx
answers.
| scenario (64 connections) | 3 instances, 1.0.0 | / rc.1-11 | 3 instances / 1 process with 3 loops |
|---|---|---|---|
| static-small | 84 231 | 1.39 | 1.03 |
| respond | 108 335 | 1.38 | 1.02 |
| cache-hit | 87 333 | 1.44 | 1.02 |
| tls-h2-static-small | 74 051 | 1.46 | 1.04 |
| proxy | 43 570 | 1.32 | 1.00 |
| static-1m | 10 429 | 1.05 | 1.01 |
| php | 11 194 | 1.00 | 1.00 |
- Three instances serve three times one instance (efficiency 0.98 to 1.04), and slightly more than one process with three event loops on the same CPUs.
- PHP is bound by the single CPU of PHP-FPM in this topology: equal for every count and build. On one instance 1.0.0 is 1.17x rc.1-11 on PHP.
- 1.0.0 needs 25 to 30 % less CPU per request than rc.1-11 on the web paths (the profile-guided build of ADR-0047 and the 1.0.0 fixes).
Raw results, in the bench host’s results directory: results/xeon-1.0.0-cmp, xeon-1.0.0-exp, xeon-inst-1.0.0,
xeon-inst-rc1-11. (scalwsd --version in the environment records shows an older build
first on the host’s PATH; the service runs the package’s /usr/bin/scalwsd 1.0.0.)