Architecture

Decision log

Significant decisions get an ADR in docs/adr/. Smaller ones are logged here with date and reason so they can be revisited.

DateDecisionReason
2026-10-05ADR-0001 Rust + Tokio + hyper data planeHandoff §1/§3
2026-10-05ADR-0002 rustls + aws-lc-rsMature TLS, PQ hybrid KX default, FIPS path
2026-10-05ADR-0003 YAML via serde-saphyr, validate-then-swapserde_yaml is deprecated upstream; atomic reload
2026-10-05ADR-0004 Handler / RuntimeAdapter splitKeep language specifics out of the core
2026-10-05ADR-0005 prometheus-client + tracing JSON, bounded labelsHandoff §10
2026-10-05ADR-0006 static confinement (canonicalise now, openat2 next)Handoff §11
2026-10-05ADR-0007 Linux-first, cross-platform developmentDev machine is Windows + Docker
2026-10-05Route matching uses the percent-decoded, dot-segment-resolved pathPrevents /%61dmin and /x/../admin route bypass
2026-10-05Encoded slash (%2F) is preserved for routing and refused by static filesAvoids creating segment boundaries from encoded data; apps that need %2F still get it via proxy
2026-10-05Requests with both Content-Length and Transfer-Encoding rely on hyper: TE wins, connection closes after the responseRFC 9112 §6.1; verified by cl_te_smuggling_does_not_reach_upstream
2026-10-05Client-supplied Forwarded/X-Forwarded-*/X-Real-IP are always replacedNo trusted-proxy configuration exists yet; spoofing-safe default
2026-10-05Connection limit pauses accept() instead of accept-and-closeKernel backlog provides natural backpressure; no user-space queue
2026-10-05Listener/metrics/max_connections changes require restartRebinding sockets during reload is out of M1 scope; explicit error instead of partial apply
2026-10-05No tower middleware stackSingle explicit pipeline function is easier to audit; revisit when middleware count grows
2026-10-05Benchmark load generator: oha 1.16.0, pinned CPUs per roleReproducible, JSON output; server/upstream/load never share CPUs
2026-10-05Static hot path does syscalls on the async worker only when the kernel guarantees they will not block (RESOLVE_CACHED, RWF_NOWAIT)Removes the blocking-pool round trip that cost ~5 context switches per request
2026-10-05Large-file chunk size 256 KiB (64 KiB measured 30% slower)scripts/profile.sh static-1m
2026-10-05Benchmarks and profiles serve the document root from a Docker volume (ext4), not overlayfsoverlayfs defeats RESOLVE_CACHED; production roots are on real filesystems
2026-10-05ADR-0008 managed processes: no shell, cleared env, process groups, 5 crashes/min → FailedHandoff §13
2026-10-05ADR-0009 PHP via fastcgi-client (small crate, wrapped, reviewed) + httparse for CGI headersHandoff §1 “use mature libraries”; replacement path documented
2026-10-05WSGI is served by gunicorn onlyuvicorn’s WSGI interface is deprecated; hypercorn’s is less used
2026-10-05Python: one supervised server process per instance, its own --workersThe application servers already manage worker processes well
2026-10-05Node: N supervised processes, one socket each, round-robin over ready onesNode has no built-in multi-process server; keeps crashes per worker
2026-10-05PHP-FPM workers run as nobody when scalws runs as rootPHP-FPM refuses root workers; per-tenant users arrive in M3
2026-10-05WebSocket tunnels are limited server-wide, not per connection slotUpgraded IO leaves hyper’s connection accounting; a separate semaphore bounds it
2026-10-05Persistent FastCGI connections only for managed pools; replay only buffered bodies (≤ 64 KiB)Kept-alive connections pin FPM workers; a stale connection must never lose a non-replayable request
2026-10-05No LSAPI adapter for performanceProfiling showed the gains attributed to LSAPI (persistent connections, less setup) are available over FastCGI; PHP execution is the remaining cost
2026-10-05Benchmark container needs --cap-add SYS_NICERe-pinning PHP-FPM workers (another uid) to upstream CPUs requires it; the harness warns otherwise
2026-10-05ADR-0010 cgroup placement via a fixed /bin/sh trampolinestd/tokio offer no clone3(CLONE_INTO_CGROUP) and pre_exec needs unsafe; moving after spawn would let early forks escape
2026-10-05memory.swap.max defaults to 0 when a memory limit is setMeasured: without it a 200 MiB allocation under a 50 MiB limit succeeded via swap
2026-10-05io.weight best effortNot available with the WSL2 kernel’s I/O scheduler; skipped with one warning
2026-10-05Tenant request quotas instead of attributing scalws CPU to tenantsHandoff §7: do not claim cgroups isolate CPU used inside the shared server
2026-10-05Per-tenant Unix users designed, not implementedHandoff §7 lists them as a later hardening feature
2026-10-05ADR-0011 profiles as embedded YAML data + small fact extractors in codeHandoff §6 “data-driven”; facts like a Python app object cannot be expressed as data
2026-10-05profile: never inspects application files at load timeA deploy must not silently change how an application runs (handoff §6)
2026-10-05Explicit runtime overrides a profile’s runtime as a wholePredictable; field-level merging deferred
2026-10-05ADR-0012 cache: moka storage, explicit freshness only, opt-in per appMature eviction; never infer cacheability (handoff §8)
2026-10-05ADR-0013 per-source state: 64 shards, fixed capacity, evict idle, fail open when all activeMemory bounded by configuration, not by attacker address count
2026-10-05Deny rules answer 404, not 403Do not confirm that a sensitive resource exists
2026-10-05Purge endpoint on the loopback metrics listenerInterim admin surface until the authenticated admin API (M9)
2026-10-05Property-based fuzzing (proptest) instead of cargo-fuzz for nowRuns on stable in every test run; coverage-guided fuzzing needs nightly
2026-10-05ADR-0014 optimizer: pure engine, caller supplies time and applies decisionsDeterministic, unit- and property-testable without clocks
2026-10-05First optimizer action: Node worker targets onlyOnly runtime where scalws owns individual workers; others get recommendations/advisories
2026-10-05workers.autoscale default recommendHandoff §8: recommend-only by default
2026-10-05Disposition names applied/rolled_back identical in JSON, metrics and logsOne vocabulary for operators and tooling
2026-10-05ADR-0015 diagnostics: pure scalws-diag, evaluated in the optimizer’s window loopOne sampling path, deterministic and testable; works with the optimizer disabled
2026-10-05eBPF deferredHandoff: optional enrichment only; every M7 finding comes from cgroup files and counters
2026-10-05Phases: time to headers and transfer onlyscalws rejects instead of queueing, so there is no queue phase; connect time hidden in pooled clients
2026-10-05ADR-0016 AI advisor: OpenAI-compatible HTTP provider, disabled by defaultWorks with llama.cpp/Ollama/vLLM/LM Studio without hard-coding a model
2026-10-05Remote model endpoints need allow_remote: true; HTTPS deferredTelemetry stays on the host unless the operator decides otherwise
2026-10-05Validate model output even with response_formatNot every server enforces schemas; nothing unvalidated is shown
2026-10-05ADR-0017 admin API on a Unix socket in scalws-server, access by file permissions + auditHandoff §11; no network exposure; peer credentials attributable
2026-10-05No in-process privilege dropUnit runs unprivileged with CAP_NET_BIND_SERVICE; multi-threaded setuid avoided
2026-10-05Rollback re-applies a kept configuration as a new generationOne code path for every change; history stays linear and auditable
2026-10-05Packages with nFPM, stripped binariesOne definition for DEB and RPM; 9.6 MB instead of 30 MB
2026-10-05Gate 6 verified with pass/fail scripts in the bench imageRepeatable on any host; criteria in bench/README.md
2026-10-05ADR-0018 ACME with instant-acme, HTTP-01 only, renewal via controller re-applyPure Rust/rustls/aws-lc-rs; one activation path; no restart
2026-10-05Allow licence CDLA-Permissive-2.0Mozilla root-certificate data pulled in by the ACME client’s HTTPS stack
2026-10-06ADR-0019 HTTP/3 opt-in feature, same pipeline via call_boxedh3 is 0.0.x; one request path for all protocols
2026-10-06ADR-0020 containers via a daemonless engine CLI as supervised workersReuses supervision, pools and tenancy; no daemon or OCI tooling of our own
2026-10-06--init and a post-exit rm --force for container workersPID-1 servers ignore SIGTERM; killed podman run leaves containers behind
2026-10-06ADR-0021 multi-node: pull-based controller, signed bundles, existing apply path on nodesNo distributed store; control-plane outage blocks changes, never serving
2026-10-06Proxy drives upstream connections in the request task (own pool)Profiling: the task hop of hyper-util’s client dominated; +28 % rps
2026-10-06Per-core runtimes not adoptedMeasured upper bound ≈ +10 % for proxying; not worth the complexity now
2026-10-06ADR-0022 trusted proxies: right-most untrusted XFF entry; PROXY v1/v2 from trusted peers onlySpoof-resistant client address for limits, logs and applications
2026-10-06ADR-0023 remote admin: mTLS only, same router as the socketHandoff §11: remote only explicitly and strongly authenticated
2026-10-06ADR-0024 cache disk tier: one file per variant, writer thread, index rebuilt at startNo new dependency; request path never blocks on disk
2026-10-06ADR-0025 eBPF: cgroup_skb network accounting only, C program + Aya, feature-gatedThe one signal cgroup v2 lacks; no nightly toolchain; never required
2026-10-06Allow licence Zlibfoldhash via aya (optional eBPF feature); OSI-approved permissive
2026-10-06ADR-0026 per-tenant Unix users via setpriv; CAP_KILL requiredKernel drops the parent-death signal and stop signals to other uids without it (found in testing)
2026-10-06ADR-0027 sendfile via placeholder buffers recognised by address in the I/O layerNo unsafe, no hyper fork; byte-exact tests guard the hyper queue-strategy dependency
2026-10-06ADR-0028 open file cache: identity check per hit (statx), optional validity window+10 % small files; skipping the check gained nothing more
2026-10-06ADR-0029 per-core event loops sharing one listening socket+38 % upper bound on respond; shared socket balances long-lived connections better than SO_REUSEPORT hashing
2026-10-06Keep hyper; drop hyper-util auto-detectionBare hyper per core ≈ nginx; auto layer cost 14 %; own parser only +21 % at high risk
2026-10-06mimalloc defaultProxy +17 %, cache hit +26 % (2-run average), +20 MiB RSS
2026-10-06ADR-0030 kTLS opt-inHTTPS 1 MiB 3.9k → 6.2k req/s; needs the tls module
2026-10-06ADR-0031 .htaccess with the linear-time regex crateTenant-controlled input: no backtracking, no proxy, bounded
2026-10-08ADR-0042 per-domain counters by configured pattern, indexed and sharded per event loopCardinality set by configuration, not clients; no request-path hashing; gate: no regression