# nanoVM for coding agents
Source: https://lithos-box.mintlify.app/coding-agents
The shortest path from an API key to running code in an isolated microVM — skill install, SDK patterns, MCP tools.
**Initial pilot:** use the invited organization, at most 20 sandboxes, and at most
4 vCPU / 8 GiB per sandbox. GPU and live migration are unavailable. A host-loss
recovery creates a new sandbox from a durable checkpoint; it is not automatic
same-ID failover. See [launch and support](/launch).
You are an agent that has (or is about to get) a nanoVM API key. This page is
the shortest path from that key to running code in an isolated microVM, with
the checks that tell you each step worked. The Markdown of any page on this
site is available by appending `.md` to its URL; the whole site is at
[/llms.txt](/llms.txt).
## TL;DR — two commands
```bash theme={null}
python3 -m venv .venv && . .venv/bin/activate
pip install "$(curl -fsSL https://console.nanovm.dev.lithosai.cloud/sdk/LATEST)"
python -m nanovm skill install
```
The venv line is there for a reason: Homebrew and Debian Pythons refuse `pip install` into the
system interpreter (`externally-managed-environment`), and ship `python3`/`pip3` only. `/sdk/LATEST`
is a one-line file naming the current wheel URL, so nothing here pins a version. Do not
`pip install nanovm` from PyPI — that name belongs to an unrelated project.
Then tell your agent: **"set up nanoVM"**. The skill it just loaded carries the
setup procedure: it asks you for an API key (console → API Keys), exports it,
runs the 30-second self-test and reports `VERDICT: PASS`. From there it uses
the SDK for real work. That's the whole onboarding; everything below is
detail.
* `skill install` writes `./.claude/skills/nanovm/` — project scope, this directory only (it
prints the absolute path and says so). `--dir ~/.claude/skills` installs for every project;
`--cursor` adds a Cursor rule. `python -m nanovm skill show` prints the skill.
* Prefer tools over code, or your agent has no shell (Claude Desktop, chat
hosts)? `pip install "mcp>=2,<3"` and
`claude mcp add nanovm --transport stdio -e NANOVM_TOKEN=lith_sk_... -- python -m lithosbox mcp`.
* With a `lith_sk_` key only `NANOVM_TOKEN` is needed; the SDK defaults to the key
edge `https://sk.nanovm.dev.lithosai.cloud`.
## 0. What you get
A **sandbox** = a microVM you fully control: run commands, write files, start
servers, expose a port on a public HTTPS URL, checkpoint (snapshot) and
restore or fork it. Boots from any OCI image; the default sandbox has Python
3.12, Node 22 and the usual data-science stack, 2 vCPU / 1 GiB, and starts in
well under a second. Sandboxes are isolated per organization.
## 1. Prove the environment (30 seconds)
```bash theme={null}
export NANOVM_TOKEN=lith_sk_... # from the console's API Keys page
python3 -m venv .venv && . .venv/bin/activate
pip install "$(curl -fsSL https://console.nanovm.dev.lithosai.cloud/sdk/LATEST)"
python -m lithosbox quickstart
```
Expected: one `ok` line per step (auth, create, exec, stdin, session, tools,
snapshot, wait-until-durable, restore, delete) and `VERDICT: PASS`. A `FAIL` line names the step
and the exception; the text is enough to act on (bad key → `AuthError`, image
still building → inspect its template status before another create). `--keep` leaves the sandbox running and
prints its `vm_id`; `--image X` tests a specific image; `--json` appends a
machine-readable summary.
## 2. Use it from Python (preferred)
```python theme={null}
import os
from lithosbox import LithosBox
nv = LithosBox() # reads NANOVM_API_URL / NANOVM_TOKEN; ONE per process
sb = nv.sandboxes.create() # or image="node:22", template="my-env", snapshot_id=...
r = sb.run("python3 -c 'print(6*7)'") # runs to completion; r.exit_code, r.stdout, r.stderr
sb.files.write("/work/data.csv", open("data.csv", "rb").read()) # files.read/write/ls/exists; ≤ 4 MiB per write
with sb.session() as sh: # persistent shell: cwd/env survive between run() calls
sh.run("cd /work && pip install -q pandas")
print(sh.run("python3 -c 'import pandas; print(pandas.__version__)'").stdout)
sb.run("python3 -m http.server 8080", background=True) # something that must outlive the call
url = sb.expose(8080).url # public HTTPS URL to that port
snap = sb.snapshot(wait_durable=True) # checkpoint; durable (survives its node) once .durable — seconds
sb.delete()
```
Rules that save you time:
* **One `LithosBox()` per process.** Its connections are reused; a new client per call costs \~200 ms each.
* **`run`/`exec` are scoped**: they return when the command *and everything it spawned* exit. Use `background=True` for servers, `session()` for multi-step shell work, and the argv form `exec(["cmd", arg])` whenever the command carries untrusted input.
* **Bulk data goes through `stdin`**, not argv (argv strings are capped at 128 KiB by the guest kernel; stdin at 4 MiB per call — loop for more).
* **First use of an image builds a template once** (seconds to tens of minutes depending on image size and cache state). `create()` waits for it by default; the wait is per image, not per sandbox, and public images are shared by every organization.
* **Delete what you create.** `with nv.sandboxes.create() as sb:` deletes on exit. Snapshots outlive their sandbox.
* **Long jobs**: one request has \~2 minutes at the edge (`create` gets 5). Start long work with `background=True` or in a session, poll a file or a log with later calls.
## 3. Claude Code skill (default)
```bash theme={null}
npx skills add https://docs.nanovm.dev.lithosai.cloud
```
Or from the SDK: `python -m nanovm skill install`. The skill teaches the agent
the SDK patterns above and when to reach for a sandbox (untrusted code, heavy
dependencies, a clean environment per task, parallel experiments via `fork`).
## 4. Use it as MCP tools (no shell / interactive)
```bash theme={null}
pip install "$(curl -fsSL https://console.nanovm.dev.lithosai.cloud/sdk/LATEST)" "mcp>=2,<3"
python -m nanovm mcp --check # prints the tool list
# Claude Code:
claude mcp add nanovm --transport stdio -e NANOVM_API_URL=$NANOVM_API_URL -e NANOVM_TOKEN=$NANOVM_TOKEN -- python -m lithosbox mcp
# Any MCP host (stdio): {"command": "python", "args": ["-m", "nanovm.mcp_server"], "env": {"NANOVM_API_URL": "...", "NANOVM_TOKEN": "..."}}
```
Tools: `create_sandbox`, `exec`, `shell` (persistent session), `exec_background`,
`write_file`, `read_file`, `expose_port`, `snapshot`, `branch`, `list_sandboxes`,
`delete_sandbox`, and the lifecycle verbs `pause`, `resume`, `sleep`, `stop`, `start`, `reboot`
plus `list_snapshots` / `delete_snapshot`. Ids are plain strings you carry between calls.
To query **these docs** as an MCP tool instead, connect this site's own server:
`claude mcp add --transport http nanovm-docs https://docs.nanovm.dev.lithosai.cloud/mcp`.
## 5. Raw HTTP, if you must
`POST /vms` `{}` → `{"vm_id": …}`; `POST /vms/{id}/exec` `{"args": [...], "stdin_b64": "..."}` →
`{"exit_code", "stdout", "stderr"}`; `DELETE /vms/{id}`. Header
`Authorization: Bearer $NANOVM_TOKEN`. `503` with `template_id` = first use of
an image: poll `GET /templates/{template_id}` until `status` is `ready`, then
retry. `503` whose body says `connection error` = a control-plane component
restarting: retry in a few seconds. Full route table: [HTTP API](/reference/http-api).
## 6. Verify before you report success
* `sb.exec(["true"]).exit_code == 0` after any wake/start (sleep and `snapshot(leave_paused=True)` wake transparently; `stop()` needs `start()`).
* Read back files you wrote (`read_file` / `exec(["cat", path])`) — the write path is a shell redirect and its exit code is the truth.
* Snapshots are the only artifact that outlives a sandbox. One is restorable the moment it is taken
and durable (survives its node; exportable) a few seconds later when `.durable` is True —
`snapshot(wait_durable=True)` waits. A deleted sandbox's disk is gone.
## Long-running work and idle sleep
The platform never auto-sleeps a sandbox that is doing work (a background process, guest CPU, live
connections, or an exposed port). You do not need to poll or "keep-alive" a sandbox to keep a job,
server or in-sandbox agent running. Prefer `run(..., background=True)` for anything that outlives the call;
`exec()` returns as soon as the foreground command exits even if it left daemons behind.
# How it thinks
Source: https://lithos-box.mintlify.app/concepts
Sandboxes, images and templates, runtimes, shapes, rest states, and the durability rules.
**Sandbox.** A microVM with its own kernel, filesystem and network, identified
by `vm_id` (UUID). Created from an image, a template, or a snapshot.
**Image / template.** Any OCI reference. A template is that image booted once
and checkpointed; creates restore the checkpoint instead of booting. First use
of an (image, shape) registers a build — `503 template building` until ready.
**Runtime.** `container` (default): the image's entrypoint is PID 1 with a
kernel of its own. `vm`: the image *is* the machine — systemd, Docker daemons
(experimental at launch).
**Shape.** `cpus` (default 2, launch max 4), `memory_mb` (default 1024,
128–8192), `disable_internet` (no egress). Snapshot/template creates take the
source's shape. Every sandbox gets a fixed 16 GiB sparse writable disk.
## Rest states
Cheapest to most durable (p50, measured 2026-09-04, default sandbox):
| call | cost | vCPUs | wake | survives node loss | use for |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ----------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `snapshot(leave_paused=True)` | \~50 ms (+ \~170 ms to restore) | paused | transparent, on the next call | the snapshot does, once `durable` (seconds) | checkpoint, then idle across a model call |
| `pause` / `resume` | \~32 ms each | paused | explicit `resume` | no | a deliberate hold you control (advanced) |
| `sleep` | \~30 ms | paused | transparent, on the next call | no | idle reclamation |
| `fork` / `fork(n)` | \~55–75 ms one child; `n=4` ≈ 120–150 ms for all four (one checkpoint, parallel restores, one shared layer) | running | — | no | fork an experiment at a decision point; `n` capped at 4 at launch |
| `archive` / `unarchive` | 2–7 s / \~1–2 s | freed; memory archived to object storage | explicit `unarchive` | **yes** | done for a while; release running resources |
| `reboot` | \~1–2 s | running | — | no (disk kept; memory, processes, sessions and exposed listeners lost) | recover a wedged guest (fork bomb, hung kernel) without losing files |
`archive` is the one operation whose cost varies with what is in the sandbox
(it packs and uploads writable memory and disk); it is a teardown-and-keep, not
a suspend. **When in doubt, do nothing** — autosleep covers idle; `archive()`
covers "done for days".
## Durability rules
Survives delete: snapshots, exports. Survives node loss:
archived sandboxes, durable snapshots. Survives reboot: the writable disk.
Everything else is as durable as the node under it.
# Work with files
Source: https://lithos-box.mintlify.app/guides/files
Read, write, list, and move files in a sandbox — and when to move bulk data another way.
```python theme={null}
sb.files.write("/app/config.json", data) # creates parent dirs
text = sb.files.read("/app/out.txt")
blob = sb.files.read_bytes("/app/plot.png")
names = sb.files.list("/app")
sb.files.exists("/app/done") # -> bool
```
One noun for all file work: `write / read / read_bytes / list / remove / rename / mkdir / exists`.
`remove` is recursive; `write` creates parent directories.
## Sharp edges
4 MiB per `write` call (chunk bigger payloads), 16 MiB per read
(the exec output cap; `read_bytes` is base64 under the hood). For bulk data,
download inside the sandbox (`sb.run("curl -O …")`) instead of pushing it
through the API.
# Use your own image
Source: https://lithos-box.mintlify.app/guides/images
Any OCI image, private registries, pre-built templates, and the full-VM runtime.
```python theme={null}
sb = nv.sandboxes.create(image="ghcr.io/org/app:tag", cpus=4, memory_mb=4096)
nv.registries.put(host="ghcr.io", username="me", password="") # private pulls, stored once
t = nv.templates.build("python:3.12-slim", cpus=2, memory_mb=1024) # pre-build to skip first-use wait
sb = nv.sandboxes.create(image="docker:28-dind", runtime="vm") # the image IS the machine (experimental)
```
The **first** create of an image at a shape builds a template once (seconds for
small images, minutes for huge ones); the SDK waits by default
(`wait_for_template=False` raises `TemplateBuildingError` to poll yourself).
After that it is fast for everyone (public images) or your org (private ones).
## Sharp edges
Templates are per `(image, cpus, memory_mb, runtime)`. In the
`vm` runtime don't start a second dockerd on dind images — the image's own
daemon already holds the socket. Omitting the image gives the **default
sandbox** (Python 3.12, Node 22, common data-science stack, 2 vCPU/1 GiB,
\~50–100 ms from a warm pool); it sets `PIP_NO_CACHE_DIR=1`, so repeated
installs in one sandbox want `PIP_NO_CACHE_DIR=0 pip install …`.
# Control cost
Source: https://lithos-box.mintlify.app/guides/lifecycle
Idle sleep is automatic; archive when you're done for days. Usually the right move is nothing.
You mostly don't have to: **a sandbox idle by every measure (no work, no live
connections, no API calls for \~60 s) is slept automatically and costs nothing;
the next call wakes it transparently.** A running server or background job
keeps it awake indefinitely. Beyond that:
```python theme={null}
sb.sleep() # nap right now; next call wakes it (ms). Not durable.
sb.archive() # put it away: memory+disk to object storage, host freed,
# billing stopped. Survives node loss. Seconds.
sb.unarchive() # bring it back (seconds). info.state reads "archived" while parked.
```
Rule of thumb: do nothing for idle gaps; `archive()` when you're done for days.
(`stop()`/`start()` are the old names for archive/unarchive and keep working.)
For the full menu of rest states with measured costs — pause/resume, sleep,
`snapshot(leave_paused=True)`, archive, reboot — see [How it thinks](/concepts#rest-states).
## Sharp edges
Paused and slept sandboxes keep RAM resident and **hold a slot against the
20-sandbox cap**; archived ones don't. `archive()` composes from any rest
state. Data not in a snapshot, an archived sandbox, or an export is gone at
delete.
# See what's happening
Source: https://lithos-box.mintlify.app/guides/observability
Every sandbox keeps its own paper trail — events, console logs, utilization metrics, and an org audit log. No setup.
Every sandbox keeps its own paper trail — no setup, nothing to instrument:
```python theme={null}
sb.events() # lifecycle timeline, newest first: created, slept,
# archived, rebooted, forked… — even after delete.
sb.logs() # tail of the serial console (boot output, kernel
# messages). Works on paused/slept without waking.
sb.metrics() # 30 s utilization samples (~2 h): CPU, memory, net.
nv.audit() # org-wide: who changed what, with what outcome.
```
`sb.metrics()` with no arguments returns live 30-second host samples for the
last \~2 hours. With `hours=` or `start=`/`end=` (RFC3339, up to 15 days back)
it returns per-minute ledger history instead — including for archived and
deleted sandboxes; a point with `measured=false` is a gap, never a zero.
The console shows the same data: each sandbox has a detail page (timeline,
log tail, utilization graphs), and the org has Usage and Audit pages.
## Sharp edges
An archived sandbox has no live console or metrics — its timeline still
answers. Metrics are measured on the host (the guest is never touched), so a
slept sandbox shows flat CPU rather than waking to be asked.
# Run commands
Source: https://lithos-box.mintlify.app/guides/run-commands
Shell strings, argv form, persistent sessions, and background work — plus the real limits, exactly where they bite.
```python theme={null}
r = sb.run("pytest -q | tail -1") # shell string: pipes, globs, redirects
r.exit_code; r.stdout; r.stderr # runs to completion, children included
sb.run("npm run dev", background=True) # something that OUTLIVES the call
r = sb.exec(["python3", "-c", code]) # argv form: no shell, no injection
with sb.session() as sh: # cwd/env persist across calls
sh.run("cd /work && . .venv/bin/activate")
sh.run("pip install -q -r requirements.txt")
```
Use `run()` for everyday work, the argv form (`run(["cmd", arg])`) the moment
any part of the command comes from untrusted input, `session()` when state must
carry over, and `background=True` for servers and workers (the idle policy sees
them and keeps the sandbox awake while they live).
## Sessions vs. background
They answer different questions. A **session** keeps *state* between commands:
one shell process stays alive across `.run()` calls, so `cd`, `export`, an
activated venv, and shell functions carry over — exactly like a terminal window
you keep typing into. Filesystem changes persist either way; it is only
process-local state (cwd, environment, shell internals) that dies with each
plain `run()`'s fresh shell. **`background=True`** changes one process's
*lifetime*: the call returns immediately and the process outlives it. They
compose — launch a background job from inside a session and keep working.
## Sharp edges
A synchronous call has \~110 s at the edge — longer work belongs in
`background=True` or a session, collected later. One argv string caps at
128 KiB (kernel limit). stdout/stderr are UTF-8 text capped at 16 MiB per
stream (`ExecResult.truncated` + an inline notice mark a cut) — big or binary
output goes to a file. Background execs return no pid — record one yourself
(`cmd & echo $! > pid`) and redirect output to a file or it is discarded.
# Save, restore, fork
Source: https://lithos-box.mintlify.app/guides/save-restore
Checkpoint memory + disk, restore into new sandboxes, and fork live copies for parallel work.
## Save and restore
```python theme={null}
snap = sb.snapshot() # checkpoint memory + disk, VM keeps running
snap = sb.snapshot(wait_durable=True) # block until it also survives node loss (seconds)
sb2 = nv.snapshots.restore(snap) # NEW sandbox at exactly that point (~170 ms)
```
A snapshot is restorable the moment the call returns and **durable** (survives
the node it was born on) seconds later; `wait_durable=True` blocks for that.
`snapshot(leave_paused=True)` parks the vCPUs right after the checkpoint — the
next call wakes it in \~10–20 ms — the right shape for a snapshot-every-turn
agent loop that idles across model calls.
**Sharp edges:** restoring takes the snapshot's shape (cpus/memory are not
overridable at restore). Deleting a sandbox does not delete its snapshots.
Export (downloading a snapshot's bytes out of the platform) is experimental —
see the [HTTP API reference](/reference/http-api#snapshots-and-exports).
## Fork for parallel work
```python theme={null}
kids = sb.fork(4) # 4 running siblings from ONE point in time (~120–150 ms total)
kid = sb.fork() # one
```
A fork is a live checkpoint + restore: children resume the parent's exact
processes and open files. Up to 4 per call at launch. Use one `fork(n)` call,
not n `fork()` calls — the fan-out takes a single checkpoint of the parent.
**Sharp edges:** external network connections are duplicated into every child
(each side thinks it owns the socket) — close connections you don't want
forked, or fork between commands, not mid-flight.
# Serve something
Source: https://lithos-box.mintlify.app/guides/serve
Expose a port on a public HTTPS URL.
```python theme={null}
sb.run("python -m http.server 8000", background=True)
url = sb.expose(8000).url # public HTTPS, live immediately
```
`sb.ingress()` lists a sandbox's exposures; `nv.ingress.delete(id)` removes one.
Anyone with the URL can reach the service — your application must provide any
required authentication.
## Sharp edges
The edge speaks HTTP/1.1 and h2 only (HTTP/1.0 gets `426`).
Across `archive()`/`unarchive()` the URL can 404 for a moment while the route
follows the new placement, and processes do not survive archiving — restart
your server after `unarchive()`. A deleted sandbox's URL can take \~30 s to fail
fast. Exposing an archived sandbox is allowed; the mapping goes live on
`unarchive()`.
# nanoVM
Source: https://lithos-box.mintlify.app/index
Run your code in fast, isolated microVM sandboxes behind a plain HTTP/JSON API and a Python SDK.
nanoVM runs your code in fast, isolated microVMs ("sandboxes") behind a plain
HTTP/JSON API and a Python SDK. Everything a first-day user needs is on this
page; everything else is one deliberate step deeper.
**Initial pilot:** access by invitation, at most 20 sandboxes per organization and
4 vCPU / 8 GiB per sandbox. Read the [launch guide](/launch) for first-run
instructions, cold-image expectations, recovery behavior, support, and pilot terms.
The timings in these docs are historical development measurements, not service guarantees.
## Start
Log in at `https://console.nanovm.dev.lithosai.cloud` (your first login creates
your organization), create an API key on the **API Keys** page (shown once,
starts with `lith_sk_`), then:
```bash theme={null}
python3 -m venv .venv && . .venv/bin/activate
python -m pip install "$(curl -fsS https://console.nanovm.dev.lithosai.cloud/sdk/LATEST)"
export NANOVM_API_URL=https://sk.nanovm.dev.lithosai.cloud NANOVM_TOKEN=lith_sk_...
```
The whole platform, in ten lines:
```python theme={null}
from lithosbox import LithosBox
nv = LithosBox() # reads NANOVM_* env; ONE per process
sb = nv.sandboxes.create(image="python:3.12-slim") # or create() for the default sandbox
sb.run("pip install -q flask") # shell semantics, runs to completion
sb.files.write("/app/app.py", code) # files in, files out
print(sb.run("python /app/app.py --check").stdout)
sb.run("flask --app /app/app run -p 8000", background=True)
print(sb.expose(8000).url) # public HTTPS URL
snap = sb.snapshot() # save point (memory + disk)
sb2 = nv.snapshots.restore(snap) # load it into a NEW sandbox
sb.delete(); sb2.delete()
```
Never `pip install nanovm`/`lithosbox` from PyPI — those names are not ours.
Keep one client per process: its connections are reused, which is most of the
difference between a 10 ms create and a 200 ms one. `with nv.sandboxes.create() as sb:`
deletes on exit (best effort — the downloadable first-run example shows explicit
delete-and-confirm). Complete first run with curl instead:
`curl -fsS https://console.nanovm.dev.lithosai.cloud/examples/first-sandbox.sh | bash`.
**Coding agents:** `python -m lithosbox skill install`, then tell the agent
"set up nanoVM" — details in [For coding agents](/coding-agents).
## Where to go next
Shell strings, argv form, sessions, and background work.
Checkpoint a sandbox and fork live copies for parallel work.
The complete SDK surface — one operation per job, arguments select behavior.
Every route, the sandbox object, and every status code.
## Getting help
Report anything odd — a stuck sandbox, a confusing error, a slow create — with
the `vm_id` and the time; every request is traceable on our side. See the
[launch guide](/launch) for support channels and pilot terms.
# Sandbox launch guide
Source: https://lithos-box.mintlify.app/launch
First run, pilot limits, image expectations, recovery behavior, and support.
Start with the default sandbox, run one command, and confirm cleanup before moving to your own image or longer jobs. Access is by invitation.
## First run
1. Use the account setup or sign-in link in your invitation. Complete email verification if prompted. If access is still pending, contact your inviter with the email address you used.
2. Check the organization name in the console. API keys and sandboxes belong to that organization.
3. Open **API Keys**, create a key, and copy its one-time value. Keep it private.
4. Open **Sandboxes → Get started → Python** and run the block in a terminal with Bash, curl, and Python 3.10 or newer. The example asks for your key, prints the sandbox ID and `42`, then deletes the sandbox. The curl tab offers the same sequence without installing the SDK.
5. Confirm the sandbox was deleted. Then use the [introduction](/) to run your application.
The first-run examples create a default sandbox only. The Python example requires a virtual environment; both examples clean up on normal completion and execution errors. If a request or cleanup cannot be confirmed, inspect the printed sandbox ID in the console before trying again.
If virtual-environment setup reports that `ensurepip` is unavailable, install the
Python venv package for your Python version (usually `python3-venv` on Debian/Ubuntu),
or use a virtual environment you already maintain. Stop at a failed setup step;
do not continue by installing the SDK into system Python.
## Launch limits
| Resource or behavior | Initial pilot |
| ------------------------ | ---------------------------------------------------------------------------------------------------------- |
| Concurrent sandboxes | Up to 20 per invited organization; running, paused, and sleeping guests count toward the resident limit |
| Sandbox shape | Up to 4 vCPU and 8 GiB RAM; default 2 vCPU / 1 GiB |
| Fork fanout | Up to four children per branch request, within the organization limit |
| GPU | Not available |
| Images | CPU workloads using compatible OCI images; the first use may need an image build |
| Public endpoints | Anyone with the URL can reach an exposed service; the application must provide any required authentication |
| Availability and billing | Governed by the pilot terms supplied with your invitation |
For larger jobs, contact your inviter before changing the workload assumptions. Deleting unused sandboxes releases their capacity. Stopping a sandbox archives its state; retained snapshots and archives can continue to consume storage.
## Images and waiting
A first image build, a cold restore on a host, and creation from a warm cache have different costs. Development warm timings are not cold-start guarantees. Large images can take tens of minutes to prepare.
Measured during the September 2026 qualification (typical/observed worst, not contractual guarantees): a warm create returns in well under a second; a cold restore of the largest pilot images took 60–110 seconds. Sandbox creation is allowed up to five minutes end to end (SDK 0.4.6 widens only the create call; other requests keep the 120-second default). If your own client timeout is shorter, a large-image create can time out client-side while the create still succeeds — inspect existing sandboxes before retrying.
The SDK handles template-building responses within its configured wait budget. Inspect the template's `status` and `status_detail` if progress stops. An expired client wait does not by itself prove that the underlying build failed. Repeatedly creating new requests is not a substitute for checking the existing operation.
Long-running commands belong in a session or background execution with later result collection. See [Run commands](/guides/run-commands).
## Networking
Outbound connectivity is IPv4 only; software that tries IPv6 first will log an unreachable attempt before falling back. Sandboxes share pilot egress addresses, and public package mirrors occasionally throttle or stall downloads: during qualification roughly 2% of heavy package-install runs hit a stalled or refused mirror connection that was not a platform fault. Write install steps to retry (for example `apt-get -o Acquire::Retries=3`) and prefer images that pre-install heavy dependencies over installing them at runtime.
## Recovery
| Action/state | What to expect |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Pause | Holds the guest on its current host; use Resume. It does not protect against host loss. |
| Sleep | Holds the guest on its current host; an eligible request wakes it. It does not protect against host loss. Idle guests sleep automatically, measured 60–110 seconds after the last activity (median 80 s); wake is transparent and took \~25 ms median. |
| Stop | Archives the guest and pins it stopped. Use Start explicitly. |
| Snapshot: not durable yet | A checkpoint exists locally. It still depends on its source host. |
| Snapshot: durable | A checkpoint is saved to object storage. Restoring creates a new sandbox at that checkpoint. |
| Snapshot: unknown | Durability was not reported. Confirm it before relying on host-loss recovery. |
| Interrupted | The host was lost. Contact support to recover from an available durable checkpoint; later work may be lost. |
| Delete | Removes the live sandbox. Separately retained snapshots remain subject to their own durability and retention. |
The pilot does not offer automatic same-ID continuation after host loss or live migration. Automatic archiving is not part of the pilot; explicitly Stop or Delete a guest when finished. A busy server or background job may intentionally remain awake.
Before a risky experiment, take a snapshot and wait for `durable: true` if you need recovery after host loss. Test restoring important checkpoints. An earlier snapshot cannot recover work written after it.
## Usage and pilot terms
The usage panel shows measured resource usage, not an invoice. Read the pricing or credit arrangement, pilot duration, storage retention, and end-of-pilot cleanup terms in your invitation before running sustained workloads. Ask your inviter if any of those terms are missing. Usage display and billing are different: do not infer a charge from a resource counter alone. Usage is sampled on 60-second intervals per state (running, paused, slept), so sub-minute state changes blend into the surrounding interval; network egress is not metered in the pilot.
## Support
Use the support contact or channel supplied with your invitation. If you cannot sign in, contact the person who invited you; signing in again is necessary after access has been granted. Email delivery and access approval are separate steps.
For a stuck request, slow image, missing resource, or unexpected result, include:
* Organization name, sandbox or snapshot ID, and UTC time.
* The action you attempted, the error text, and whether it still reproduces.
* SDK version and image reference, if relevant.
* A small reproduction with private data removed.
Never include API keys, passwords, cookies, signed URLs, or private registry credentials. Preserve the failing sandbox when practical so support can investigate; delete it if you need to release capacity and record its ID first.
Your invitation defines support hours and the incident-update channel. A reply window is not a guarantee of uninterrupted service. For a host-loss incident, avoid assuming repeated Start requests will recover the same guest.
# HTTP API
Source: https://lithos-box.mintlify.app/reference/http-api
Every route, the sandbox object, snapshots, exports, templates, registries, usage, and every status code.
Base URL `https://sk.nanovm.dev.lithosai.cloud`, `Authorization: Bearer `.
JSON bodies; times are `*_unix_ns`. (The archive/unarchive verbs travel as the
original `/stop` and `/start` routes.)
## Sandboxes
| Route | Body | Response |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /vms` | `image` **or** `template` (name, `name:version`, or template id) **or** `snapshot_id`; optional `cpus`, `memory_mb`, `writable_size_bytes`, `disable_internet`, `runtime` (`container`/`vm`). Empty body = default sandbox. | `201` sandbox object |
| `GET /vms` · `GET /vms/{id}` · `DELETE /vms/{id}` | — | `{"vms": […]}` · sandbox object · `204` |
| `POST /vms/{id}/exec` | `{"args": ["cmd", "…"], "stdin_b64": ""}` — or session form `{"session_id", "command", "close_session"}` | `{"exit_code", "stdout", "stderr"}`; session form: cwd/env/background processes persist per `session_id` |
| `POST /vms/{id}/pause` · `/resume` | — | `204` |
| `POST /vms/{id}/sleep` | — | `{"slept": true}`; any later call wakes it |
| `POST /vms/{id}/stop` (= archive) | — | `{"stopped": true}`; memory + disk archived durably |
| `POST /vms/{id}/start` (= unarchive) | — | `{"started": true, "node": "…"}` |
| `POST /vms/{id}/reboot` | — | updated sandbox object; keeps the disk, kills processes/sessions, new guest IP, exposed ports re-pointed; `409` on an archived sandbox (unarchive first) |
| `POST /vms/{id}/snapshot` | `{"leave_paused": false}` | `201 {"snapshot_id", "vm_id", "parent_snapshot_id", "state", "image", "created_unix_ns"}` |
| `POST /vms/{id}/branch` (= fork) | optional `{"count": N}` (2–4 at launch); a single fork may carry `child_vm_id` (client UUID; retry-safe). A fork is a live checkpoint: the child resumes the parent's running processes and open connections; forking mid-command can leave the child unhealthy for \~1–2 s — fork between calls | `201` sandbox object; with `count`, `{"vms": […]}` — N siblings from one point in time |
| `GET /vms/{id}/snapshots` | — | `{"snapshots": […]}` |
| `GET /vms/{id}/events?limit=` | — | `{"events": [{"kind", "detail", "node", "at"}, …]}` newest first — the lifecycle timeline; survives delete until retention |
| `GET /vms/{id}/logs?tail_bytes=` | — | `{"content", "truncated"}` — serial console tail (≤256 KiB); never wakes the guest; archived sandboxes answer 404 (no live console) |
| `GET /vms/{id}/metrics?since_unix=&max_samples=` | — | `{"samples": [{"at_unix", "interval_s", "cpu_ms", "mem_mb", "rx_bytes", "tx_bytes", "state"}, …], "cpus"}` — 30 s host-side samples, \~2 h kept |
| `GET /vms/{id}/metrics/history?hours=` (or `start=&end=`, RFC3339) | — | `{"points": […]}` — per-minute ledger history (≤ 15 days back; `hours` 1–360); works for archived and deleted sandboxes; a point with `"measured": false` is a gap, not a zero |
| `GET /audit?limit=&before=` | — | `{"records": [{"id", "key_id", "action", "resource", "outcome", "request_id", "at"}, …], "next_before"}` — the org's mutating API calls, newest first |
| `POST /vms/{id}/expose` | `{"guest_port": 8080, "public_port": 0}` | `201 {"ingress_id", "guest_port", "gateway_node", "public_port", "public_url"}` — live once the guest runs; exposing an archived sandbox is allowed (live on unarchive); a deleted sandbox's URL can take \~30 s to fail fast; across archive/unarchive the URL may 404 briefly while the route re-installs |
| `GET /vms/{id}/ingress` · `DELETE /ingress/{iid}` | — | `{"ingresses": […]}` · `204` |
**Sandbox object:** `vm_id`, `tenant`, `state` (`creating`, `running`,
`paused` — you called `pause`, only `resume` undoes it; `slept` — the idle
policy paused it, the next call wakes it; `archived` — parked to object
storage: after your `archive()` only `unarchive()` undoes it
(`SandboxStoppedError` otherwise); when the idle policy parked it, the next
call wakes it in seconds; `interrupted` — its host died, contact support to
recover from a durable checkpoint; `migrating`, `deleting`, `failed`), `image`,
`cpus`, `memory_mb`, `guest_ip`, `runtime`, `created_unix_ns`, `parent_vm_id`
(set on a fork child), `node`, `shard`, `disable_internet`. While archived,
`node`/`guest_ip`/`mgmtd_addr` are last-known values, not a live placement.
## Snapshots and exports
| Route | Response |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /snapshots` | `{"snapshots": [{"snapshot_id", "vm_id", "parent_snapshot_id", "state", "durable", "image", "logical_bytes", "created_unix_ns"}]}` — `durable` flips `true` when the upload finishes (seconds); restore works before that, export does not |
| `GET /snapshots/{sid}` · `DELETE /snapshots/{sid}` | snapshot object · `204` |
| `POST /snapshots/{sid}/export` | `202 {"export_id"}`; `409 snapshot is not durable yet` until `durable` (retry in seconds). **Experimental at launch.** |
| `GET /exports/{eid}` | `{"export_id", "snapshot_id", "state", …}`; `pending` → `ready` (`files` = `memory.img.zst`, `disk.img.zst` — zstd; each entry has `bytes` + `raw_bytes` — plus `state.json`, `manifest.json` v2, presigned, own TTL) or `failed` (with the scrubbed reason after three attempts; SDK `exports.wait(eid)` raises `ExportFailed`). Deleting the snapshot afterwards does not revoke an export |
## Templates
| Route | Body | Response |
| -------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `POST /templates` | `{"name", "image", "version"?, "cpus"?, "memory_mb"?, "disable_internet"?, "runtime"?}` | `201` template object (status `building`) |
| `GET /templates` | — | `{"templates": […]}` — yours plus the shared public-image templates your creates used, marked `"shared": true` (read-only; a failed shared template is rebuilt by the next create of that image) |
| `GET /templates/{tid}` · `DELETE /templates/{tid}` | — | template object (also for a shared id a `503` handed you) · `204` (own templates only) |
Template object: `template_id`, `name`, `version`, `source_image`, `status`
(`building` → `ready` | `failed`), `status_detail`, `runtime`, `base_snapshot_id`,
`created_unix_ns`, `last_used_unix_ns`.
## Registries
| Route | Body |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `POST /registries` | `{"host": "ghcr.io", "kind": "static", "username": "…", "secret": "…"}` or `{"host": ".dkr.ecr..amazonaws.com", "kind": "ecr-assume-role", "role_arn": "…", "external_id": "…"}` → `204` |
| `GET /registries` | `{"registries": [{"host", "kind", "created_unix_ns", "updated_unix_ns"}]}` (never returns secrets) |
| `DELETE /registries/{host}` | `204` |
## Usage
`GET /usage?start=&end=` (default last 24 h) →
`{"start", "end", "states": [{"state", "vm_seconds", "cpu_seconds",
"memory_mb_seconds"}, …], "snapshot_byte_seconds"}` — per-state rows;
whole-minute samples (short-lived sandboxes round up); window capped at 32 days;
empty `states` is a valid no-usage answer. `snapshot_byte_seconds` integrates
held snapshots' logical bytes (deleted ones stop counting).
`GET /usage/series?hours=` (or `start=&end=`) is the activity-graph time series
behind the console's Usage charts. `GET /usage-export` is operator-only
(`USAGE_EXPORT_SUBJECTS`; normal keys get 403).
## Status codes
| Code | Meaning |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | malformed body or field, e.g. `image "…" is not a valid image reference`; also one exec argument over 131,071 bytes (pass data via stdin — the SDK refuses client-side) |
| `401` / `403` | missing or invalid key / not permitted |
| `404` | no such sandbox, snapshot or template in your organization |
| `409` | precondition failed: template not ready, runtime disagrees with the template, an operation on an archived sandbox (`sandbox is stopped` → unarchive), a guest operation on a paused sandbox (`sandbox is paused` → `resume`; SDK `SandboxPausedError`), a checkpoint of a guest whose agent is not answering (`sandbox agent unreachable` → `reboot` or delete), a snapshot delete while an export is in progress, or — rarely — a guest RPC still in flight (retry) |
| `429` + `Retry-After`, body `rate limited` | the per-organization request budget for this **class** (acquire = create/fork/restore; ops = everything else) is spent this minute; wait and retry. Fixed-window bucket per gateway replica (two today): each holds half the per-minute figure, refilled in full at its minute — a sustained load can pass up to 2× the nominal figure without a 429, and after a 429 the budget returns all at once. The nominal figures are the per-organization guarantee, not a ceiling. `x-ratelimit-*` headers are advisory, not a live counter |
| `403` `{"error":"quota","kind":"cap"}` | organization concurrent-sandbox cap: **20** running, creating, **paused and slept** per organization (they keep RAM/vCPU resident; archived ones don't count) — delete or archive first; waiting does not help (SDK `QuotaError.kind == "cap"`) |
| `503 sandbox agent unreachable` | the guest stopped answering (fork bomb, OOM inside it); others unaffected — `reboot()` (keeps disk) or delete; archive would checkpoint and a starved guest cannot be checkpointed (SDK `SandboxUnhealthyError`) |
| `503` + `Retry-After: 30`, `{"error": "template building", …}` | first use of this image at this shape; poll `GET /templates/{template_id}` until `ready`, then retry |
| `503` `{"error":"capacity","kind":"placement"}` + `Retry-After` | no host can fit the shape right now (or a fork fan-out's parent host can't take all children); SDK `CapacityError` — transient, retry with backoff. Distinct from the cap (403) and rate limits (429) |
| `503` with `connection error` / `Unavailable` | a control-plane component is restarting; the request did not reach the sandbox — retry in seconds |
| `5xx` | platform failure; idempotent requests can be retried |
# Limits & behaviors
Source: https://lithos-box.mintlify.app/reference/limits
Every hard limit, what happens at the bound, and the platform behaviors worth knowing.
**Hard limits** (measured/enforced as of 2026-09-05; each is a real bound):
| What | Bound | What happens at the bound |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cpus` | 1–4 at launch | `400` before any host work |
| `memory_mb` | 128–8192 at launch | `400` |
| writable disk | 16 GiB sparse, fixed | `writable_size_bytes` other than 16 GiB → `400` |
| processes per sandbox | one per MiB of `memory_mb` (four for `runtime="vm"`), floor 256 / cap 4096 (1024 / 16384 for vm) — 1024 at the default 1 GiB; a cgroup pids cap, independent of `cpus`; `ulimit -u` is not it (root is exempt from RLIMIT\_NPROC) | forks fail with `EAGAIN` inside the sandbox. A steady load recovers as processes exit. An exec or session whose processes reach the cap is treated as a fork storm and that exec's process group is killed; the sandbox stays up and answers |
| sessions per sandbox | 256 concurrent `session()` shells; the 257th open returns 409 `session limit (256) reached` (ConflictError). `close()` frees a slot. | |
| container memory | guest RAM minus max(128 MiB, 10 %) reserved for the guest agent and kernel | a process over the cap is OOM-killed inside the sandbox; the agent survives and keeps answering |
| `exec` argv | 128 KiB (kernel `MAX_ARG_STRLEN`) | `400`; ship bulk data via `stdin` |
| `exec` stdout / stderr | 16 MiB per stream, text (UTF-8; invalid bytes become U+FFFD) | truncated: `stdout_truncated` / `stderr_truncated` `true` (SDK `ExecResult.truncated`) + an inline notice; write large or binary output to a file and read it back |
| synchronous `exec` | \~110 s at the edge | the call fails; use `run(…, background=True)`/sessions for long work |
| request budget | per organization, per request class — the documented numbers are the EFFECTIVE fleet-wide budget | `429` + `Retry-After` (see [status codes](/reference/http-api#status-codes)) |
**Rate limits** are per organization per minute (new organizations: 300
acquisitions — creates/snapshots/forks/exposes/templates/exports — and 12,000
other operations; DELETE is never limited). Each figure is a per-minute
guarantee enforced as a fixed-window bucket per gateway replica (two), refilled
in full each minute — so sustained throughput can measure up to \~2× the figure
before any 429, and after a 429 the budget returns all at once; a 429 carries
`Retry-After`, and successful responses carry `x-ratelimit-remaining`
(`nv.rate.acquire` / `nv.rate.ops` in the SDK).
## Behaviors
**Idle sleep policy (since 2026-09-07).** The platform never auto-sleeps a
sandbox that is doing work: a process that outlived its exec, guest CPU above
the idle floor, live network connections, or an exposed port all count as work.
Only a sandbox idle by every measure, with no API call for \~60 s, is slept; the
next call wakes it transparently. No maximum hold.
**Fork bombs are contained** to their sandbox; a watchdog freezes and kills the
storm near the pids cap and the sandbox's agent normally survives (measured
2026-09-05/07). If the guest wedges anyway: `SandboxUnhealthyError` → `reboot()`
or delete.
**`archive`/`unarchive` is a true resume**: processes, sockets and memory
continue. `/tmp` is ordinary writable-disk space (not RAM), so it survives
archiving and counts toward the disk.
**`delete()` returns before the guest is gone**: the record answers 404
immediately (repeat DELETE = no-op 204) while the host tears down
asynchronously — typically under a second. Its URL and in-flight execs fail
during that window; that is teardown, not a leak.
**Network policy.** Link-local `169.254.0.0/16` is blackholed for every sandbox.
`disable_internet` is inherited by snapshots' restores and fork children; a
no-egress connect blocks until the client's own timeout (no fast rejection) —
give such clients short connect timeouts. Environment variables cannot be set
per sandbox at create: bake them into the image or set them per run
(`run("VAR=value cmd")` or a session's `export`).
**Clock.** Guests boot with `clocksource=kvm-clock`: `CLOCK_MONOTONIC` is
consistent across vCPUs.
**Harbor benchmarks.** The `nanovm-harbor` wheel (`/sdk/` on the console) is a
Harbor environment provider (`--env nanovm_environment:NanoVMEnvironment`); it
handles template builds, retries, compose tasks, and ships
`python -m nanovm_prebuild images.txt`. README inside the wheel or the repo's
`integrations/harbor/`.
# Python SDK
Source: https://lithos-box.mintlify.app/reference/sdk
The whole surface is twelve names — one operation per job, arguments select behavior. Everything else is an alias.
The SDK is **LithosBox** (`import lithosbox`; class `LithosBox`; `Nanovm` and
`import nanovm` remain aliases). Sync and async (`AsyncNanovm`) with identical
surfaces. Constructor: `api_url`, `token` — or `auth_url`/`client_id`/`client_secret` —
and `timeout`; all read `NANOVM_*` env vars when omitted.
**The whole surface is twelve names.** Each is one operation whose arguments
select the behavior — there is one way to run a command, one way to read
metrics, one way to put a sandbox away. Every argument listed here is the whole
contract; there are no other public methods worth learning (older names are
silent aliases).
### `nv.sandboxes.create(...)`
`create(image=None, template=None, snapshot_id=None, cpus=None, memory_mb=None, disable_internet=False, runtime=None, vm_id=None, wait_for_template=True, warm_timeout=300)`
One source: an OCI `image` ref, a `template` (name, `name:version`, or id), or a
`snapshot_id` to restore into a NEW sandbox; none = default sandbox (Python 3.12 +
Node 22). Shape: `cpus` 1–4, `memory_mb` 128–8192 (defaults 2/1024).
`runtime="vm"` gives full-VM semantics (real /dev, dockerd works). `vm_id` (a
UUID you mint) makes the create retry-safe. Returns a `Sandbox`; context-manager
deletes on exit. Also `get(id)`, `list()`, `delete(id)`.
### `sb.run(command, background=False, stdin=None) -> ExecResult`
THE command verb. `command` str = shell (`sh -c`: pipes, globs, redirects).
`command` list = argv, no shell — the only safe way to pass untrusted values.
Blocks until the command and its children finish (\~110 s wall at the edge):
`.exit_code/.stdout/.stderr` (+ `.stdout_truncated` past the 16 MiB stream cap).
`background=True` (shell string only) returns immediately and the process
outlives the call, keeping the sandbox awake. `stdin` bytes/str ≤ 4 MiB (the
way to ship files/binary data — argv strings cap at 128 KiB and cannot hold NUL).
### `sb.session(session_id=None) -> Session`
A shell where `cd`, `export`, venvs and background jobs persist across
`.run(cmd)` calls (256 concurrent per sandbox; same id from any handle = same
shell; `.close()` ends it for everyone). Use for multi-step shell work; use
plain `run()` for everything else.
### `sb.files.*`
`write(path, data) / read(path) / read_bytes(path) / list(path) / remove(path) / rename(src, dst) / mkdir(path) / exists(path)` —
all file work (4 MiB per write, 16 MiB per read; loop for bigger). `remove` is
recursive. `write` creates parents.
### `sb.expose(guest_port, public_port=0) -> IngressInfo`
Public HTTPS URL (`.url`) for a port the guest listens on. Live once the server
answers; survives sleep/wake; re-installs across archive/unarchive (may 404 for
seconds). `sb.ingress()` lists, `nv.ingress.delete(id)` removes.
### `sb.archive()` / `sb.unarchive()`
Put the sandbox away: memory+disk to object storage, host freed, compute
billing stopped, **survives node loss**; composes from any rest state.
Unarchive restores it running, processes intact (seconds). A later `run()` on
an archived sandbox raises — unarchive first.
### `sb.snapshot(leave_paused=False, wait_durable=False) -> Snapshot`
Durable checkpoint of memory+disk (\~50 ms; durable seconds later —
`wait_durable=True` blocks for it). Restore into a NEW sandbox:
`nv.snapshots.restore(snap)` or `create(snapshot_id=...)`. Snapshots outlive
their sandbox. `nv.snapshots.list/get/delete`.
### `sb.fork(n=1) -> Sandbox | list[Sandbox]`
Live copies from this instant — running processes and open connections included
(n ≤ 4 per call; fork between commands, not mid-command). One checkpoint, n
parallel restores.
### `sb.events(limit=200) -> list[dict]`
The lifecycle timeline, newest first: `{kind, detail, node, at}` for
created/paused/resumed/slept/archived/unarchived/rebooted/snapshotted/forked/deleted.
Survives deletion.
### `sb.logs(tail_bytes=65536) -> str`
Serial-console tail (boot output, kernel messages; ≤ 256 KiB per read).
Host-side: never wakes a paused/slept guest. Archived sandboxes have no live
console (their `events()` and `metrics(hours=…)` still answer).
### `sb.metrics(hours=0, start="", end="") -> dict`
No arguments: `{"samples": [...], "cpus": n}` — live 30 s host samples, last
\~2 h (`cpu_ms`, `mem_mb`, `rx/tx_bytes`, `interval_s`; CPU fraction =
`cpu_ms/(interval_s*1000*cpus)`). With `hours=` or `start=`/`end=` (RFC3339,
≤ 15 days back): `{"points": [...]}` — per-minute ledger history, including
archived and deleted sandboxes; a point with `measured=false` is a gap, never a
zero (normalize by its `util_seconds`).
### `nv.usage(start=None, end=None)` / `nv.audit(limit=200, before=None)`
Org metering aggregates (per-state vm/cpu/memory seconds + snapshot
byte-seconds) and the org's audit trail of mutating API calls
(`{id, key_id, action, resource, outcome, request_id, at}`; page with
`before=`).
***
That is the product. Everything below is for when you need it — nothing there
changes how the twelve behave.
| Operation | Notes |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `sb.pause()` / `sb.resume()` | ms-scale suspend, RAM held on the host (archive is the durable version) |
| `sb.sleep()` | hint the idle reclaimer; the platform already does this automatically, and any call wakes transparently |
| `sb.reboot()` | fresh boot, disk kept, processes/sessions gone, new guest IP |
| `nv.templates.create/list/get/wait/delete` | build an image once, create from it fast |
| `nv.registries.add/list/remove` | private image registry credentials |
| `snapshot.export()` + `nv.exports.get/delete` | download a snapshot as a zstd image |
| `GET /usage/series` | the activity-graph time series behind the console's Usage charts |
| `nv.rate` | client-side view of the rate-limit headers (`nv.rate.acquire/.ops`; pace on `.remaining`) |
Older method names from 0.4.x (`exec`, `exec_background`, `stop`/`start`,
`branch`, `files.ls`) keep working forever as aliases of the twelve — existing
code never breaks — but new code and these docs use one name per operation.
## Errors
`AuthError` (401/403 → key), `NotFoundError` (404), `ConflictError` (409:
template not ready / sandbox stopped), `TemplateBuildingError` (503: first-use
build, `create()` waits), `ServerError` (5xx: retry idempotent calls). A 503
whose text says "connection error" is a transient control-plane restart: retry
in a few seconds. `SandboxUnhealthyError` (503: the guest stopped answering — a
fork bomb or OOM inside it): `reboot()` or `sb.delete()` and create a new one;
archiving cannot checkpoint a starved guest, so it fails. `TransportError`: the
connection failed before a response — idempotent calls were already retried;
for a create the outcome is UNKNOWN, so `nv.sandboxes.list()` before retrying
(never double-create blindly).
`QuotaError` (429/403) is one of two things — read `.kind`: `"rate"` = the
organization's per-minute request budget is spent; wait `.retry_after` seconds
(from `Retry-After`) and retry. Creates, snapshots, forks, exposes, templates
and exports share one budget; exec and other operations a much larger one;
**DELETE is never rate limited**, so releasing resources always works. `"cap"` =
the concurrent-sandbox cap; delete or archive a sandbox, then retry — waiting
does not help. Never loop on a 429 without waiting. Full status-code table:
[HTTP API](/reference/http-api#status-codes).