Sandbox pool sizing
The sandbox pool trades idle capacity for predictable latency. This page covers sizing it, the invariants that keep it safe, and the failure modes worth knowing.
Why a pool exists
Booting an isolated environment per request would make every run_code call
start with a multi-second cold create. Kaizen keeps a small pool allocated so a
lease is usually instant.
The knobs
| Setting | Default | Meaning |
|---|---|---|
SANDBOX_WARM_POOL_MIN | 3 | Minimum allocated workers |
SANDBOX_POOL_MAX_ALLOCATED | 32 | Hard ceiling across warm, leased, resetting, and creating |
SANDBOX_POOL_TARGET_IDLE | 1 | Clean reserve maintained during demand |
SANDBOX_BURST_IDLE_TTL_SECONDS | 300 | Cooldown before clean surplus is deleted |
SANDBOX_MAX_CONCURRENT_CREATES | 3 | Deployment-wide Kaizen create concurrency |
SANDBOX_ACQUIRE_WAIT_SECONDS | 30 | Bounded FIFO wait for a worker being created or reset |
SANDBOX_ACQUIRE_QUEUE_MAX | 256 | Hard queue-memory bound |
SANDBOX_MAX_CONCURRENT_PER_MEMBER | 2 | Per-member fairness |
All eight settings are live controls in Admin → Settings → Agents & tools. An accepted edit publishes one versioned policy to Valkey for every API replica; no restart is required. Controller URL, Valkey URL, lease backend, deployment identity, worker image, and lease timing remain deployment wiring and require rollout or restart.
Lowering a bound never interrupts an active execution. New admission observes
the lower member cap immediately, surplus clean workers retire after current
leases reset, and a smaller queue limit rejects new tickets without deleting
existing tickets. SANDBOX_ACQUIRE_WAIT_SECONDS = 0 restores immediate-busy
behavior when no clean worker or new create is available.
Each worker is 2 CPU / 4 GiB at limits, so the ceiling is also a statement about burst
capacity your cluster must be able to satisfy: at the shipped
SANDBOX_POOL_MAX_ALLOCATED of 32, a full pool is charged 64 CPU / 128 GiB
against a quota on limits, while reserving only 1.6 CPU / 8 GiB.
Capacity model
Size from measured concurrent execution demand — not total users:
concurrent workers ≈ peak execution arrivals/second × p95 execution seconds
headroom = reset/create failures + burst reserve + node disruption
1,000 signed-in users do not imply 1,000 workers. If 2% run code concurrently, the starting requirement is roughly 20 workers plus measured headroom.
Queue wait should stay below the caller's end-to-end tool timeout. If members
regularly wait the full SANDBOX_ACQUIRE_WAIT_SECONDS and then fail, the
ceiling is too low — raising the timeout only makes the failure slower.
Remember that artifact runs, package tests, and workspace-persisted agent calls
each take a lease too. run_code is not the only consumer.
Persistent Python and Node cells do not pin workers when workspace persistence
is enabled. Each cell checkpoints supported state into the same bounded
workspace snapshot and crosses the unconditional release boundary. Capacity
therefore remains a function of concurrent cells, not the number of
conversations that have used a kernel. The restored checkpoint still counts
toward SANDBOX_WORKSPACE_MAX_BYTES.
If SANDBOX_WORKSPACE_PERSIST is disabled, persistent kernels use the legacy
conversation-affinity path and hold a worker. Size that mode separately or keep
the scalable default enabled.
Allocation behavior
Cold creates reserve capacity under the allocator lock and perform the network request after releasing it. Independent creates therefore run concurrently, and releases and resets are not blocked behind a slow pod creation.
Valkey holds one deployment-scoped FIFO and worker state map. The warm minimum,
allocated ceiling, create bound, queue, and per-member cap therefore keep the
same meaning when api-backend has multiple replicas.
Every execution path — restore, policy, dispatch, snapshot, timeout, transport error, cancellation — crosses one unconditional release boundary. There is no path that leaks a lease.
Reset or discard
After use, Kaizen kills leftover processes and strictly wipes the workspace before returning the instance to the pool.
A sandbox is reused only when the reset can be proven clean. A reset failure evicts the worker rather than returning uncertain state to another member. The reuse decision requires positive evidence, not merely the absence of an error.
Leases, crash takeover, and coordination outages
Pool ownership lives in deployment-scoped Valkey state. Leases carry
SANDBOX_LEASE_TTL_SECONDS (900) and heartbeat every
SANDBOX_LEASE_HEARTBEAT_SECONDS (60). Another replica can continue a routed
session by atomically taking over its ownership record.
Expired ownership is handled conservatively. Because workers do not validate
Kaizen's operation token, an expired endpoint is never handed to a new owner.
Kaizen marks it retiring, requests controller deletion, confirms 404, then
reopens capacity and creates a clean replacement. A late release or reset from
the former owner cannot publish that replacement.
Creation failures are also conservative: a reservation remains counted when the HTTP response is ambiguous. Inventory reconciliation locates the worker by its deterministic requested name and confirms deletion before removing the reservation. Recovery depends on the controller preserving Kaizen's requested name unchanged in inventory; both supported controller backends provide that contract.
SANDBOX_LEASE_BACKEND=redis and a reachable
SANDBOX_REDIS_URL are mandatory for controller-created workers. A temporary
Valkey reachability or controller-inventory outage makes new sandbox
admission fail closed; Kaizen does not start a process-local fallback pool.
Loss of the Valkey keyspace is more disruptive: controller inventory becomes
untrusted, so reconciliation fences and deletes every Kaizen worker it finds,
including workers serving active executions, before rebuilding the pool.
The revision-coupling invariant
sandbox-worker is not an extension Deployment. The controller creates its
pods on demand from whatever image tag the deployment selected, and the API and
worker are revision-coupled.
An api-only rollout is safe only after the matching worker tag has been
pushed. Otherwise the controller stays healthy while every new worker enters
ImagePullBackOff, and Kaizen reports the resulting create timeout as sandbox
unavailability — a confusing symptom with a simple cause.
Use a full deployment whenever the matching worker image has not been published. The repository wrapper checks the registry before a filtered single-service rollout and refuses when the companion tag is missing. Do not bypass that check.
During a rolling deployment, Kaizen records the selected worker image in shared pool coordination. Idle workers from the previous revision are fenced and replaced immediately. A worker already serving an execution is allowed to finish and reset, then is replaced before it can accept another lease. This keeps active work intact while converging the entire warm pool on the selected release image. Reconciliation republishes the active replica's image selection on every pass. This recovers both from Valkey being unavailable at startup and from an older replica restarting late in a rolling update and overwriting the selector. A transient rollout ordering cannot leave the surviving API Ready with an empty pool tied to the previous release. Published catalog deployments resolve that image to an immutable digest; development deployments use a unique revision tag.
Workspace storage
Compressed workspace bytes use the S3-compatible object store by default, under
an owner- and session-hashed key
(SANDBOX_WORKSPACE_BACKEND = object_store, SANDBOX_WORKSPACE_MAX_BYTES 8
MiB).
The retained Redis store remains available, but the canonical path deliberately avoids putting multi-megabyte values in the shared Valkey memory budget.
Hashline snapshot and named-register authority uses a separate compressed object
under the same composite identity, bounded by
SANDBOX_HASHLINE_STATE_MAX_BYTES (64 MiB). Unlike workspace persistence,
authority reads and commits fail closed: losing the state may require a fresh
read_workspace, but Kaizen never guesses what a four-hex tag or named
register meant. Local affinity tests use an in-process store; multi-replica
deployments use the selected shared workspace backend.
Current known cost: the worker RPC uses base64 JSON and creates a full gzip-tar per persisted execution. Binary streaming and incremental manifests are follow-up work.
Symptoms and responses
| Symptom | Likely cause | Response |
|---|---|---|
| First execution always slow | Warm pool exhausted | Raise SANDBOX_WARM_POOL_MIN |
| Waits ~30s then fails | Ceiling too low | Raise SANDBOX_POOL_MAX_ALLOCATED |
| "Sandbox unavailable" after a partial deploy | Worker tag missing | Publish the companion tag; full rollout |
| Workers churn constantly | Reset failures | Investigate reset errors — eviction is correct, the churn is the signal |
| One member monopolizes capacity | Per-member cap | Lower SANDBOX_MAX_CONCURRENT_PER_MEMBER |
| All replicas reject new leases | Valkey or controller inventory unavailable | Restore shared coordination; do not switch to memory in controller mode |
| All workers are deleted after Valkey recovery | Valkey keyspace was lost | Let reconciliation finish and pool rebuild before reopening member traffic |
| Valkey memory pressure | Retained workspace store | Confirm SANDBOX_WORKSPACE_BACKEND = object_store |