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 scalws on 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; --persist also writes it to scalws.yaml (comments kept) and reloads; --clear removes 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/tenants shows each tenant’s user.
  • Configuration: resources.io_bandwidth (io.max on the disks of the tenant’s application roots); io_weight returns 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: DirectoryIndex keeps 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/tcp cannot 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 --check output (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 ErrorDocument that 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_buffering does (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.1 listener (a listener on 127.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_REQUEST is 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/*.php no longer bypassed with shell.php/ or an index.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: openat2 support is decided once per document root, not on a request’s EPERM.
  • 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 past metrics.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-stall and scripts/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.sh refuses 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-config feature schema, 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 unit scalws-console.service, enabled by the installer, which creates user admin with a random password and prints it once). Separate process; talks to scalws-controller over 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): reload adds 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 a harmful flag in status; 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-threads without event_loops in 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 needs net.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: reuseport keeps the previous behaviour.
  • WebSocket tunnels end with a close frame 1001 Going Away at 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 in bench/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

BeforeNow
scb-webdscalwsd (web server daemon)
scbctlscalwsctl (CLI)
scb-controllerscalws-controller (ScalWS Adaptive Runtime controller)
crates scb-*scalws-*
package scb-webscalws
/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: scbserver: 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-controller and the ScalWS Adaptive Runtime (ADR-0033…0036): several scalwsd instances 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 with sendfile over TLS.
  • .htaccess support (applications[].htaccess: true, ADR-0031): mod_rewrite, mod_alias, access rules, ErrorDocument, DirectoryIndex, Header, Expires; new crate scalws-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 directory 2770; 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 / .gz siblings by Accept-Encoding q-values, with Vary: 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 (feature ebpf, ADR-0025): cgroup_skb programs count network bytes and packets per application cgroup; server.ebpf.network: true publishes scalws_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; metrics scalws_cache_disk_entries, scalws_cache_disk_bytes.

Added (Python process pool, 2026-10-06)

  • Python process_pool: true: scalws runs workers.min..max single-worker server processes (uvicorn/hypercorn/gunicorn with one worker each) as a scalable pool — live scaling by the optimizer or scalwsctl scale, zero-drop scalwsctl 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.endpoint may be https:// (system roots plus optional ai.ca_bundle); non-loopback endpoints still need allow_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 from X-Forwarded-For of 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 and REMOTE_ADDR use 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; diagnostic scripts/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, crate scalws-runtime-container): containers run by podman run / docker run as 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 cleanup hook (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-Svc on TCP.
  • Bench: h3-static-small scenario (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 with tls: {acme: true}, renewal through the configuration controller (no restart), per-application backoff, state in server.acme.state_dir (0600 keys), GET /v1/certs, scalwsctl certs, metrics scalws_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 /dashboard on 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]), scalws system 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.autoscale accepts the YAML 1.1 boolean off/false as off.
  • systemd unit: RuntimeDirectoryMode=0750 so group scalws reaches 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 unless allow_remote; api_key_env; timeout, max_queue, min_interval, max_tokens), automatic explanations on new warning/critical findings, GET/POST /explain, scalwsctl explain [--refresh], metrics scalws_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 target scalws::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::audit log target, scalws_optimizer_decisions_total, GET /optimizer journal on the admin listener, scalwsctl recommendations.
  • Configuration: server.optimizer {enabled, interval, journal_size}, workers.target_concurrency.

Changed

  • workers.autoscale defaults to recommend (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, Vary variants, profile/config bypass rules, stale-while-revalidate with single-flight refresh, invalidation on unsafe requests, purge by URL, prefix, tag or all (POST /cache/purge on the loopback admin listener), X-Cache/Age headers.
  • 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_timeout for slow readers; strict_sni listeners answer 421.
  • Metrics scalws_cache_lookups_total, scalws_cache_entries, scalws_cache_bytes, scalws_security_blocked_total; rejection reason source_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 in profiles/ (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) and scalwsctl profiles.
  • profile: application key: supplies the runtime when omitted (static profiles) and health checks; runtime is optional when a profile is set.

Added (M3 tenancy, 2026-10-05)

  • scalws-cgroups (ADR-0010): cgroup v2 hierarchy under scalws’s delegated group (or server.cgroups.path), tenant limits cpu, memory (+ swap, default 0), pids, io_weight; workers join tenants/<tenant>/<app> before their first instruction; limits rewritten on reload; removed groups pruned; modes auto/required/off.
  • Per-tenant request quotas: max_concurrent_requests (503) and requests_per_second
    • burst (429), metric scalws_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 with PATH_INFO; CGI response parsing; httpoxy protection; .php never 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 by max_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_timeout configuration keys; proxy runtime gains websocket and tls_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 with RESOLVE_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 with deny_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, /metrics and /healthz.
  • scalwsd daemon (SIGHUP reload, SIGTERM drain) and scalwsctl check.
  • Integration and security test suites; Docker-based benchmark harness with nginx and Caddy comparison configs; CI workflow; cargo-deny policy; systemd unit.