ReferenceAsk
Remy Reference/Architecture/Architecture/Release Sandboxes
06Architecture

Release Sandboxes

The sandbox a release's methods run in, the warm pool it comes from, and what is inside it once assigned.

A deployed app's methods run in a release sandbox: a Kata microVM on the kata_apps island, assigned to one release, kept warm while the app is in use, and replaced when it is not. This chapter covers the lifecycle of that sandbox, the warm pool it comes from, what is inside it, and how it talks to the platform.


#Persistent, Not Per-Invocation

A release sandbox is not created per request. When a method is first invoked for a release, the platform claims a warm sandbox from the pool, configures it for that release, and records the assignment. Subsequent invocations for the same release go to the same sandbox over a persistent WebSocket connection. The sandbox stays alive as long as it is being used.

The pod owns its own death. At creation, the platform injects its timings as environment variables from one configuration file. The worker reads them and hardcodes nothing.

ClockValueWhat it does
Unclaimed max age4 hoursA warm pod that is never assigned exits on its own
Assigned idle30 minutesAn assigned pod exits after this long without an execute
Handler timeout30 minutesThe cap on a single method execution
Background cutoff30 minutesHow long post-response background work may keep flushing
Shutdown drain25 secondsIn-flight work finishes before the process exits on SIGTERM
Kubernetes deadline24 hoursThe hard backstop for a wedged pod that never self-exits

Sandboxes come in two roles. An interactive sandbox serves requests where a user is waiting, and it is reused across invocations. A disposable sandbox runs one execution and exits on its own. Cron and email triggers use these, so a background run never keeps a shared pod alive after the work is done. A disposable sandbox is never reused for a normal request.


#The Warm Pool

Every release sandbox comes from a warm pool: pods booted ahead of demand, labeled warm, waiting to be claimed. A claim turns a warm pod into this release's pod in tens of milliseconds, not the seconds a cold boot takes.

Global, not per replica. The orchestrator runs as several replicas. The pool is one pool across all of them. Which warm pod a caller receives is decided by an atomic pop in a Redis registry. The pool label on the pod then flips to claimed, and that flip is the durable record. The registry itself is an index, rebuildable from the pods, rather than a second source of truth.

Two tiers. A reserved tier is claimable only by interactive work, where a person is waiting. A shared tier is claimable by anyone, including background work. The tier is stamped on the pod at birth, so the claim enforces the partition instead of relying on callers to respect it.

One leader sizes, every replica admits. The control plane observes demand and decides how many pods should exist. It runs on exactly one replica, chosen by leadership over a Valkey lease. The data plane (the atomic claim and the waiter queue) runs on every replica. This is the standard Kubernetes controller shape: one leader-elected reconciler, everything else stateless.

The sizing is textbook queueing. The controller measures arrival rate and boot time, holds the in-flight quantity those imply plus square-root staffing headroom, adds stock for the largest burst it has recently observed, and adds anything currently queued. The target is the maximum of recent wants over a stabilization window, so scale-down happens by samples expiring rather than by a decay constant. Every term is a pure function of declared inputs, with no clock and no I/O in the decision path. That is what lets the controller's scenarios run deterministically in a regression harness.

Admission. When no warm pod is available, an interactive caller waits, within a bounded budget, for a fill. If the pool cannot honor that wait, the caller gets a clear error instead of being left hanging. Creates are paced per pass to stay inside the Kubernetes API server's fairness limits, and a rate-limit response from the API server pauses fills with jitter so replicas do not resume in lockstep.


#What Is Inside

The worker image is a pure runtime: a pinned Node.js, the platform SDK, and a worker process that listens on the worker port. It runs as an unprivileged user. It carries no compiler toolchain. A release's native dependencies are built once at publish and shipped in the dependency artifact.

At assignment the platform configures the sandbox with everything the release needs:

  • The dependency artifact. A prebuilt node_modules tarball for the release, fetched by the pod from a short-lived presigned URL, stamped with the ABI it was built against so the worker can verify a match before extracting. See Job Runners.
  • The app's own secrets. The prod values of the secrets the builder stored, decrypted by the platform and injected as environment variables. See Secrets.
  • The callback origin. The one hostname the sandbox reaches back on.
  • Its own sandbox id. Every execute frame names the sandbox it is for, and the worker rejects a frame addressed to a different id. Pod IPs are recycled quickly. This check is what makes a stale routing entry harmless.

Each execute frame carries the compiled handler by its immutable artifact key, the handler name, the parameters, a hook token scoped to this execution, the auth context, and the database bindings for the environment. The worker caches compiled handlers by key, so code travels only on a cold miss.

The sandbox holds only what belongs to this app. Every managed capability the app uses is a call back into the platform, authorized by the hook token: database queries go to the app database role, platform actions go to the hook endpoints, and model calls go to the model service. No provider key, no platform credential, and no other app's data is ever inside the VM.


