States: creating -> running -> suspended -> terminated | failed. Every terminal state names its cause in a field, not in prose. You never parse an error string to learn how a sandbox died.
| user_exit | the workload exited on its own; exit code attached |
| user_destroy | you called destroy |
| user_oom | the workload exceeded its memory request |
| user_timeout | an exec exceeded its timeout |
| user_image_build_failed | your image/template could not build; build log attached |
| policy_killed_ttl | expires_at passed; enforced server-side with teardown proof |
| provider_capacity | we lacked capacity. Typed 409, reservable, never billed |
| provider_infra | our fault. Never billed, by contract |
from numinous import NuminousError
try:
sb = nc.sandboxes.create(...)
except NuminousError as e:
e.cause # "provider_capacity"
e.is_provider_fault # True: our problem, your credit
e.retryable # True (or reserve ahead: nc.capacity.reserve(...))ttl_seconds at create becomes an absolute expires_at. The platform enforces it within seconds. There is no idle timer. A sandbox mid-upload or mid-build never gets reaped early, and an orphan never outlives its deadline. Enforcement produces policy_killed_ttl plus a teardown proof.
# a retried worker re-sends the same launch_token:
a = nc.sandboxes.create(..., launch_token="job-41-attempt-2")
b = nc.sandboxes.create(..., launch_token="job-41-attempt-2")
assert a["id"] == b["id"] # replay, not a second sandbox
assert b["idempotent_replay"]out = nc.sandboxes.destroy(sb["id"])
out["teardown_proof"]
# {"verified_absent": true, "method": "...", "at": "..."}
# issued only after the data plane confirms absence.
# you never write a reaper.Export runs on the host, outside the sandbox. It works while the sandbox runs, after it exits, after an OOM, after a TTL kill. A GB-scale artifact upload cannot race its own teardown, because the sandbox being gone does not matter.
# sandbox already terminated? doesn't matter:
nc.sandboxes.export(sb["id"], "/logs", to="s3://bucket/run-41/")Suspend manually, or opt in to idle auto-suspend. Agents spend most of their wall clock waiting on model output; with auto-suspend the sandbox sleeps between exec calls, bills storage instead of compute, and wakes automatically on the next exec or file operation.
nc.sandboxes.suspend(sb["id"]) # scale to zero: storage cost only
nc.sandboxes.resume(sb["id"]) # same machine, processes intact
# opt-in sleep between agent steps:
sb = nc.sandboxes.create(template_id=tpl, vcpu=4, mem_gib=8,
auto_suspend_idle_seconds=60)
nc.sandboxes.exec(sb["id"], "make test") # wakes it if asleepFreeze a running sandbox into a new template while it keeps running, then boot any number of copies from that point. The per-episode reset primitive for RL rollouts.
ck = nc.sandboxes.checkpoint(sb["id"], name="after-setup")
fork = nc.sandboxes.create(template_id=ck["template_id"], vcpu=2, mem_gib=4)Running sandboxes are sampled about every 30 seconds. The series is yours to query, and it is the measured basis for active-usage pricing.
m = nc.sandboxes.metrics(sb["id"])
m["avg_cpu_pct"], m["samples"][-1]