Internals: benchmark reports
Release-mode comparison with LiteSpeed Web Server Enterprise (run 7): 2026-10-12
Note (2026-11-13): a newer comparison exists,
2026-11-12-xeon-1.0.0.md(the 1.0.0 package, current nginx and OpenLiteSpeed). This report is kept as the record of its date.
Machine and pinning as in 2026-10-10-xeon-bare-metal-pgo.md: 2× Xeon E5-2650 v2, server
on four physical cores (CPUs 0-3), upstream and applications on 4-7, oha on socket 1;
performance governor, turbo off, BIOS power profile at maximum performance (see
2026-10-11-xeon-release-nginx-openlitespeed.md). MODE=release: 10 s warm-up, 30 s measurement,
5 runs, medians. scalws: the PGO + BOLT build of tools/pgo-build.sh (ADR-0047, commit
a7dc639), which adds the Python allocator settings, the fixed-response shortcut and the
controller’s cgroup subgroup to run 6; final with both experimental settings,
final-hyper with defaults.
LiteSpeed Web Server Enterprise 6.3.7 (official release tarball, trial licence) replaces
OpenLiteSpeed as the third server, with bench/configs/litespeed (the OpenLiteSpeed
settings in its XML form). It has no fixed-response directive, so respond is not
measured for it. Every measurement of every server answered 2xx only. No run was excluded.
The clock log (socket-0 cores every 5 s) shows 2 593–2 600 MHz under load; the samples
below 2.4 GHz fall almost all into the 1 h 45 min the harness stood still (see below), when
the cores were idle.
| scenario | scalws final | final-hyper | nginx 1.28 | nginx 1.31 | LiteSpeed 6.3.7 | final / nginx 1.31 | final / LSWS |
|---|---|---|---|---|---|---|---|
| static-small | 108 441 | 106 858 | 96 551 | 99 224 | 110 346 | 1.09 | 0.98 |
| static-1m | 11 581 | 11 584 | 11 642 | 11 676 | 8 932 | 0.99 | 1.30 |
| respond | 204 022 | 199 561 | 201 219 | 209 330 | n/a | 0.97 | n/a |
| proxy | 98 888 | 82 883 | 80 011 | 92 496 | 29 975 | 1.07 | 3.30 |
| cache-hit | 160 071 | 159 507 | 84 669 | 85 819 | 48 280 | 1.87 | 3.32 |
| tls-h2-static-small | 96 148 | 94 751 | 66 811 | 68 954 | 69 862 | 1.39 | 1.38 |
| php | 42 569 | 41 811 | 36 217 | 36 897 | 18 689 | 1.15 | 2.28 |
| node | 10 823 | 10 932 | 10 749 | 10 824 | 11 618 | 1.00 | 0.93 |
| python | 6 366 | 6 360 | 6 465 | 6 540 | 5 859 | 0.97 | 1.09 |
Server CPU per request (µs, cpu_us): static-small 36.88 / nginx 1.31 40.30 / LSWS 17.64; static-1m 345.38 / 342.56 / 223.62; respond 19.60 / 19.10 / n/a; proxy 40.34 / 43.16 / 66.42; cache-hit 24.98 / 46.59 / 41.24; tls-h2-static-small 41.30 / 57.88 / 28.57; php 62.00 / 78.40 / 106.31; node 65.69 / 66.38 / 91.79; python 84.89 / 87.38 / 117.93.
Versions: nginx 1.28.3 (Ubuntu 26.04 package) and 1.31.6 (built from the signed source
tarball); load: oha with 64 connections.
p99 latency of every server (ms, median of 5 runs, p99_ms):
| scenario | scalws final | final-hyper | nginx 1.28 | nginx 1.31 | LiteSpeed |
|---|---|---|---|---|---|
| static-small | 0.69 | 0.79 | 0.74 | 0.71 | 1.68 |
| static-1m | 6.84 | 8.18 | 6.41 | 6.66 | 19.04 |
| respond | 0.40 | 0.42 | 0.41 | 0.40 | n/a |
| proxy | 0.91 | 1.14 | 1.01 | 0.95 | 4.13 |
| cache-hit | 0.50 | 0.51 | 0.88 | 0.83 | 2.99 |
| tls-h2-static-small | 1.37 | 1.43 | 1.14 | 1.09 | 1.80 |
| php | 1.77 | 1.79 | 2.05 | 2.02 | 5.83 |
| node | 11.49 | 11.37 | 11.53 | 11.45 | 10.69 |
| python | 13.78 | 14.48 | 15.51 | 15.61 | 12.79 |
Server CPU per request of every server (µs, median of 5 runs, cpu_us_per_req):
| scenario | scalws final | final-hyper | nginx 1.28 | nginx 1.31 | LiteSpeed |
|---|---|---|---|---|---|
| static-small | 36.88 | 37.42 | 41.42 | 40.30 | 17.64 |
| static-1m | 345.38 | 345.27 | 343.56 | 342.56 | 223.62 |
| respond | 19.60 | 20.04 | 19.87 | 19.10 | n/a |
| proxy | 40.34 | 48.22 | 49.96 | 43.16 | 66.42 |
| cache-hit | 24.98 | 25.07 | 47.23 | 46.59 | 41.24 |
| tls-h2-static-small | 41.30 | 41.93 | 59.72 | 57.88 | 28.57 |
| php | 62.00 | 64.20 | 80.31 | 78.40 | 106.31 |
| node | 65.69 | 76.39 | 74.04 | 66.38 | 91.79 |
| python | 84.89 | 99.39 | 95.97 | 87.38 | 117.93 |
Peak resident memory of the server processes (MiB, median of 5 runs, peak_rss_mib):
| scenario | scalws final | final-hyper | nginx 1.28 | nginx 1.31 | LiteSpeed |
|---|---|---|---|---|---|
| static-small | 40 | 42 | 80 | 80 | 71 |
| static-1m | 40 | 40 | 80 | 80 | 71 |
| respond | 42 | 42 | 80 | 80 | n/a |
| proxy | 43 | 44 | 84 | 83 | 72 |
| cache-hit | 41 | 43 | 81 | 80 | 72 |
| tls-h2-static-small | 53 | 49 | 99 | 98 | 73 |
| php | 44 | 46 | 81 | 81 | 72 |
| node | 42 | 42 | 83 | 84 | 71 |
| python | 42 | 44 | 84 | 84 | 72 |
p99 latency (ms), scalws final / nginx 1.31 / LSWS: static-small 0.69 / 0.71 / 1.68; proxy 0.91 / 0.95 / 4.13; cache-hit 0.50 / 0.83 / 2.99; tls-h2-static-small 1.37 / 1.09 / 1.80; php 1.77 / 2.02 / 5.83; node 11.49 / 11.45 / 10.69; python 13.78 / 15.61 / 12.79; respond 0.40 / 0.40 / n/a.
LiteSpeed Enterprise: what to keep in mind
- Two workers. The trial licence starts two
lshttpdworkers whateverworkerProcessessays (pgrep -c -f 'lshttpd - #'during the runs: 2); nginx and scalws use all four server CPUs. On server-bound scenarios LSWS therefore had half the cores. Its CPU per request is lower on static files (17.6 vs 36.9 µs on static-small) and it still reached 110 346 req/s there, 2 % ahead of scalwsfinal; with the four workers of a larger licence it would likely lead static-small clearly. It was behind on everything that passes through an upstream or the cache (proxy 3.3×, cache-hit 3.3×, PHP 2.3×). - Cancelled requests. In every run
oharecorded “operation was canceled” against LSWS on static-small (1 707–1 871 of ~3.3 million requests, 0.05 %) and static-1m (7–25), and once “connection closed before message completed”; no other server had any error besides the requests still in flight at the end of the measurement (“aborted due to deadline”). Every response LSWS sent was 2xx. - PHP. LSWS’s PHP-FPM used half the application CPU of the others (45.9 s vs ~91 s per
run): with two workers and the same pool (four PHP-FPM processes,
maxConns 4) it did not keep the pool busy. - Node.js is the one runtime where LSWS is ahead (7 %). It sends fewer request headers
to the application: no
X-Forwarded-For,X-Forwarded-ProtoorX-Request-Id(2026-10-12-app-respond-analysis.mdmeasured 83.4k vs 77.0k instructions per request in Node.js for the two header sets). scalws keeps these headers. - Shutdown. LSWS ignores SIGTERM unless it has handled a SIGUSR2 first; once in run 7
the TERM came too early and the harness waited 1 h 45 min (16:05–17:50 local).
bench/run.shnow repeats both signals until the process is gone.
Compared with run 6
- Python:
final6 366 (run 6: 5 957), now 0.97× nginx 1.31 with less server CPU per request (84.9 vs 87.4 µs). The managed uvicorn workers get glibc allocator thresholds (MALLOC_MMAP_THRESHOLD_,MALLOC_TRIM_THRESHOLD_), which remove the mmap/munmap per request seen on bare metal; application CPU is equal (60 s per run, two saturated workers), so the remaining 3 % is on the application side. respond0.97× nginx 1.31 (run 6: 0.95×) with the fixed-response shortcut; the ranges of the five runs overlap (scalws 203 269–210 092, nginx 201 909–213 934).node1.00× nginx (run 6: 1.00×),php1.15× (1.06×),proxy1.07× (1.07×),tls-h2-static-small1.39× (1.37×).cache-hitfinal160 071 vs 173 288 in run 6 whilefinal-hyperis unchanged (159 507 vs 158 764). The only data-plane change on that path is the fixed-response check in routing (one call per request that returns nothing for a proxy route), whichfinal-hyperruns as well. Open: a second PGO build of the same commit would show whether this is profile-to-profile variation.static-smallfinal108 441 vs 111 956 (−3 %), within the run-to-run spread of earlier bare-metal runs.
Results, in the bench host’s results directory: results/xeon-r7 (aggregate.csv, aggregate.txt, run-1 …
run-5); clock log r7-mhz.log on the bench host.