#Assignment, Reuse and Invalidation

The HTTP role owns the mapping from release to sandbox in Redis. When a release is promoted, the previous release's sandboxes are stopped by app label, and the new release claims fresh ones on first use. When a secret changes, the app's sandboxes are stopped so no pod runs with stale values. Dead sandbox detection and retry live in the HTTP role: a sandbox that stops answering is dropped from the mapping and the next invocation claims another.

The worker image is pinned to a moving alias. Every pod start re-checks the digest, and the pool controller recycles warm pods running a stale image, so a push to the image rolls the warm pool without a platform deploy.

What This Gives an App
A warm sandbox on first request, in the tens of milliseconds, without a cold boot on the request path.
Its own VM, holding only its own code, dependencies, and secrets.
Bounded execution: a hung method cannot pin a sandbox or the platform.
Background triggers that cannot keep an interactive pod alive.
Immediate invalidation when it publishes or rotates a secret.
DiagramFIG. 06 — RELEASE SANDBOXES
Pool controller · leaderone sizes, every replica admitstarget ← arrival · boot · burst · queueHTTP roleowns release → sandbox mapdetects and replaces dead podsApp database roledatabase queriesHook endpointsplatform actionsModel servicemodel callsclaimedWARM POOL · kata_appsTHE PLATFORMHTTP ROLE DRIVES THE SANDBOX · PERSISTENT WEBSOCKETconfigure: dependency artifact URL · secrets · callback origin · sandbox idexecute frame: handler @ artifact key · params · hook token · auth · db bindingsRESERVED · interactive onlya person is waitingSHARED · anyone, incl. backgroundtier stamped at birthall labeled warm · claim flips it to claimedRedis registry · atomic pop · rebuildable indexCLAIMatomic pop · tens of msRelease sandboxcarries its release, appId, and roleWorker runtimepinned Node.js · platform SDK · worker :port · unprivilegedDependency artifactprebuilt node_modules · ABI-stamped · presigned URLApp secretsprod values, injected as env varsCompiled-handler cacheby artifact key · code travels only on a cold missSandbox idnamed in every frame · mismatched frame rejectedrole at claim: interactive (reused) · disposable (one run, self-exit)Holds only this app: code, dependencies, secrets.No provider key. No platform credential. No other app's data.REACHED VIA THE CALLBACK ORIGINHTTPS · hook token scoped to one executionINVALIDATION · STOP-BY-APPthe HTTP role stops the app's sandboxes on promote or secret rotation; the next use claims fresha sandbox that stops answering is dropped from the map and replaced
warm pod · pooled ahead of demandrunning component / platform surfaceclaim · drivecallback · the only path backgrouping
A release sandbox is persistent, not per invocation. Pods boot ahead of demand and wait in the warm pool, split into a reserved tier only interactive work can claim and a shared tier open to anything. A claim is an atomic pop from a Redis registry: a warm pod becomes this release’s pod in tens of milliseconds, versus seconds for a cold boot. Once claimed, it is configured for one release, holding that app’s runtime, dependency artifact, and secrets, and nothing from another app or from the platform. The HTTP role drives it over a persistent WebSocket in execute frames; the sandbox’s only path back is the callback origin, over HTTPS, authorized by a per-execution hook token. The HTTP role can end it early with stop-by-app, whenever a release is promoted or a secret rotated, and the next invocation claims a fresh one.

Figure 06, "Release Sandboxes": the lifecycle of one sandbox on kata_apps, read left to right. Left, the warm pool: a leader-elected pool controller (one replica sizes it from arrival rate, boot time, the largest recent burst, and the queue; every replica admits) over two shelves of warm pod glyphs, reserved (interactive only) and shared (anyone, including background). A claim is an atomic pop from a Redis registry, in tens of milliseconds. Center, the claimed sandbox as the hero: a "claimed" state pill marks the flip from warm; it carries its release, app id, and role. Its anatomy is a divided list: worker runtime (pinned Node.js, platform SDK, unprivileged), the dependency artifact (a prebuilt node_modules, ABI-stamped, fetched from a presigned URL, shown as a highlighted row), app secrets (injected as env vars), a compiled-handler cache (by artifact key), and its sandbox id (named in every frame). A footer sets apart the role line (interactive, reused; or disposable, one run) and the negative claim: it holds only this app's code, dependencies, and secrets, and no provider key, platform credential, or other app's data. Right, the platform: the HTTP role drives the sandbox over a persistent WebSocket (configure, then execute frames). The sandbox's only path back is the callback origin, over HTTPS with a per-execution hook token, reaching three surfaces shown by a dashed egress fan: the app database role, the hook endpoints, and the model service. A center note carries invalidation (stop-by-app on promote or secret rotation). No trust boundary in this figure, so no crimson.