#Sandbox
A Sandbox is an isolated execution environment where you can run code, manage files, and control network access. Each sandbox is created from a Template and has a writable root filesystem that is checkpointed across pause/resume for the same sandbox identity.
For shell access and file copy over standard SSH clients, see SSH.
Sandbox0 Cloud clients should use https://api.sandbox0.ai. Set SANDBOX0_BASE_URL only when you are connecting to a self-hosted or private deployment.
Sandbox Model#
Key Identifiers#
| Field | Description |
|---|---|
id | Unique sandbox identifier (e.g., sb_abc123) |
template_id | Template used to create this sandbox |
team_id | Team that owns this sandbox |
user_id | User who claimed this sandbox |
pod_name | Kubernetes pod name (internal) |
cluster_id | Cluster where sandbox runs in multi-cluster deployments. Present on claim and list responses, not on sandbox detail responses. |
Lifecycle States#
| Status | Description |
|---|---|
starting | Sandbox is being initialized |
running | Sandbox is active and ready to use |
paused | Sandbox has no runtime; identity and latest rootfs checkpoint are preserved |
terminating | Sandbox identity and durable state are being deleted |
failed | Sandbox encountered an error |
Pause and resume use internal lifecycle transactions, but pausing and resuming are not caller-visible statuses. Use the paused boolean as a convenience for status == "paused". Pause does not preserve running processes, memory, sockets, PID state, or live REPL sessions.
Persistent Root Filesystem#
Sandbox0 persists the sandbox writable root filesystem as part of checkpointed pause/resume:
- when
ttlexpires or pause is requested, Sandbox0 saves a rootfs checkpoint before releasing the runtime pod - when the sandbox resumes, Sandbox0 creates or reuses a runtime pod and restores the latest rootfs checkpoint before initializing sandbox processes
- files written outside mounted volumes survive pause/resume for the same sandbox identity after a checkpoint succeeds
- rootfs checkpoints for the sandbox identity are deleted when the sandbox is deleted or
hard_ttlexpires
Use the root filesystem for transparent same-sandbox continuity. Use Snapshot And Restore for named rootfs snapshots, restore, and fork operations. Use Volumes for storage that must be mounted by multiple sandboxes or accessed directly through Volume APIs.
Claim a Sandbox#
Claim a sandbox from a template. The sandbox is created from a pre-warmed pool for fast startup (<200ms cold start).
/api/v1/sandboxes
Request Body#
| Field | Type | Description |
|---|---|---|
template | string | Template ID to use |
snapshot_id | string | Optional rootfs snapshot ID used to initialize the writable root filesystem |
config | object | Optional sandbox configuration |
Sandbox Configuration#
| Field | Type | Description |
|---|---|---|
env_vars | object | Sandbox-level default environment variables for new procd-managed processes |
ttl | integer | Time to live in seconds (soft limit, triggers auto-pause; default: 0, disabled) |
hard_ttl | integer | Hard sandbox lifetime in seconds (deletes identity and durable state; default: 0, disabled) |
resources | object | Optional per-sandbox resource override. Only resources.memory is accepted; CPU is derived from the platform memory-per-CPU ratio with a 150m minimum limit. |
network | object | SandboxNetworkPolicy. Controls traffic rules, protocol controls, credential bindings, and destination-scoped egress auth |
webhook | object | Webhook configuration |
auto_resume | boolean | Auto-resume when accessed (default: true) |
services | array | Sandbox Services for public HTTP routes, including Sandbox Functions |
When the cluster claim/start admission budget is full, the claim API returns 429 with error.code set to claim_start_throttled and a Retry-After header in seconds. Retry after that delay. Team quota failures also return 429, but use error.code set to quota_exceeded.
See Network and Protocol Controls for outbound control, Sandbox Services for public HTTP controls, Sandbox Functions for inline public handlers, and Credential for outbound auth and secret handling.
Sandbox env_vars override template/container environment variables for new contexts, supervised session attempts, command services, and function executions. Per-context, per-session, per-command, service runtime, and function env_vars override sandbox env_vars for that narrower scope. Warm processes start before the sandbox is claimed, so they do not receive claim-time sandbox env_vars.
Sandbox Resources#
Set config.resources.memory when one sandbox needs a different memory limit from its template. The minimum is 128Mi. The platform maximum defaults to 32Gi and can be changed by the operator with services.manager.config.sandboxMaxMemory.
The request only accepts memory. Manager derives CPU from services.manager.config.teamTemplateMemoryPerCpu and applies the larger of that value and the 150m minimum CPU limit to the sandbox runtime container. Existing running sandboxes can update memory with the Update Sandbox API.
SDK helpers expose this as WithSandboxMemory / UpdateSandboxMemory in Go, memory / updateMemory in TypeScript, memory / update_memory in Python, and --memory in the s0 CLI.
TTL vs Hard TTL#
Sandbox0 uses two-tier TTL to balance resource efficiency and flexibility:
| Field | Behavior | Use Case |
|---|---|---|
ttl | Runtime soft pause: When expired, Sandbox0 checkpoints the writable root filesystem, deletes the runtime pod, and marks the sandbox paused. | Keep sandboxes alive during active use while freeing compute resources during idle periods. |
hard_ttl | Sandbox hard delete: When expired, Sandbox0 deletes the sandbox identity and durable state, including paused rootfs checkpoints. | Enforce a maximum lifetime for compute and storage resources. |
The relationship: ttl <= hard_ttl. When ttl expires first, sandbox pauses but can be resumed.
When hard_ttl expires, the sandbox is deleted and later access returns not found. Resume starts a new runtime generation from the latest rootfs checkpoint only while the sandbox is paused and still within its hard TTL.
Example timeline (sandbox created with ttl=300 and hard_ttl=3600):
t=0: Sandbox createdt=300: TTL expires → sandbox auto-pausest=310: User calls refresh → TTL reset to 300, Hard TTL reset to 3600 (new hard deadline att=3910)t=610: TTL expires again → sandbox auto-pausest=3910: Hard TTL expires → sandbox identity and durable state are deleted
goctx := context.Background() client, err := sandbox0.NewClient( sandbox0.WithToken(os.Getenv("SANDBOX0_TOKEN")), sandbox0.WithBaseURL(os.Getenv("SANDBOX0_BASE_URL")), ) if err != nil { log.Fatal(err) } // Claim a sandbox from the "default" template sandbox, err := client.ClaimSandbox(ctx, "default", sandbox0.WithSandboxHardTTL(300), sandbox0.WithSandboxMemory("512Mi"), sandbox0.WithSandboxEnvVars(map[string]string{ "APP_ENV": "development", }), ) if err != nil { log.Fatal(err) } fmt.Printf("Sandbox ID: %s\n", sandbox.ID) defer client.DeleteSandbox(ctx, sandbox.ID)
Get Sandbox Details#
Retrieve full details about a sandbox.
/api/v1/sandboxes/{id}
gosb, err := client.GetSandbox(ctx, sandbox.ID) if err != nil { log.Fatal(err) } fmt.Printf("Status: %s\n", sb.Status) fmt.Printf("Template: %s\n", sb.TemplateID) fmt.Printf("Expires at: %s\n", sb.ExpiresAt)
Get Sandbox Status#
Get the current status of a sandbox (lighter weight than full details).
/api/v1/sandboxes/{id}/status
gostatus, err := client.StatusSandbox(ctx, sandbox.ID) if err != nil { log.Fatal(err) } fmt.Printf("Status: %s\n", status.Status.Value)
Observability#
Sandbox0 exposes chart-ready runtime metrics, historical logs, and a canonical per-sandbox audit ledger. Audit is stored directly in ClickHouse and never wakes a paused sandbox. It is separate from platform service telemetry and the metering usage ledger. Audit ingestion and the observability-event query and watch API require the enterprise sandbox_audit feature; logs, runtime metrics, and the metric catalog do not.
/api/v1/sandboxes/{id}/observability/events
/api/v1/sandboxes/{id}/observability/logs
/api/v1/sandboxes/{id}/metrics
/api/v1/sandboxes/{id}/metrics/catalog
Runtime metrics#
The metrics endpoint returns bounded, server-downsampled series ready for charts. The default query covers the last hour and requests CPU utilization, memory utilization, and network I/O. A query cannot span more than 30 days.
| Parameter | Type | Description |
|---|---|---|
start_time | RFC3339 timestamp | Start of the window; defaults to one hour before end_time |
end_time | RFC3339 timestamp | End of the window; defaults to the current time |
metrics | comma-separated string | Canonical metric names from the catalog |
step_seconds | integer | Requested bucket width; the server uses at least 15 seconds and may increase it to honor max_points |
statistic | string | auto, average, minimum, maximum, last, or rate; auto averages gauges and rates counters |
max_points | integer | Maximum points per series (default: 240, max: 1000) |
The built-in catalog is intentionally bounded:
| Metric | Kind | Unit | Meaning |
|---|---|---|---|
sandbox.cpu.utilization | gauge | ratio | CPU usage divided by the configured CPU limit |
sandbox.cpu.usage | gauge | cores | CPU cores currently used |
sandbox.cpu.time | counter | seconds | Cumulative CPU time; auto returns its rate in cores |
sandbox.cpu.limit | gauge | cores | Configured CPU limit |
sandbox.memory.usage | gauge | bytes | Cgroup memory usage |
sandbox.memory.working_set | gauge | bytes | Memory working set |
sandbox.memory.available | gauge | bytes | Memory available within the sandbox limit |
sandbox.memory.limit | gauge | bytes | Configured memory limit |
sandbox.memory.utilization | gauge | ratio | Working set divided by the configured memory limit |
sandbox.network.io | counter | bytes | Receive or transmit bytes, selected by the direction dimension; auto returns bytes per second |
sandbox.network.errors | counter | count | Receive or transmit errors, selected by the direction dimension; auto returns errors per second |
sandbox.process.count | gauge | count | Current sandbox process count |
sandbox.rootfs.writable.usage | gauge | bytes | Main runtime container writable-layer usage when the CRI reports it |
sandbox.rootfs.writable.inodes | gauge | count | Main runtime container writable-layer inodes when the CRI reports them |
Each series contains zero or more continuous segments. Runtime resets and collection gaps start a new segment, so clients must not connect lines across segments. The response also reports typed gaps, freshness relative to the requested end_time, the effective step_seconds, and whether the result is partial. Missing or unsupported observations are omitted and never synthesized as zero.
typescriptimport { Client, SandboxRuntimeMetricName } from "sandbox0"; const client = new Client({ token: process.env.SANDBOX0_TOKEN! }); const metrics = await client.sandbox("sb_abc123").getMetrics({ startTime: new Date(Date.now() - 60 * 60 * 1000), metrics: [ SandboxRuntimeMetricName.SandboxCpuUtilization, SandboxRuntimeMetricName.SandboxMemoryWorkingSet, SandboxRuntimeMetricName.SandboxNetworkIo, ], maxPoints: 120, });
Logs and audit events#
The upgraded /observability/events endpoint is the only public audit-event read API. It returns 403 feature_not_licensed unless audit is explicitly enabled and cluster-gateway has a valid enterprise license containing sandbox_audit. The caller also needs the independent sandboxaudit:read permission.
| Parameter | Type | Description |
|---|---|---|
start_time | RFC3339 timestamp | Include events that occurred at or after this time |
end_time | RFC3339 timestamp | Include events that occurred at or before this time |
limit | integer | Maximum events to return (default: 100, max: 1000); ignored in exact event_id mode, which returns at most two payload variants |
cursor | string | Opaque cursor from a previous response; in watch mode, use the cursor from a watermark line |
watch | boolean | Stream matching records as application/x-ndjson until the client disconnects |
context_id | string | Log context filter |
stream | string | Log stream filter: stdout, stderr, or pty |
source | string | Trusted event producer, including cluster_gateway or netd |
event_type | string | Audit category such as api_access or network_audit |
outcome | string | completed, denied, error, succeeded, failed, accepted, or unknown |
actor_kind | string | human, api_key, service, or sandbox_workload and other trusted actor kinds |
actor_id | string | Trusted user, API key, service, or workload identifier |
action | string | Stable action such as sandbox.pause, process.exec, file.write, or network.connect |
resource_type | string | Resource category such as sandbox or sandbox_network |
operation_id | string | Correlates the attempt and result of one operation |
event_id | UUID | Exact lookup mode for one stable audit ID; cannot be combined with time, cursor, watch, or other event filters |
Every audit fact includes a stable event_id, schema version, trusted actor, action, resource, producer, operation correlation, outcome, and bounded request metadata. Pagination cursors and watch watermarks are response transport metadata and are not repeated inside each signed event. integrity contains the canonical SHA-256 payload digest, Ed25519 signature, signing key ID, and query-time signature_status. List and watch responses recompute each returned row's digest and verify its signature. An exact event_id lookup fetches enough rows to set event_id_conflict=true when the same stable ID appears with multiple payload hashes; this marker does not overwrite signature_status, so callers can distinguish payload collisions from signature failures.
Gateway operations produce an attempt before authorization or execution and a result after either the downstream response or a failed admission decision. ClickHouse is the only audit read source: the fsync-backed delivery buffer holds pending events for retry, but list and watch reads cannot see an event until ClickHouse acknowledges it. With the default durable_async mode, non-mutating and public-exposure operations may proceed after local durable enqueue, and healthy canonical visibility is normally delayed by the asynchronous delivery path and its default one-second flush/replay cycles. canonical_sync instead requires ClickHouse acknowledgement before admission and adds that round trip to the critical path.
State-changing Sandbox API operations are always strict, regardless of the configured delivery mode. Cluster-gateway requires ClickHouse to acknowledge the attempt before execution, buffers the HTTP response, and requires ClickHouse acknowledgement for the signed result before returning that response. If canonical admission fails after the attempt is durably buffered, the downstream operation is not executed and cluster-gateway attempts to append a correlated signed failed result with HTTP 503, execution_started=false, operation_executed=false, failure_stage=canonical_audit_admission, and failure_code=canonical_ack_unavailable for replay. If terminal-result custody also fails, the response reports audit_result=unrecorded; the orphan attempt is an audit-completeness incident, not evidence that the operation ran. If a result from an operation that did execute is only buffered for retry, the caller receives 503 rather than a false success. A 5xx or panic result after execution is recorded as unknown, because the downstream operation may have completed even if the gateway did not receive a definitive response.
Network allow decisions are fsynced to a node-local spool before netd opens the upstream connection. In durable_async, canonical delivery follows asynchronously; in canonical_sync, ClickHouse must acknowledge the allow attempt first, including for server-first SSH probes. A ClickHouse outage therefore delays audit-query visibility in durable_async but denies new flows in canonical_sync; an unwritable local buffer also fails closed unless synchronous canonical fallback succeeds. The result is appended when the flow closes. Consequently a long-lived keep-alive connection has one allow attempt and one eventual flow result; parsed HTTP or MCP operations may appear inside its bounded protocol details rather than as separate connections. Raw proxy and credential-resolution error strings remain local diagnostics and are not copied into the canonical ledger because they may contain upstream or credential details.
Set watch=true to stream matching log or event records as NDJSON in ingestion order. Without cursor or start_time, a watch stream starts at request time and does not replay historical rows. When watch=true, end_time is not supported. A watch stream emits event or log lines, watermark lines with a resume cursor, periodic heartbeat lines, and an error line before termination if the backend fails after the stream starts. Runtime metrics are queried as bounded time series and do not expose a watch mode.
ClickHouse-backed deployments store canonical audit facts in sandbox_audit_events; logs and wide runtime samples remain separate historical projections with independent retention TTLs. Cluster-gateway records authenticated Sandbox API access (including regional detail requests), netd records network attempts and results, manager publishes logs, and node-local ctld instances collect sandbox-wide CRI runtime samples every 15 seconds with jitter.
Historical data query endpoints return 503 when the backend is disabled or unavailable. The static metric catalog remains available so clients can build selectors without probing storage.
Metering windows are separate usage-ledger records for quota, showback, and billing export. Audit does not duplicate metering state even when both use the same region ClickHouse component. process.* and file.* facts currently prove authenticated gateway requests and their HTTP result; they do not observe every process effect, SSH/SFTP command, arbitrary POSIX write, direct SandboxVolume operation, or manager-initiated lifecycle reconciliation. Public exposure streams and WebSockets have a durably captured open attempt, but an abrupt node/process loss can prevent their eventual close result. Cluster-gateway pending audit events use a PVC that production deployments must back with reattachable durable storage; node-local netd flow buffers survive same-node Pod restarts but not permanent node loss. Deployments that must prove completeness across node loss should preserve audit signing public keys and export signed facts/checkpoints to independently controlled immutable storage. Per-row signatures detect modified returned facts; by themselves they do not prove that a privileged ClickHouse administrator never deleted a row.
List Sandboxes#
List all sandboxes with optional filters.
/api/v1/sandboxes
Query Parameters#
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status (starting, running, failed, completed, terminating) |
template_id | string | Filter by template ID |
paused | boolean | Filter by paused state independently of status |
limit | integer | Max results per page (default: 50, max: 200) |
offset | integer | Pagination offset (default: 0) |
golimit := 10 sandboxes, err := client.ListSandboxes(ctx, &sandbox0.ListSandboxesOptions{ Status: "running", TemplateID: "default", Limit: &limit, }) if err != nil { log.Fatal(err) } for _, sb := range sandboxes.Sandboxes { fmt.Printf("- %s (status: %s)\n", sb.ID, sb.Status) }
Update Sandbox#
Update sandbox configuration at runtime without restarting the sandbox.
/api/v1/sandboxes/{id}
Updatable Fields#
Only the following fields can be updated at runtime:
| Field | Type | Description |
|---|---|---|
ttl | integer | Time to live in seconds (soft limit) |
hard_ttl | integer | Hard sandbox lifetime in seconds |
resources | object | Runtime resource override. Only resources.memory is accepted. |
network | object | Network policy configuration |
auto_resume | boolean | Auto-resume when accessed |
services | array | Sandbox Services for public HTTP routes, including Sandbox Functions |
env_vars and webhook cannot be updated at runtime. These fields only take effect when the sandbox is created. To use different sandbox-level environment variables, create a new sandbox.
go_, err = client.UpdateSandboxMemory(ctx, sandbox.ID, "2Gi") if err != nil { log.Fatal(err) }
Pause And Resume#
Pause and resume is covered in a dedicated page because it affects TTL, auto_resume, service routes, SSH, and webhook behavior.
See Pause And Resume for explicit pause, state inspection, resume, and auto-resume behavior.
See Snapshot And Restore for named rootfs snapshots, restore, and fork operations. Snapshot and fork accept a running or paused source sandbox; restore requires a paused target sandbox.
Refresh Sandbox TTL#
Extend the sandbox time-to-live. This resets both ttl and hard_ttl (if configured) from the current time while the sandbox has a runtime.
/api/v1/sandboxes/{id}/refresh
Request Body#
| Field | Type | Description |
|---|---|---|
duration | integer | Duration to extend TTL in seconds (optional, defaults to original TTL) |
If duration is not specified, both ttl and hard_ttl are reset to their original configured values. Use this to keep a sandbox alive as long as the user is actively using it.
go// Refresh with default duration (original TTL) resp, err := client.RefreshSandbox(ctx, sandbox.ID, nil) if err != nil { log.Fatal(err) } fmt.Printf("New expires at: %s\n", resp.ExpiresAt) // Refresh with custom duration (e.g., 1 minute) resp, err = client.RefreshSandbox(ctx, sandbox.ID, &apispec.SandboxRefreshRequest{ Duration: apispec.NewOptInt32(60), }) if err != nil { log.Fatal(err) } fmt.Printf("New expires at: %s\n", resp.ExpiresAt)
Delete Sandbox#
Terminate and delete a sandbox. This action is irreversible.
/api/v1/sandboxes/{id}
go_, err = client.DeleteSandbox(ctx, sandbox.ID) if err != nil { log.Fatal(err) } fmt.Println("Sandbox deleted")
Next Steps#
Pause And Resume
Control checkpointed pause and resume behavior before wiring long-lived workflows.
Snapshot And Restore
Create named rootfs snapshots, restore paused sandboxes, and fork sandbox state.
Contexts
Run REPL and command contexts inside a sandbox and stream process output.
Supervised Sessions
Run reconnect-safe processes with stable identities, attempts, and retained events.