PandaStack
Sandboxes

Sandbox lifecycle

Born, used, paused, hibernated, forked, snapshotted, deleted — every state a sandbox can be in.

A sandbox is a single Firecracker microVM with a dedicated kernel, rootfs, network namespace, and lifecycle. Sandboxes are identified by a UUID (e.g. 278a4f42-3467-4424-98e6-a547646dd0fd) and always belong to a workspace.

States

                    create


                  ┌─ creating ─┐

                       │ snapshot restored, SSH ready

                    running ◄────────────┐
                       │                 │
                   pause │                │ resume
                       ▼                 │
                    paused ──────────────┘

                       │ hibernate

                  hibernated  (memory written to disk, VM stopped)

                       │ wake (manual or implicit on next request)

                    running

   any state ──► failed   (orchestrator marks unhealthy sandboxes)
   any state ──► deleted  (rootfs purged, slot released)

Lifetime guarantees

TransitionP50P99Notes
Create → running49 ms64 msSnapshot-natid path on every create, n=50 prod. See Performance.
Pause → resume<5 ms8 msJust firecracker pause / resume. No memory writes.
Hibernate → wake~150 ms220 msMemory written to disk, restored on demand.
Fork (CoW + reflink)~50 ms80 msCheaper than create; shares pages with parent.
Kill~30 ms60 msTap interface released, NAT slot freed, rootfs unlinked.

Creating

sandbox = Sandbox.create(
    template="code-interpreter",
    ttl_seconds=3600,
    metadata={"job": "demo"},
)
const sandbox = await Sandbox.create({
  template: "code-interpreter",
  ttlSeconds: 3600,
  metadata: { job: "demo" },
});
pandastack sandbox create --template code-interpreter --ttl 3600 --metadata job=demo
curl -X POST https://api.pandastack.ai/v1/sandboxes \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"template": "code-interpreter", "ttl_seconds": 3600, "metadata": {"job": "demo"}}'

The returned object includes id, boot_ms, boot_mode, guest_ip, and any metadata you supplied. boot_mode = "snapshot-natid" means the sandbox was restored from the template's baked snapshot — the normal path for every create; cold means the template had no snapshot yet (first-ever boot on that host).

Pause and resume

pause halts the vCPU without touching memory. Useful when an agent is mid-loop and waiting on a human approval — you stop paying for CPU but keep the entire process tree alive.

sandbox.pause()
# ...wait for user input...
sandbox.resume()
await sandbox.pause();
// ...wait for user input...
await sandbox.resume();
pandastack sandbox pause <sandbox-id>
# ...wait for user input...
pandastack sandbox resume <sandbox-id>
curl -X POST https://api.pandastack.ai/v1/sandboxes/<sandbox-id>/pause \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

# ...wait for user input...

curl -X POST https://api.pandastack.ai/v1/sandboxes/<sandbox-id>/resume \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

The sandbox stays on the same host. No state is written to disk.

Hibernate and wake

hibernate is the heavier sibling: it dumps the sandbox's memory + disk to a snapshot, then stops the Firecracker process entirely. Host RAM and vCPUs are freed.

sandbox.hibernate()
# ...minutes or hours later...
sandbox.wake()
await sandbox.hibernate();
// ...minutes or hours later...
await sandbox.wake();
pandastack sandbox hibernate <sandbox-id>
# ...minutes or hours later...
pandastack sandbox wake <sandbox-id>
curl -X POST https://api.pandastack.ai/v1/sandboxes/<sandbox-id>/hibernate \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

# ...minutes or hours later...

curl -X POST https://api.pandastack.ai/v1/sandboxes/<sandbox-id>/wake \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Two things to know:

  1. Hibernation is automatic for persistent sandboxes. The server-side idle sweeper hibernates any persistent: true sandbox that's been idle for 5 minutes. Wake is implicit on the next API request.
  2. Hibernated sandboxes still cost storage. A snapshot of a 2 GB memory sandbox is ~2 GB on disk. Use kill if you don't need it back.

TTL and persistence

Every sandbox has a TTL — the wall-clock seconds until the orchestrator kills it. Default is 1 hour. Update at runtime:

sandbox.set_ttl(7200)              # extend to 2 hours
sandbox.set_persistent(True)       # opt out of TTL; survive idle sweeper
print(sandbox.lifecycle())
# {"ttl_seconds": 7200, "persistent": true, "idle_seconds": 12}
await sandbox.setTtl(7200);
await sandbox.setPersistent(true);
console.log(await sandbox.lifecycle());
pandastack sandbox set-ttl <sandbox-id> --ttl 7200
pandastack sandbox set-persistent <sandbox-id> --persistent true
pandastack sandbox lifecycle <sandbox-id>
FieldMeaning
ttl_secondsTime until kill, refreshed by any API activity on the sandbox.
persistentIf true, ignores TTL but is hibernated when idle. Wakes on next request.
idle_secondsSeconds since the last exec / filesystem / network request.

Forking

child = sandbox.fork(metadata={"branch": "feature-x"})
fanout = sandbox.fork_tree(count=8, metadata={"batch": "search"})
const child = await sandbox.fork({ metadata: { branch: "feature-x" } });
const fanout = await sandbox.forkTree({ count: 8, metadata: { batch: "search" } });
pandastack sandbox fork <sandbox-id> --metadata branch=feature-x
pandastack sandbox fork-tree <sandbox-id> --count 8 --metadata batch=search
# Fork once (repeat the call for a fan-out)
curl -X POST https://api.pandastack.ai/v1/sandboxes/<sandbox-id>/fork \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Forks share memory pages with the parent (copy-on-write) and rootfs blocks via XFS reflink. Fork is ~3× faster than create-from-snapshot and uses an order of magnitude less disk. Promote a child to standalone with child.promote() — it'll keep running even after the parent is killed.

Snapshotting

snap_id = sandbox.snapshot()
# → "snap-…" (the snapshot id)
const snap = await sandbox.snapshot();
// → { id: "snap-…", sandbox_id, created_at, mem_path, state_path }
pandastack sandbox snapshot <sandbox-id>
curl -X POST https://api.pandastack.ai/v1/sandboxes/<sandbox-id>/snapshots \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Take a named snapshot any time. Boot a fresh sandbox from it later by passing from_snapshot: snap.id to create. Use this for golden-image flows ("here's the env after a 20-step setup, branch from here forever").

Lease semantics

Every running sandbox holds a lease in the shared store. The agent that owns it refreshes the lease every 10 s. If the lease expires (agent crash, network partition), the scheduler marks the sandbox failed and releases its NATID slot.

This is what lets you safely have a multi-node cluster: a dead agent's sandboxes don't linger as zombies.

Health monitor

While a sandbox is running, the agent runs a background health monitor that:

  • Pokes the SSH socket every 5 s.
  • Checks Firecracker's /state endpoint every 30 s.
  • Tracks RSS + vCPU usage for metrics.
  • Marks the sandbox failed after 3 consecutive failed probes.

You can subscribe to lifecycle changes via the events stream.

Cleanup with using

In Python, Sandbox is a context manager:

with Sandbox.create(template="code-interpreter") as sandbox:
    sandbox.exec("python3 train.py")
# kill() called on __exit__

In TypeScript, Sandbox is AsyncDisposable:

{
  await using sandbox = await Sandbox.create({ template: "code-interpreter" });
  await sandbox.exec("python3 train.py");
}  // sandbox.kill() called automatically

On this page