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.

Get started View benchmarks

Signed apt repository · 1 build · v1.0.0

1.29×

nginx 1.31.6 · TLS + HTTP/2 small file

1.54×

nginx 1.31.6 · response cache hits

0.86×

nginx 1.31.6 · fixed in-memory response, ScalWS behind

Bare metal, release mode
test context and all results →
ADAPTIVE RUNTIME Interactive explainerExplore the architecture →

Adaptive Runtime: how ScalWS decides when to add instances

Throughput
144.5k req/s
p99
10 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: 144.5k req/s, p99 10 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.

Measured against nginx and OpenLiteSpeed, losses included.

1.29× nginx 1.31.6 in the tested TLS + HTTP/2 small-file workload and 1.54× on cache hits. Behind: Small static file 0.98×, 1 MiB static file 0.99×, Fixed response 0.86×, Reverse proxy 0.83×, Node.js 0.98×, Python (uvicorn) 0.97×. Same bare-metal host, same pinned cores for every server. Opt-in own HTTP/1 server and client (http1_server: experimental, upstream_client: experimental): 1.27× on TLS + HTTP/2, 1.70× on cache hits, 0.93× on fixed responses.

  • Small static file0.98×
  • 1 MiB static file0.99×
  • Fixed response0.86×
  • Reverse proxy0.83×
  • Cache hit1.54×
  • TLS + HTTP/2 small file1.29×
  • PHP (PHP-FPM)1.06×
  • Node.js0.98×
  • Python (uvicorn)0.97×
ScalWS requests/s divided by nginx 1.31.6 requests/s, release-mode run of 2026-11-12 on bare metal. Right of the line: ScalWS faster. Log scale.
Host
Bare metal, 2× Xeon E5-2650 v2; server on CPUs 0-5 of one socket: 3 ScalWS instances of 2 CPUs under scalws-controller, sandbox on; load generator on the other socket; turbo off
Method
oha with 64 connections; 10 s warm-up, 30 s measurement, 5 runs with rotating order; medians
Build
The published 1.0.0 package as installed, with its defaults (the hyper-based HTTP/1 server and upstream client); the opt-in own HTTP/1 server and upstream client (http1_server: experimental, upstream_client: experimental) are a separate, secondary column
Peers
nginx 1.31.6, nginx 1.28.3, OpenLiteSpeed 1.9.3
Memory
Peak server RSS 109–120 MiB for ScalWS, 103–131 MiB for nginx 1.31.6, 55–70 MiB for OpenLiteSpeed (range over the scenarios)

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: 11.8k, 11.8k and 11.8k req/s with 1, 2 and 3 instances. Efficiency 0.50 and 0.33. ADR-0034

The controller is never in the request path.

Instances accept from listening sockets the controller opens once and hands them (or from a kernel 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 kernelshared accept queue
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 instance on 1 CPU 47.0k req/s 24 ms p99
3 instances on 3 CPUs 95.6k req/s 18 ms p99
3 instances on 3 CPUs 144.5k req/s 10 ms p99

Bursts from idle to 1024 connections, three in a row, zero errors, autoscaling in web-tier mode with one CPU per instance. Instances are counted at the end of each burst; each instance adds a CPU. With the same CPUs, one larger instance does about as well: 1.00× to 1.05× 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

    Starts accepting. New connections arrive.

  5. Draining

    Stops accepting first; the other instances keep the queue. 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

In the packages and opt-in in the configuration: HTTP/3 (http3: true on a listener). Experimental and opt-in at build time: 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.06× 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.

0.98× nginx 1.31.6, application-bound. OpenLiteSpeed is ahead (8.08k against 7.97k req/s).

Python

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

0.97× 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, dnf, zypper and apk repositories for the RHEL family, Fedora, Debian, Ubuntu, openSUSE, SLES and Alpine (x86_64). scalwsctl check runs the exact prepare path a reload would. Building from source is in the README.

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

Read the evidence, then the code paths.