Decision records
ADR-0014: Deterministic optimizer (policy engine)
- Status: Accepted (2026-10-05)
Context
Handoff §8: a policy engine fed with rolling telemetry; bounded actions (runtime worker targets, connection-pool bounds, selected cache parameters); every rule has min/max, cooldown, hysteresis, preconditions and rollback conditions; recommend-only by default, automatic mode opt-in per application; every change audited (measurements, rule, old/new value, reason, observed outcome). The local LLM (M8) is not the optimizer.
Decision
scalws-policyis pure: it receives one telemetry snapshot per application and evaluation window and returns decisions. No I/O, no clocks of its own, no randomness; identical input sequences give identical decisions (unit-tested with synthetic telemetry).- Telemetry per application (window =
server.optimizer.interval, default 10 s): requests, 5xx ratio, latency p50/p95/p99 (power-of-two microsecond buckets: accurate to a factor of two, enough for the generous rollback thresholds), average in-flight requests (sampled every second), ready/target workers, tenant memory pressure (memory.current / memory.maxfrom cgroups, when limits are enforced), cache hit ratio. Latency is time to response headers. - Worker utilisation = average in-flight / (ready workers ×
workers.target_concurrency; default 4, or 1 for synchronous WSGI). - Rules (parameters in
scalws-policy, documented in its source):scale_up: utilisation ≥ 0.75 in 3 consecutive windows, ≥ 20 requests per window, memory pressure < 0.85, belowworkers.max→ target + 1; cooldown 60 s.scale_down: utilisation ≤ 0.25 in 6 consecutive windows, aboveworkers.min→ target − 1; cooldown 120 s.- Hysteresis: the up/down thresholds are far apart and a streak resets as soon as a window leaves the band.
- Rollback: 3 windows after an applied change, if the 5xx ratio rose by more than 5 points or p95 grew by more than 50 % (and above 50 ms) compared with the window before the change, the old value is restored and the rule is suppressed for 10 minutes.
- Advisory only (never acted on):
memory_pressure(≥ 0.9 for 3 windows),error_rate(≥ 20 % 5xx with ≥ 20 requests).
- Modes from
workers.autoscale:off,recommend(default),auto. - Actions in this stage: live worker targets of managed Node.js worker pools. Endpoints
for
workers.maxare reserved when the instance is prepared; scaling up starts workers in free slots, scaling down takes the highest slot out of rotation, waits for its in-flight requests (up tostop_timeout) and stops it. Python receives recommendations only when it runs as one server with a fixed--workerscount; withprocess_pool: true(since 2026-10-06) scalws runs single-worker processes in a scalable pool and acts on Python like on Node.js. PHP-FPM sizes its own pool (pm = dynamic) and receives advisories only. Connection-pool and cache parameters are not changed automatically yet. - A configuration reload that changes the runtime spec creates a new instance at
workers.min; an unchanged spec keeps the instance and its current worker count. server.optimizer.enabledandintervalapply on reload;journal_sizeat start.- Audit: every decision (recommended, applied, rolled back, outcome) is logged under
target
scalws::auditand kept in a bounded in-memory journal served as JSON atGET /optimizeron the loopback admin listener;scalwsctl recommendationsreads it.
Consequences
- Telemetry is application-wide; an application with several runtimes is optimised on its default runtime.
- The journal is lost on restart (the structured log is the durable record).