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.ts on a real run. Every number below was produced by that gate on a Linux/KVM host with the msb runtime, against the REAL PiAgentSessionFactory and 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-gc present).
  • Per-VM memory, from two independent host-side sources:
    1. allSandboxMetrics() (msb SDK) → memoryHostResidentBytes: the host-resident bytes backing the guest, as the runtime accounts them;
    2. /proc/<pid>/status:VmRSS of each msb 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 wakeafter 5 wakesΔ per session
heapUsed56.08 MiB56.85 MiB0.15 MiB
RSS216.93 MiB242.79 MiB5.17 MiB
external5.70 MiB5.70 MiB0.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)

sessionmsb memoryHostResidentBytessupervisor VmRSSguest memoryBytes in use
042.09 MiB71.29 MiB52.74 MiB
141.31 MiB70.15 MiB53.71 MiB
242.39 MiB70.24 MiB53.17 MiB
343.41 MiB71.32 MiB52.30 MiB
441.78 MiB69.78 MiB55.85 MiB
mean42.20 MiB70.56 MiB53.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)

componentper session
control-plane RSS5.17 MiB
microVM supervisor RSS70.56 MiB
total (measured floor)75.73 MiB
worst case, guest touching its whole ceiling517.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)

  1. 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.
  2. Soak. N sessions × hours: is per-session memory flat or creeping (JSONL sync buffers, the outbound event ring, the agent transcript)?
  3. Fleet arithmetic. Per-tier quotas need a host budget and an overcommit policy on top of a per-session figure.