# Adaptive Runtime on bare metal: scaling, autoscaling and stability (2026-10-12)

URL: https://scalws.com/docs/benchmarks/2026-10-12-xeon-adaptive-runtime/
Source: `docs/benchmarks/2026-10-12-xeon-adaptive-runtime.md` in the ScalWS repository

Repeats the measurements of `2026-10-07-adaptive-runtime.md` (development VM) on the
bare-metal host of the release-mode runs, with the same scripts (`bench/instances.sh`,
`scripts/controller-d5.sh`; CPU sets via `UPSTREAM_CPU` and `LOAD_CPUS`).

Host: bare metal, 2× Xeon E5-2650 v2 (Ivy Bridge EP, 8 cores per socket), 121 GB, Ubuntu 26.04, kernel 7.0, `tcp_migrate_req=1`, performance governor, turbo off.
Topology separate from the release-mode comparisons: instance *i* on CPU *i* (one event loop each), upstream and PHP-FPM (4 workers) on CPU 3,
oha on the other socket (CPUs 8-15, 24-31).

scalwsd: the PGO + BOLT build of commit a7dc639 with the default HTTP/1 paths (the
`final-hyper` binary of run 7); scalws-controller built from the same tree.

## D1: N instances (reuseport) vs. one instance with the same CPUs

`bench/instances.sh`, 3 runs × 10 s after a 3 s warm-up, medians; *eff* = rps(N) / (rps(1) × N);
"vs 1×N loops" = N instances against one instance with N event loops on the same CPUs.

| Scenario (conns) | 1 inst | 2 inst | eff | vs 1×2 loops | 3 inst | eff | vs 1×3 loops |
|---|---|---|---|---|---|---|---|
| respond (64) | 50.1k | 100.2k | 1.00 | 1.03× | 151.0k | 1.00 | 1.03× |
| respond (256) | 49.7k | 99.5k | 1.00 | 1.03× | 149.7k | 1.00 | 1.03× |
| static-small (64) | 38.0k | 75.3k | 0.99 | 1.05× | 112.4k | 0.99 | 1.04× |
| static-small (256) | 37.8k | 74.3k | 0.98 | 1.04× | 111.4k | 0.98 | 1.04× |
| static-1m (64) | 5.25k | 8.82k | 0.84 | 1.00× | 10.81k | 0.69 | 1.00× |
| cache-hit (64) | 40.8k | 80.8k | 0.99 | 1.05× | 121.7k | 1.00 | 1.05× |
| TLS+H2 (256) | 30.5k | 62.0k | 1.02 | 1.03× | 94.9k | 1.04 | 1.02× |
| proxy (256) | 21.6k | 42.6k | 0.99 | 1.04× | 63.6k | 0.98 | 1.03× |
| php (64) | 11.8k | 11.8k | 0.50 | 1.00× | 11.8k | 0.33 | 1.00× |

* Web-tier paths scale linearly on bare metal (efficiency 0.98–1.04) and N instances are
  1.02–1.05× one instance with N event loops on the same CPUs: multi-instance costs
  nothing here, and gains a little.
* `static-1m` is bandwidth-bound (loopback copy): 10.8k req/s with three instances or one
  instance with three loops alike.
* PHP is bound by PHP-FPM on one CPU: 11.8k req/s with one, two or three instances
  (efficiency 0.50 and 0.33): the case the classifier must not answer with more scalwsd
  instances.
* RSS (sum of the instances): 30–54 MiB for one instance (34 MiB on the static paths),
  86–112 MiB for three; one instance with three loops: 33–62 MiB.
* Zero non-2xx responses at every point.

## D5: autoscaling under load (web-tier mode, min 1, max 3)

Concurrency sweep, 20 s per point, `scripts/controller-d5.sh`:

| Path | conns | req/s | p99 ms | errors | instances | RSS MiB |
|---|---|---|---|---|---|---|
| respond | 8 | 49.6k | 0.17 | 0 | 1 | 32 |
| respond | 32 | 50.7k | 0.66 | 0 | 2 | 62 |
| respond | 64 | 99.5k | 0.75 | 0 | 3 | 95 |
| respond | 128 | 149.4k | 0.98 | 0 | 3 | 97 |
| respond | 256 | 149.2k | 2.00 | 0 | 3 | 102 |
| respond | 512 | 147.4k | 5.01 | 0 | 3 | 108 |
| respond | 1024 | 145.3k | 10.14 | 0 | 3 | 121 |
| static-small | 8 | 83.0k | 0.12 | 0 | 3 | 95 |
| static-small | 32 | 111.8k | 0.35 | 0 | 3 | 95 |
| static-small | 64 | 112.0k | 0.74 | 0 | 3 | 96 |
| static-small | 128 | 111.5k | 1.38 | 0 | 3 | 96 |
| static-small | 256 | 110.9k | 2.62 | 0 | 3 | 105 |
| static-small | 512 | 110.6k | 5.29 | 0 | 3 | 105 |
| static-small | 1024 | 108.9k | 10.05 | 0 | 3 | 121 |

The static-small sweep followed the respond sweep without an idle pause, so three
instances were still active for it.

Bursts from idle to 1024 connections for 5 s (three in a row, 8 s apart): 47.0k → 95.6k →
144.5k req/s while the controller grew 1 → 3 → 3 instances; p99 24 → 18 → 10 ms; zero
errors. The instance counts are those at the end of each burst: the second burst started
on one instance and ended on three. The pool scaled back to one instance after the sweep.

The other D5 checks passed as on the VM: HTTP/2 with 8 × 32 streams across a scale-in from
three instances to one lost no processed stream (224 requests the client had not sent yet
were cancelled by it after GOAWAY); four slow 1 MiB downloads across a scale-in completed;
a reload while scaling to three left all instances on the new generation with zero
errors; at `max_instances` the controller logged the reason instead of acting; the memory
guard refused a new instance; 7 autoscaler actions, no restarts.

Compared with a fixed single instance on one CPU (~50k req/s on respond, D1), the
autoscaled pool reached ~3× the throughput once three instances were active, on three
times the CPUs, as D1 predicts.

Results: `bench-results/xeon-r7-instances` (`results.csv`) and `xeon-r7-d5.csv` on the
benchmark host.
