Start here
Changelog
All notable changes are recorded here. Format: Keep a Changelog.
[Unreleased]
Added (2026-10-12) — apt repository
- Signed apt repository at https://scalws.com/apt (
stable main):apt install scalwson Ubuntu 24.04+ / Debian 13+ (docs/OPERATIONS.md, “Install”).
Added (2026-10-12) — Tenant resource commands
scalws-highcpu,scalws-highram,scalws-highio(scalwsctl high cpu|ram|io): the tenants using the most CPU, memory or disk I/O now, with their limits.scalws-limitcpu,scalws-limitram,scalws-limitio(scalwsctl limit cpu|ram|io): limit a tenant (by tenant name or Unix user) at once, until scalwsd restarts;--persistalso writes it toscalws.yaml(comments kept) and reloads;--clearremoves it. Under scalws-controller every instance gets the limit, also instances started later.- Console: a Resources page lists tenants by CPU, RAM or disk I/O with their limits;
operators set or remove run-time limits there (audited as
tenant.limits). - Admin API:
POST /v1/tenants/{tenant}/limits,GET /v1/tenants/usage;GET /v1/tenantsshows each tenant’s user. - Configuration:
resources.io_bandwidth(io.maxon the disks of the tenant’s application roots);io_weightreturns to the default 100 when removed.
Security (2026-10-12) — fixes from security audit run-6 (audits/run-6/REPORT.md)
- Console: a draft in which a YAML anchor, alias or merge key copies a redacted placeholder to a field that is not secret is refused (after the save the view showed the secret).
.htaccess:DirectoryIndexkeeps at most 16 names (each is looked up for every directory request); the directory cache weighs the parsed items, not only the source..htaccess: the<Files *>deny for cut access rules is added after the file’s own<Files>blocks, so an earlier grant no longer overrides it..htaccess: every regular expression run costs 4 KiB of the request’s budget on top of its subject, and<FilesMatch>patterns are charged too.- PHP: parked output expires five minutes after parking began, also while the script is still running.
- Tenant limits: instances started later (scale-up, restart) receive the run-time limits from the controller instead of resetting the shared cgroup to the configuration; the controller refuses invalid limits with 400.
- Workers: the port owner check fails closed when
/proc/net/tcpcannot be read and refuses a connection accepted by another user.
Security (2026-10-12) — fixes from security audit run-5 (audits/run-5/REPORT.md)
- Console: draft validation messages quote no source lines (a parser’s cropped snippet showed fragments of a secret); secrets are also replaced in their escaped form.
.htaccess:<Files>access rules inside an unsupported container, open at the directive limit or never closed now deny every name; a file cut at 64 KiB or replaced by the parse-budget stand-in denies below child directories that grant access too.- PHP: an expired parked response returns its parking budget, not only its disk blocks (a stalled HTTP/2 stream could hold the shared budget indefinitely).
.htaccess: the directory cache weighs a parsed file’s source and warnings (warnings are cut at 240 bytes); a request’s regular expressions may scan at most 8 MiB (500 past that).- Console: sign-in, sign-out and re-authentication are audited at most 20 times a minute per account (failed sign-ins per client network), the rest counted in one notice; the sign-in throttle keys IPv6 clients per /64; at most 16 sessions per account.
- Workers: the per-connection owner check judges the listening socket (the kernel lists a connection not yet accepted with uid 0).
ErrorDocument: the path of the document’s URI (no query or fragment) is what is checked against the deny rules and routed.
Security (2026-10-11) — fixes from security audit run-4 (audits/run-4/REPORT.md)
- Console: secrets are replaced by
(secret)in draft validation errors, warnings and--checkoutput (parser messages quoted the restored secret’s line; high). .htaccess: access rules inside<IfModule>for a module scalws does not model, past the 1000-directive limit or in the part of a file beyond 64 KiB deny instead of being dropped..htaccess: the directory cache is weighed by path length too (long missing paths could keep ~560 MiB per application); nearly every pattern is weighed as 4 KiB; an application may spend at most 100 ms a second parsing files (past that, uncached directories deny until the next second).- Deny rules: segment-prefix matching is linear in the path (8 KiB paths cost ~20 ms of CPU each with the WordPress profile).
- A local
ErrorDocumentthat the deny rules block is not served. - Cache: entries are bound to the application that produced them (a host moved to another tenant by a reload got the previous owner’s cached responses).
- PHP: parked output for slow clients is limited to 1 GiB per pool and 4 GiB in total, and expires after 5 minutes unread.
- Workers: every new connection to a TCP worker’s port must reach a socket of the worker’s user (a port released by a descendant listener could be taken by another account).
- Console: connections that have not presented a valid session are closed after 30 s; request bodies must arrive within 10 s; the audit view pages back up to 64 MiB; at most 30 new drafts per user a minute.
Security (2026-10-11) — fixes from security audit run-3 (audits/run-3/REPORT.md)
- Console: secrets nested in a changed section no longer appear in the draft validation change list (a Developer could read every redacted secret; high).
- PHP: a client that stops reading (e.g. an HTTP/2 stream whose window never opens) no
longer holds a PHP-FPM driver: output it does not take is parked in an unlinked
temporary file, as nginx
fastcgi_bufferingdoes (one connection could take a tenant’s PHP site down; high). - Console: at most 16 connections per client address, request-head and idle deadlines; throttled re-authentication is not audited one by one; audit search survives a window starting inside a UTF-8 character; stats are scraped only from an instance’s own listener; at most 8 open drafts per user.
- Controller: readiness is read only from the instance’s own
127.0.0.1listener (a listener on127.0.0.2/[::1]with the same port made the controller kill every instance). - FastCGI client (vendored): buffered records are emitted before reading more (output no
longer piles up in memory); a connection that ends before
END_REQUESTis an error. .htaccess: access rules that cannot be evaluated (HTTP authentication,<RequireAll>,<Limit>, …) deny instead of being ignored; tenant regular expressions are budgeted (32 KiB each, at most 8 up to 256 KiB per file) and the parsed-file cache is weighed.- Cache: purged entries are not spilled back to the disk tier; bypass rules match the normalised client path after a rewrite.
- Deny rules cover PATH_INFO, trailing slashes and directory indexes
(
/wp-content/uploads/*.phpno longer bypassed withshell.php/or anindex.php). - Runtimes: a worker leaves rotation as soon as its process exits.
- Own HTTP/1 server (experimental): a pending response body is dropped when the client leaves; tiny request-body frames are copied out of the read buffer.
- PHP: client
X-Forwarded-*and client-address headers are stripped like the proxy does; FastCGI stderr is rate-limited; a managed FPM socket must belong to the tenant user. - Static files:
openat2support is decided once per document root, not on a request’sEPERM. - Console install: the generated administrator password is never printed (apt records the installer’s terminal); the installer says how to read it from the credentials file.
Added (2026-10-08) — Per-domain request counters (ADR-0042)
scalws_http_host_requests_total{tenant,app,host,status},..._response_bytes_total,..._request_bytes_total,..._cache_total{result},..._requests_in_flight: per configured domain pattern, counted by index from the host router’s entry, sharded per event loop (no lock, no label hashing, no allocation per request). Unknown hosts and domains pastmetrics.max_hosts(default 1000) count as_other. On by default (metrics.hosts); A/B gate: no throughput or CPU-per-request regression (docs/benchmarks/2026-10-08-domain-metrics.md).- Console: Domains tab on Traffic and “Top domains” on Overview (requests/s, trend, status shares, bytes in/out, cache hit ratio, in flight, totals).
scalws-h3-probe --stall-ms/--on-stallandscripts/h2-stall-dump.sh: the soak runs an independent HTTP/2 client and snapshots sockets, controller and instance state while a request is pending > 2 s (forensics for the rc1 soak’s unexplained HTTP/2 timeouts).- Soak memory-pressure probe fixed (it probed before memory was low);
FAULTS_ONLY. scripts/console-systemd-e2e.sh: console ->systemctl restart-> controller with the packaged units’ sandboxing; 12/12. Fixed: an apply re-sent after it completed (draft gone) returned 404 instead of the same operation.bench/run.shrefuses to measure when the server it started has exited (a leftover process holding the port was measured once).
Added (2026-10-08) — ScalWS Console: live traffic, every setting editable, redesign
- Traffic page and overview charts: requests/s, latency p50/p95/p99, 4xx/5xx, bandwidth, connections, TLS handshakes per site (with its domains), listener, instance and method, from the instances’ metrics (2 s samples, 15 min kept, server-sent events).
- Configuration rebuilt on JSON Schemas of the config structs (
scalws-configfeatureschema,scalws-controller config-schema): Server, Listeners & TLS, Tuning, Security, Cache, Logging & metrics, Sites, PHP / FastCGI, Proxy & applications, Advanced and Adaptive Runtime (controller.yaml) pages with every field pre-filled, defaults, units, inline validation, set/changed markers, reset and undo, search; review lists changed settings before the text diff; controller.yaml applies by controller restart with automatic restore.scalws-controller check-config. - New design system (tokens, rail navigation, metric strip, charts with table views, status
shapes, dark and light), screenshots in
docs/console-screenshots/.
Added (2026-10-07) — ScalWS Console (web administration, port 8090)
scalws-console(cmd/scalws-console,docs/console-*.md): HTTPS web administration shipped with ScalWS (same packages, own unitscalws-console.service, enabled by the installer, which creates useradminwith a random password and prints it once). Separate process; talks toscalws-controllerover its local socket only.- Overview, Adaptive Runtime (decision and pressure contributors, distribution, instances, desired count, autoscaling mode), Listeners (socket generations, QUIC demux), Logs, Audit log, About (versions, capability matrix, accounts).
- Configuration transactions: drafts (raw YAML editor and structured listeners / sites /
limits editor), validation (
scalws-config+scalwsd --check), redacted diff and impact, apply as a new generation with health check and automatic restore of the previous file, history with restore, conflict detection (base hash), idempotent operations. - Security: argon2id credentials file (re-read on change, refused when readable by others),
server-side sessions,
__Host-cookie, CSRF token + same-origin, login throttling, re-authentication for apply/rollback/purge, strict CSP, roles administrator / operator / developer / viewer checked server-side, secrets redacted in every view and in the append-only audit log.
Added (2026-10-07) — remaining gaps: listener generations, HTTP/3 with several instances
- Listener generations (ADR-0037,
docs/listener-generations.md):reloadadds and removes ports, changes bind addresses, adds HTTPS, IPv6 or HTTP/3 without restarting the controller. New addresses are bound before the generation is published (a failed bind rejects the reload, nothing changes); a socket is closed when no generation uses it. A restarted controller takes the sockets back from running instances (one queue per address survives). A generation whose instances do not start is rolled back while the previous one serves. - HTTP/3 with several instances (ADR-0039 … 0041,
docs/http3-multi-instance.md): the controller’s QUIC demux (scalws-controller quic-lb) routes datagrams by routable, encrypted connection IDs; new connections are placed on instances that accept them; replies leave directly on the shared UDP socket. Scale-out/in, rebinding, controller restart and demux crash keep established connections.scalws-controller quic-lb-status. - Connection distribution (ADR-0038,
docs/connection-distribution.md): per-instance work, pressure imbalance and aharmfulflag instatus; logged, not acted on. scalws-h3-probe(bench/h3probe): HTTP/3 load and correctness client (pinned test certificate, rebinding, truncation/reset/refused counts).scripts/controller-h3.sh,scripts/controller-listeners.sh,scripts/controller-soak.sh,bench/h3-instances.sh.
Fixed (2026-10-07, found by the soak and the new tests)
- HTTP/3 shutdown closed every connection at once (also with one instance); connections
now drain: GOAWAY, in-flight requests finish, the server waits for the client to close
(response bytes in flight are not discarded), then
H3_NO_ERROR. scalwsd --worker-threadswithoutevent_loopsin the file made every reload fail (“requires a restart”): the event-loop count was applied at start only. Under the controller this meant limit shares never reached running instances.- Per-IP limits were part of the restart-only topology: changing them (or the instance’s share) rejected the whole reload. They now change on reload; only switching per-IP limiting on/off or its table size needs a restart. Reload errors name what changed.
- An instance could fail to receive its sockets (EPIPE): it wrote its id in two writes while the controller answered after the first.
Added (2026-10-07) — Adaptive Runtime limitations addressed
- Controller
listen: inherited(new default): the controller opens the listeners once and hands them to its instances (SCM_RIGHTS,scalwsd --listen-socket); every instance accepts from one queue. Scale-in no longer needsnet.ipv4.tcp_migrate_req; no failed request over 120 rolling reloads under connection churn (reuseport: 3); with a new connection per request +8…17 % req/s and lower p99; equal with keep-alive.listen: reuseportkeeps the previous behaviour. - WebSocket tunnels end with a close frame
1001 Going Awayat a frame boundary when the server drains (scale-in, restart, SIGTERM), after the HTTP connections; clients reconnect. Test:scripts/controller-ws-drain.sh(dependency-free echo server/client inbench/apps/ws).
Fixed (2026-10-07)
- Per-core event loops dropped their runtime as soon as the HTTP drain finished, killing tasks that the next shutdown step still needed (WebSocket tunnels were cut without a close frame in about half of the scale-ins). Loops now run until the server stops them.
Changed (2026-10-07) — renamed to Scale Web Server (ScalWS), https://scalws.com
| Before | Now |
|---|---|
scb-webd | scalwsd (web server daemon) |
scbctl | scalwsctl (CLI) |
scb-controller | scalws-controller (ScalWS Adaptive Runtime controller) |
crates scb-* | scalws-* |
package scb-web | scalws |
/etc/scb, /run/scb, /var/lib/scb, user/group scb | /etc/scalws, /run/scalws, /var/lib/scalws, scalws |
metrics scb_* | scalws_* |
environment SCB_* | SCALWS_* |
response header server: scb | server: scalws |
The on-disk response-cache format keeps its magic (SCBC1); existing disk caches stay
valid. Records written before the rename (pre-Adaptive-Runtime baseline and backup,
dated benchmark reports) keep the old names: they describe artefacts that carry them.
Added (Adaptive Runtime, 2026-10-07)
scalws-controllerand the ScalWS Adaptive Runtime (ADR-0033…0036): severalscalwsdinstances on SO_REUSEPORT listeners, lifecycle and generations, observe-only and web-tier autoscaling, limit sharing, cache purge fan-out;docs/adaptive-runtime.md.- Drain: responses after the shutdown signal carry
Connection: close; fresh and idle connections get a grace period; accept loops are tracked by the drain.
Added (performance and .htaccess, 2026-10-06)
- Per-core event loops (
server.event_loops, ADR-0029): one current-thread runtime per CPU sharing the listening socket; per-loop upstream and FastCGI connection pools. - Open file cache for static files (
server.open_file_cache, ADR-0028). - Kernel TLS for HTTPS/1.1 (
server.ktls, opt-in, ADR-0030): large files withsendfileover TLS. .htaccesssupport (applications[].htaccess: true, ADR-0031): mod_rewrite, mod_alias, access rules, ErrorDocument, DirectoryIndex, Header, Expires; new cratescalws-htaccess.- mimalloc as the global allocator of scalwsd (feature
mimalloc, default on).
Changed
- Connections are served by hyper’s HTTP/1 or HTTP/2 builder directly (protocol from ALPN or a peek at the first bytes) instead of hyper-util’s auto-detection; TLS without ALPN is HTTP/1.1 (RFC 9113 §3.2).
- Response cache: hit-for-pass markers (10 s) for responses that cannot be stored, sharded miss-coalescing map, pre-resolved lookup counters; request metrics resolved once per label combination.
- Accept loops take a connection before waiting for a connection slot (fair with several sockets).
Added (tenant users and zero-copy files, 2026-10-06)
tenants[].user(ADR-0026): the tenant’s PHP-FPM/Node/Python processes run as that account (setpriv; no capabilities, no new privileges), own socket directory2770; needs CAP_SETUID, CAP_SETGID, CAP_CHOWN and CAP_KILL, checked at activation.- Large static files on plain-TCP HTTP/1 are sent with
sendfile(2)(ADR-0027,server.sendfile, default on): 1 MiB files ~1.8× faster.
Fixed
- Proxied responses kept the upstream’s HTTP version (an HTTP/1.0 upstream answered
HTTP/1.1 clients with
HTTP/1.0); the client connection now decides.
Added (hardening and serving, 2026-10-06)
- Application processes die with scalwsd even when it is SIGKILLed outside systemd
(
setpriv --pdeathsig TERM); processes left in cgroup leaves by a previous instance are killed at start (cgroup.kill). - Metric series of tenants/applications removed by a reload are dropped (after the old generation drained).
scalws_tls_certificate_expiry_seconds{listener,certificate}for configured certificates; warning 14 days before expiry, error once expired.- Static
precompressed: true:.br/.gzsiblings byAccept-Encodingq-values, withVary: Accept-Encoding; siblings older than the file are ignored. - Response cache: concurrent misses for one URL are coalesced (one upstream request; others wait up to 5 s and are answered from the cache).
security.rate_limits: [{path, requests_per_second, burst}]per client address (429 +Retry-After,scalws_security_blocked_total{reason="route_rate"}).
Added (eBPF network accounting, 2026-10-06)
- Optional
scalws-ebpf(featureebpf, ADR-0025): cgroup_skb programs count network bytes and packets per application cgroup;server.ebpf.network: truepublishesscalws_app_network_bytes/scalws_app_network_packets. Load failures are warnings only.
Added (cache disk tier, 2026-10-06)
server.cache.disk(ADR-0024): disk tier for evicted and oversized entries, bounded size with LRU, asynchronous writes, persistent across restarts, purge/invalidation on both tiers; metricsscalws_cache_disk_entries,scalws_cache_disk_bytes.
Added (Python process pool, 2026-10-06)
- Python
process_pool: true: scalws runsworkers.min..maxsingle-worker server processes (uvicorn/hypercorn/gunicorn with one worker each) as a scalable pool — live scaling by the optimizer orscalwsctl scale, zero-dropscalwsctl restart.
Added (ARM64 packages, 2026-10-06)
ARCH=arm64 scripts/package.sh [--verify]: aarch64 cross-compiled DEB/RPM, verified in arm64 Debian 12 and Fedora 41 containers (QEMU).
Added (AI advisor over HTTPS, 2026-10-06)
server.ai.endpointmay behttps://(system roots plus optionalai.ca_bundle); non-loopback endpoints still needallow_remote: true.
Added (remote administration, 2026-10-06)
server.admin.remote(ADR-0023): admin API over TCP with mandatory client certificates (mutual TLS, TLS 1.3); mutations audited with the certificate subject;scalwsctl --remote host:port --ca … --client-cert … --client-key ….
Added (trusted proxies, 2026-10-06)
server.trusted_proxies(ADR-0022): client address fromX-Forwarded-Forof trusted peers only (right-most untrusted entry), forwarded chain extended upstream; PROXY protocol v1/v2 per listener (proxy_protocol: true), untrusted peers refused; per-source limits, access logs andREMOTE_ADDRuse the real client.
Changed (proxy performance, 2026-10-06)
- Reverse proxy drives upstream HTTP/1.1 connections inside the request task with its
own LIFO pool instead of hyper-util’s per-connection tasks: +28 % rps, −15 % CPU per
request (0.51× → 0.66× nginx on the 13-byte proxy benchmark).
docs/benchmarks/2026-10-06-proxy-profile.md; diagnosticscripts/proxy-compare.sh.
Added (M10 multi-node design, 2026-10-06)
- ADR-0021: multi-node control plane design (pull-based controller, signed configuration bundles, staged rollout and rollback). Design only; no implementation.
Added (M10 container runtime, 2026-10-06)
runtime: {type: container}(ADR-0020, cratescalws-runtime-container): containers run bypodman run/docker runas supervised workers in a scalable pool; bridge (loopback publish) or host networking;--security-opt no-new-privileges,--init, optional read-only root, dropped capabilities, user, validated bind mounts, pull policy; with podman and cgroups, containers live under the application’s tenant cgroup.- Supervisor
cleanuphook (argv run after every worker exit): no container outlives its worker. - Linux test image: podman + catatonit; end-to-end container test.
Added (M10 HTTP/3 evaluation, 2026-10-06)
- Experimental HTTP/3 over QUIC behind the cargo feature
http3(ADR-0019; quinn, h3):listener.http3: true, same pipeline as HTTP/1.1/2, reloadable/ACME certificates, global and per-source connection limits, GOAWAY on shutdown,Alt-Svcon TCP. - Bench:
h3-static-smallscenario (scalws vs Caddy), HTTP/3 in the soak test;docs/benchmarks/2026-10-06-http3.md.
Fixed
- HTTP/3: completed request tasks were kept per connection until it closed (found by the benchmark before release: 1.1 GiB after 10 s).
Added (M10 ACME, 2026-10-05)
- ACME certificate automation (ADR-0018,
instant-acme): HTTP-01, one certificate per application (acme: true), served on listeners withtls: {acme: true}, renewal through the configuration controller (no restart), per-application backoff, state inserver.acme.state_dir(0600 keys),GET /v1/certs,scalwsctl certs, metricsscalws_acme_orders_total,scalws_acme_certificate_expiry_seconds. scripts/acme-e2e.sh: issuance, reload-triggered renewal and restart against Pebble.- Durations accept days (
30d). - systemd unit:
StateDirectory=scalws(/var/lib/scalws writable under ProtectSystem=strict).
Added (acceptance gate 6, 2026-10-05)
bench/soak.sh: sustained mixed load over every serving path with RSS/fd/thread sampling and a pass/fail verdict;bench/overload.sh: quota overload with refusal, memory, isolation and recovery checks. Both pass (30-minute soak: 43 M requests, flat memory). Bench upstream:/api/delay(50 ms).
Fixed
- Wording of the “running as root” warning.
Added (M9 operations, 2026-10-05)
- Admin API on a Unix socket (
server.admin.socket, default<runtime_dir>/admin.sock, mode 0660) with audited mutations: reload, rollback, application restart, scale, cache purge, explain requests; views: status, apps, tenants, config history and diff, diagnose, optimizer, explain (ADR-0017). - Configuration controller with history of the last 10 configurations, rollback, line diff, and zero-drop per-application restart.
- Read-only dashboard at
/dashboardon the metrics listener. scalwsctl: status, apps, tenants, top, reload, rollback, history, diff, restart, scale, purge, backup, restore, completion; global--config,--socket,--address.- DEB/RPM packaging (
packaging/nfpm.yaml,scripts/package.sh [--verify]),scalwssystem user, package default configuration, shell completions;docs/OPERATIONS.md.
Changed
- Cache purge and explain requests moved from the metrics listener to the admin socket;
the metrics listener is read-only (
POST→ 403). workers.autoscaleaccepts the YAML 1.1 booleanoff/falseasoff.- systemd unit:
RuntimeDirectoryMode=0750so groupscalwsreaches the admin socket.
Added (M8 local AI advisor, 2026-10-05)
scalws-ai(ADR-0016): redaction of all model input, size-capped input preparation, advice JSON schema with strict validation, OpenAI-compatible HTTP provider.- Advisor service:
server.ai(disabled by default; loopback endpoint unlessallow_remote;api_key_env;timeout,max_queue,min_interval,max_tokens), automatic explanations on new warning/critical findings,GET/POST /explain,scalwsctl explain [--refresh], metricsscalws_ai_requests_total,scalws_ai_queue_depth.
Added (M7 diagnostics, 2026-10-05)
scalws-diag(ADR-0015): deterministic findings with severity, evidence, suggestion and correlated resource causes.- Evidence collection per window: upstream failures by reason, quota rejections, runtime state and restarts, cgroup v2 memory/limit, OOM kills and pressure stall information.
- Recent failed/slow requests per application (
server.diagnostics.slow_request,recent_requests);GET /diagnose[?app=tenant/app];scalwsctl diagnose. - Metrics
scalws_diagnostic_findings_total,scalws_http_response_transfer_seconds,scalws_tls_handshake_seconds; log targetscalws::diag.
Changed
- Responses refused by tenant quotas no longer count as application requests in the optimizer telemetry.
Added (M6 deterministic optimizer, 2026-10-05)
scalws-policy(ADR-0014): pure rule engine (scale_up/scale_down with bands, streaks, cooldowns, bounds and memory precondition; outcome observation with rollback and rule suppression; memory-pressure and error-rate advisories).- Per-application telemetry (requests, 5xx, latency percentiles, in-flight, cache hit ratio, tenant memory pressure) and the optimizer task.
- Live worker scaling of managed Node.js pools (
workers.autoscale: auto); Python recommendations; PHP advisories. - Audit:
scalws::auditlog target,scalws_optimizer_decisions_total,GET /optimizerjournal on the admin listener,scalwsctl recommendations. - Configuration:
server.optimizer {enabled, interval, journal_size},workers.target_concurrency.
Changed
workers.autoscaledefaults torecommend(handoff §8: recommend-only by default).
Added (M5 cache and security, 2026-10-05)
scalws-cache(ADR-0012): opt-in bounded memory response cache (moka) with explicit freshness only,Varyvariants, profile/config bypass rules, stale-while-revalidate with single-flight refresh, invalidation on unsafe requests, purge by URL, prefix, tag or all (POST /cache/purgeon the loopback admin listener),X-Cache/Ageheaders.scalws-security(ADR-0013): profile + configured deny paths; per-source connection limit at accept and request rate (IPv4 / IPv6 /64) with bounded sharded state.limits.write_timeoutfor slow readers;strict_snilisteners answer 421.- Metrics
scalws_cache_lookups_total,scalws_cache_entries,scalws_cache_bytes,scalws_security_blocked_total; rejection reasonsource_connection_limit. - Property-based fuzz tests for path/host normalisation, ranges, Cache-Control, CGI headers, units and configuration parsing.
- Benchmark scenario
cache-hit.
Added (M4 application profiles, 2026-10-05)
scalws-profiles(ADR-0011): profiles as YAML data inprofiles/(php, wordpress, woocommerce, laravel, node, nextjs, python, django, flask, fastapi) with weighted detection rules, runtime templates, health checks, static paths, cache-bypass and security rules.- Detection engine with evidence and confidence; bounded, symlink-free scanning; fact extraction (Python application object, Django WSGI module, virtualenv, npm command).
scalwsctl detect <dir> [--json](read-only proposal) andscalwsctl profiles.profile:application key: supplies the runtime when omitted (static profiles) and health checks;runtimeis optional when a profile is set.
Added (M3 tenancy, 2026-10-05)
scalws-cgroups(ADR-0010): cgroup v2 hierarchy under scalws’s delegated group (orserver.cgroups.path), tenant limitscpu,memory(+swap, default 0),pids,io_weight; workers jointenants/<tenant>/<app>before their first instruction; limits rewritten on reload; removed groups pruned; modesauto/required/off.- Per-tenant request quotas:
max_concurrent_requests(503) andrequests_per_secondburst(429), metricscalws_tenant_limited_total.
- Accounting metrics
scalws_tenant_{cpu_seconds,memory_bytes,pids,oom_kills},scalws_app_{cpu_seconds,memory_bytes,pids},scalws_cgroups_enabled. - systemd unit:
Delegate=yes,KillMode=control-group,RuntimeDirectory=scalws. - Linux test runs use a privileged container with a delegated cgroup root; OOM isolation and live limit change are tested end to end.
Added (M2 runtime platform, 2026-10-05)
- Process supervisor (ADR-0008): argv without shell, cleared environment, own process
group, readiness probes (socket or HTTP path), exponential backoff, restart-loop
detection (
Failed), graceful stop (SIGTERM/SIGQUIT then SIGKILL), rate-limited and sanitised stdout/stderr capture. scalws-runtime-node: managed Node.js processes (N workers, round-robin over ready ones) or external upstreams.scalws-runtime-python: ASGI (uvicorn, hypercorn, gunicorn + UvicornWorker) and WSGI (gunicorn) launch templates, virtualenvs, explicit commands with{socket}/{port}.scalws-runtime-php(ADR-0009): managed PHP-FPM pools with generated configuration or external FastCGI endpoints; PHP/static/front-controller routing withPATH_INFO; CGI response parsing; httpoxy protection;.phpnever served as static content.- Reverse proxy: WebSocket tunnelling with a server-wide limit and idle timeout; HTTPS upstreams (system roots + optional CA file).
- Reloads reuse unchanged runtime instances; new ones start before the swap, retired ones drain in the background.
- Metrics
scalws_runtime_ready_workers,scalws_runtime_restarts_total. - Integration tests for supervision and acceptance tests with real PHP-FPM, Node.js,
uvicorn and gunicorn; Linux test image
docker/test.Dockerfile; CI installs runtimes. - Benchmarks: PHP, Node and Python scenarios for scalws, nginx and Caddy.
Changed (2026-10-05, PHP performance)
- Persistent FastCGI connections (
FCGI_KEEP_CONN) to managed PHP-FPM pools, bounded bymax_children, with stale-connection detection and replay of small requests; 2 s routing plan cache. PHP throughput 0.70× → 0.93–1.0× nginx, CPU per request −34 %. - PHP request bodies ≤ 64 KiB are buffered (replayable); larger ones stream.
Changed (M2)
server.runtime_dir,limits.max_upgraded_connections,limits.upgraded_idle_timeoutconfiguration keys;proxyruntime gainswebsocketandtls_ca_file.- PHP request planning uses the same non-blocking cached lookup as static files.
Changed (2026-10-05, static file performance)
- Static files: Linux lookups via
openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)from a root directory fd (closes the symlink TOCTOU window); non-blocking hot path withRESOLVE_CACHED+preadv2(RWF_NOWAIT); small files read in the lookup call; large files streamed with 256 KiB positional reads. 3.4× (13 B) and 4.6× (1 MiB) throughput. - Absolute symlinks inside a document root are now refused on Linux (ADR-0006).
scripts/profile.sh: perf-based profiling of any benchmark scenario.
Added (M0 + M1 vertical slice, 2026-10-05)
- Workspace, ADRs 0001–0007, architecture and threat-model documents.
scalws-config: YAML schema withdeny_unknown_fields, size/duration/CPU units, upstream parsing, semantic validation that reports every error with its document path.scalws-router: host routing (exact + wildcard) and segment-aligned path routing on normalised paths.scalws-tls: PEM loading, key/cert match and SAN coverage checks, SNI resolver with atomic replacement on reload.scalws-static: confined static files (traversal, encoded-slash, dotfile and symlink protections), ETag/Last-Modified, conditional and single-range requests.scalws-proxy: HTTP/1.1 reverse proxy over TCP and Unix sockets with hop-by-hop stripping, forwarding headers, timeouts, failure classification.scalws-http: accept loop with connection cap, TLS handshake timeout, HTTP/1.1 + HTTP/2 limits, idle-connection closing, graceful drain.scalws-server: request pipeline, prepare/activate/reload,/metricsand/healthz.scalwsddaemon (SIGHUP reload, SIGTERM drain) andscalwsctl check.- Integration and security test suites; Docker-based benchmark harness with nginx and Caddy comparison configs; CI workflow; cargo-deny policy; systemd unit.