# Capacity Planning How many concurrent student Labs `rn-lab-host-01` can actually run — determined from measured data after deployment, not guessed or marketed now. ## The model `infra/scripts/capacity-model.js`: ``` usable = hostTotal * (1 - overheadPct/100) - platformReserved withMargin = usable * (1 - marginPct/100) perLabLimit = floor(withMargin / perLabUsage) recommended = min(cpuBoundLimit, memoryBoundLimit) # whichever runs out first warning = floor(recommended * 0.8) hardAdmission = recommended ``` Computed once for CPU, once for memory; the tighter (smaller) of the two constrains the actual recommendation — a host can be memory-starved long before its CPU threads are exhausted, or vice versa, and the model doesn't pretend otherwise. **Inputs** (`infra/environments/production/capacity.yml.example` — explicitly placeholder data until real numbers exist): - `host_cpu_threads`, `host_memory_mb` — from the real Lab Host VM, once sized - `overhead_pct` — Proxmox/hypervisor + OS overhead reserved off the top - `platform_reserved_cpu`/`platform_reserved_memory_mb` — headroom for the egress proxy, node-exporter, cadvisor, promtail themselves (they run on the Lab Host too) - `per_lab_cpu`/`per_lab_memory_mb` — **measured**, from `infra/scripts/loadtest/`, not assumed - `safety_margin_pct` — extra headroom below the raw math, for burst/variance **Outputs**: recommended concurrent Labs, an 80%-of-recommended warning threshold (for alerting — `LabHostHighMemory` in `docs/OBSERVABILITY.md`), and a hard admission-control threshold the Provisioner's resource policy should enforce so it refuses to boot a Lab past that point rather than degrading every running Lab. ## Load-test tooling that produces the measured inputs `infra/scripts/loadtest/`: - `boot-labs.js` — reuses `platform/test/session-helper.js` (built in Phase 3A) to drive the real admin Prepare-Labs endpoint, booting N Labs and recording boot time/failure rate per Lab into a JSON report. - `sample-resources.sh` — samples real `docker stats` for every `rootnode-lab-*` container every 5 seconds to CSV (timestamp, name, CPU%, mem usage, mem%, net I/O, block I/O, PIDs). Together these produce the `per_lab_cpu`/`per_lab_memory_mb` this model needs, plus data on how boot time and failure rate change as concurrency rises — the dimensions this phase was told to cover: single Lab boot, multiple simultaneous boots, steady-state active Labs, terminal WebSocket load, scenario validation load, Gateway throughput, API request volume, Postgres connection activity. ## What this document deliberately does not claim No concurrency number in this repo (`capacity.yml.example`'s numbers included) is a real, validated figure — every one is a placeholder until run against the real Lab Host VM with real per-Lab measurements. Treating a placeholder as a marketing/sales number would be actively misleading; the whole point of the model is that the real number gets filled in once, from measurement, after deployment (`docs/RUNBOOK_LAB_HOST_RESOURCE_PRESSURE.md` step 4 is the trigger to re-measure and update it if the per-Lab footprint later changes).