Phase-1 Capacity Envelope (§29.2 load gate, re-measured under R7.3)
July 17, 2026 · View on GitHub
Generated by
packages/backend/test/phase1-gate/load.gate.test.tson a real run. Every number below was produced by that gate on a Linux/KVM host with the msb runtime, against the REALPiAgentSessionFactoryand REAL detached microVMs. When the gate skips (no KVM) this file is NOT rewritten: it is never populated from a non-measurement.Measured 2026-07-14T14:32:57.997Z · host: 32 vCPU, 61815.19 MiB RAM · Node v26.4.0
Reproduce (heap figures require a forced GC, so the report is only published from a run that has one):
NODE_OPTIONS=--expose-gc pnpm --filter @pi-managed/backend exec \ vitest run test/phase1-gate/load.gate.test.ts
What was measured — and what was not
Measured (real). 5 sessions woken through the production path and held
simultaneously awake. Each session = a real ManagedSessionRuntime + a real Pi
AgentSession (built by PiAgentSessionFactory: in-memory auth storage, model
registry, per-session resource loader, settings manager, JSONL SessionManager, the 9
sandbox-bound customTools, the managed extensions) + a real detached microVM, with a
real exec executed inside every guest.
NOT measured — needs a live provider key. No model turn was run. Nothing token-driven is in these numbers: no prompt cache, no conversation transcript, no tool output retained in history, no per-turn model-client allocation. A session under real load costs strictly more than the figures below, by an amount this environment cannot measure. Treat every number here as a floor: the cost of a session that has not yet spoken to a model.
Method
- N = 5 sessions (override with
GATE_LOAD_N), woken sequentially, then all held awake at the same time. - Control plane:
process.memoryUsage()of the Node process, sampled before the first wake and after the Nth — forced GC before every sample (--expose-gcpresent). - Per-VM memory, from two independent host-side sources:
allSandboxMetrics()(msb SDK) →memoryHostResidentBytes: the host-resident bytes backing the guest, as the runtime accounts them;/proc/<pid>/status:VmRSSof eachmsb sandbox --name <name>supervisor: the entire VMM process (resident guest pages + libkrun + msb's own memory). This is the number to budget with — it is what the host actually pays per VM.
- VM shape:
ubuntu:22.04, 1 vCPU, 512 MiB guest ceiling.
Measured (this run)
Control plane (one Node process holding all 5 sessions)
| before wake | after 5 wakes | Δ per session | |
|---|---|---|---|
| heapUsed | 56.08 MiB | 56.85 MiB | 0.15 MiB |
| RSS | 216.93 MiB | 242.79 MiB | 5.17 MiB |
| external | 5.70 MiB | 5.70 MiB | 0.00 MiB |
After the in-guest execs: RSS 244.89 MiB, heapUsed 56.93 MiB. Mean wake latency (provision + boot + real agent construction): 0.2 s/session.
Per-VM (the dominant cost)
| session | msb memoryHostResidentBytes | supervisor VmRSS | guest memoryBytes in use |
|---|---|---|---|
| 0 | 42.09 MiB | 71.29 MiB | 52.74 MiB |
| 1 | 41.31 MiB | 70.15 MiB | 53.71 MiB |
| 2 | 42.39 MiB | 70.24 MiB | 53.17 MiB |
| 3 | 43.41 MiB | 71.32 MiB | 52.30 MiB |
| 4 | 41.78 MiB | 69.78 MiB | 55.85 MiB |
| mean | 42.20 MiB | 70.56 MiB | 53.56 MiB |
The guest ceiling is 512 MiB, but a guest only resides what it touches, so the measured resident cost sits far below it. Memory is overcommitted by design. A capacity model that multiplies sessions × 512 MiB is wrong by roughly an order of magnitude; a model that assumes the idle RSS above is wrong the other way the moment an agent allocates inside the guest (a build, a test run, a large file). 512 MiB is the worst case one session can force.
Per-session total (idle, no model turn)
| component | per session |
|---|---|
| control-plane RSS | 5.17 MiB |
| microVM supervisor RSS | 70.56 MiB |
| total (measured floor) | 75.73 MiB |
| worst case, guest touching its whole ceiling | 517.17 MiB |
What this licenses — and what it does not
Licensed by the data. The floor per idle woken session on this host is 75.73 MiB. The control plane is NOT the constraint: 5.17 MiB per session, a rounding error beside the VM. The per-session ceiling is bounded by the guest's 512 MiB limit plus the control-plane share.
NOT licensed by the data: any per-tier concurrency number. Two inputs are missing:
(1) the token-driven steady-state cost (no live key ⇒ not measured), and (2) a production
host memory budget — these figures are per host, while a tier quota is a per-tenant
policy over a fleet. What the data does bound is the arithmetic: on a host with M usable
bytes, concurrent sessions ≤ M / (per-session total), i.e. between M / 75.73 MiB (all guests idle) and M / 517.17 MiB (every guest at its
ceiling). The per-tier defaults in domain/tier-config/config.ts and
domain/quota/plans.ts (free=2 / pro=10 / enterprise=50) were not derived from this
measurement and are not derivable from it.
Open (what a live provider key would close)
- Token-driven steady state. Re-run this gate with a real key over a real multi-turn workload: prompt cache + transcript + tool material per session, and how they grow with turns. Until then no number here describes a session that is actually working.
- Soak. N sessions × hours: is per-session memory flat or creeping (JSONL sync buffers, the outbound event ring, the agent transcript)?
- Fleet arithmetic. Per-tier quotas need a host budget and an overcommit policy on top of a per-session figure.