ScalWS

Linux · Rust · Adaptive Runtime

The adaptive
web server.

A Linux web and application server in Rust that adds web-tier capacity only when the web tier is the bottleneck.

Explore the architecture →
ADAPTIVE RUNTIMEInteractive explainer

Adaptive Runtime: how ScalWS decides when to add instances

Throughput
143.2k req/s
p99
14 ms
Class
WebTierSaturated
Action
None (at max 3)

class=WebTierSaturated action=None reason="pressure 0.79; at max_instances 3"

Three instances on three CPUs: 143.2k req/s, p99 14 ms, zero errors. At max_instances the controller logs the reason instead of acting.

Measured: D5 burst, idle to 1024 connections, zero errors. Pressure bars and values inside log lines are illustrative.

1.37×

nginx 1.31.6 · TLS + HTTP/2 small file

1.93×

nginx 1.31.6 · response cache hits

0.92×

nginx 1.31.6 · fixed in-memory response

Bare metal, release mode
test context and all results →

Measured against nginx and OpenLiteSpeed, losses included.

1.37× nginx 1.31.6 in the tested TLS + HTTP/2 small-file workload and 1.93× on cache hits. Behind: Fixed response 0.92×, Reverse proxy 0.98×, Python (uvicorn) 0.93×. Same bare-metal host, same pinned cores for every server.

Host
Bare metal, 2× Xeon E5-2650 v2; server pinned to 4 physical cores, load generator on the other socket; turbo off
Method
oha with 64 connections; 10 s warm-up, 30 s measurement, 5 runs, medians
Build
PGO + BOLT release build with the experimental own HTTP/1 server and upstream client (opt-in, not the default)
Peers
nginx 1.31.6, nginx 1.28.3, OpenLiteSpeed 1.9.3
Memory
Peak server RSS 36–41 MiB for ScalWS, 79–98 MiB for nginx 1.31.6, 38–45 MiB for OpenLiteSpeed (4 scenarios, medians)
Full results and methodology →
  • Small static file1.13×
  • 1 MiB static file1.00×
  • Fixed response0.92×
  • Reverse proxy0.98×
  • Cache hit1.93×
  • TLS + HTTP/2 small file1.37×
  • PHP (PHP-FPM)1.86×
  • Node.js1.00×
  • Python (uvicorn)0.93×
ScalWS requests/s divided by nginx 1.31.6 requests/s, release-mode run of 2026-10-11 on bare metal. Right of the line: ScalWS faster. Log scale. Earlier VM results are superseded.

Cloning the server is not scaling.

“Every N requests, start another instance” is wrong whenever the application behind the server is the limit. The ScalWS controller first decides which tier is saturated, and only a saturated web tier may act.

WebTierSaturated

PressureScore above 0.75, driven by CPU together with event-loop lag. No application at its CPU allotment.

ScaleOut within min, max, memory and cooldown limits.

PhpSaturated · NodeSaturated · PythonSaturated

The application uses at least 90 % of its CPU allotment. Even if the web tier is busy too, more instances would not raise throughput.

None, plus a logged recommendation for the operator.

MemoryPressure · ConnectionPressure · InsufficientEvidence

Low free memory, many requests in flight with idle CPUs, or too few samples.

None, with the reason in the log.

Measured in front of PHP-FPM on one CPU: 13.3k, 15.4k and 15.8k req/s with 1, 2 and 3 instances. Efficiency 0.58 and 0.40. ADR-0034

The controller is never in the request path.

Instances share each listener through the kernel’s SO_REUSEPORT group and terminate TLS themselves. scalws-controller watches their metrics and runs their lifecycle. If it stops, instances keep serving.

Control plane and data plane →

control plane

scalws-controller observe → classify → decide → lifecycle

data plane

ClientsTCP :80 / :443
Linux kernelSO_REUSEPORT group
scalwsd × N each terminates TLS, serves HTTP, cache, static, proxy, FastCGI
ApplicationsPHP-FPM, upstreams (external endpoints)
No controller proxy hop in the normal HTTP data path. The kernel hands each new connection to one instance; the connection, its keep-alive requests and its HTTP/2 streams stay there.

Elasticity, not free efficiency.

Instances Throughput p99
1 on 1 CPU 37.2k req/s 46 ms
2 on 2 CPUs 81.5k req/s 19 ms
3 on 3 CPUs 143.2k req/s 14 ms

Bursts from idle to 1024 connections, three in a row, zero errors, autoscaling in web-tier mode with one CPU per instance. Each step adds a CPU. With the same CPUs, one larger instance does about as well: 0.92× to 1.14× across web-tier scenarios. Multi-instance buys the ability to add and remove capacity, not more work per CPU.

Scaling data →

Traffic only reaches ready instances.

  1. Starting

    Spawned with an id, a CPU set and a configuration generation. Bound, not listening.

  2. Warming

    Configuration prepared, runtimes and caches initialised.

  3. Ready

    Readiness passed. Still no traffic.

  4. Active

    Joins the SO_REUSEPORT group. New connections arrive.

  5. Draining

    Listeners close first, queued connections migrate, in-flight requests finish.

Drain testing found and fixed four defects that also affected a single instance on every SIGTERM. Lifecycle, generations and recovery →

What works today.

Serving

Applications and tenancy

Operations

Experimental and opt-in at build time: HTTP/3 and eBPF accounting. Experimental and opt-in in the configuration: the own HTTP/1 server and upstream client (the default stays hyper). The multi-node control plane is designed (ADR-0021), not built. .htaccess support is a subset, not Apache compatibility.

PHP, Node.js and Python as first-class runtimes.

PHP

Managed PHP-FPM pools over FastCGI, front controller and PATH_INFO, or an external FastCGI endpoint.

1.86× nginx 1.31.6, same PHP-FPM pool. Varies run to run; under investigation.

Node.js

Supervised processes, one socket each, round-robin over ready workers. Or any upstream.

1.00× nginx 1.31.6, application-bound. OpenLiteSpeed is ahead (0.93×).

Python

ASGI and WSGI through uvicorn, hypercorn or gunicorn, with virtualenvs.

0.93× nginx 1.31.6, same uvicorn server, application-bound.

Supervised with readiness, crash backoff, restart-loop detection and graceful drain. Under the Adaptive Runtime controller, application tiers must be external endpoints: managed pools are refused so instances do not each start their own.

Install it, check it, run it.

Signed apt repository for Ubuntu 24.04+ and Debian 13+ (amd64). scalwsctl check runs the exact prepare path a reload would. Building from source is in the README.

Getting started →
curl -fsSL https://scalws.com/apt/scalws-archive-keyring.gpg \
  -o /usr/share/keyrings/scalws-archive-keyring.gpg
echo 'deb [signed-by=/usr/share/keyrings/scalws-archive-keyring.gpg] https://scalws.com/apt stable main' \
  > /etc/apt/sources.list.d/scalws.list
apt update && apt install scalws
scalwsctl check && systemctl enable --now scalwsd

Read the evidence, then the code paths.