# How to run background AI agents in cloud sandboxes Take each task off a queue. Give it a sandbox named after the task. Run the agent there, and collect the result when it exits. **On Runtime, 500 background tasks of 20 minutes each cost about $5.63 a month** on 2 vCPU, 4 GiB sandboxes, at the rates in effect on 26 October 2026. The reason is that a sandbox is billed for the CPU its commands use rather than the CPUs it holds. A paid account runs 100 sandboxes at the same time. A create past that limit waits for room instead of failing. So a burst of tasks queues by itself. ## The short answer A worker takes a task. It creates the task's sandbox with an idempotency key made from the task id. Then it runs the agent and returns what the agent changed: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime({ waitForCapacityMs: 600_000 }); // queue up to 10 minutes for room export async function runTask(task: { id: string; repo: string; prompt: string }) { await using sbx = await runtime.sandboxes.create( { name: `task-${task.id}`, labels: { kind: "background", task: task.id }, diskMiB: 8192, maxTotalCostMicros: 2_000_000, // at most $2 for this task }, { idempotencyKey: `task-${task.id}` }, // a retried worker never makes a second sandbox ); // No time limit: the sandbox runs while the agent works, however long. await sbx.exec(["git", "clone", "--depth", "1", task.repo, "repo"], { check: true, timeoutMs: 300_000, }); await sbx.files.upload("./agent", "/workspace/agent"); await sbx.files.write("/workspace/prompt.txt", task.prompt); const agent = await sbx.spawn("python3 /workspace/agent/main.py /workspace/prompt.txt", { cwd: "/workspace/repo", }); const done = await agent.wait(); const diff = await sbx.exec("git diff", { cwd: "/workspace/repo" }); return { exitCode: done.exitCode, diff: diff.stdout }; } ``` ```python check from withruntime import Runtime runtime = Runtime(wait_for_capacity=600) # queue up to 10 minutes for room def run_task(task_id: str, repo: str, prompt: str) -> dict: with runtime.sandboxes.create( name=f"task-{task_id}", labels={"kind": "background", "task": task_id}, disk_mib=8192, max_total_cost_micros=2_000_000, # at most $2 for this task idempotency_key=f"task-{task_id}", # a retried worker never makes a second sandbox ) as sbx: # No time limit: the sandbox runs while the agent works, however long. sbx.exec(["git", "clone", "--depth", "1", repo, "repo"], check=True, timeout_ms=300_000) sbx.files.upload("./agent", "/workspace/agent") sbx.files.write("/workspace/prompt.txt", prompt) agent = sbx.spawn("python3 /workspace/agent/main.py /workspace/prompt.txt", cwd="/workspace/repo") done = agent.wait() diff = sbx.exec("git diff", cwd="/workspace/repo") return {"exit_code": done.exit_code, "diff": diff.stdout} ``` Leaving the block deletes the sandbox, so a finished task stops incurring charges. If a crash puts the task back on the queue, the same idempotency key returns the same sandbox instead of starting a duplicate. ## What goes wrong with background agents, and what stops it | Risk | What handles it | | ---------------------------------------- | ------------------------------------------------------------------------------------------ | | More tasks arrive than can run | `create` waits and retries on `quota_exceeded`, `no_capacity` or `no_credit_running_limit` | | A worker stops partway through | The agent process lives in the sandbox; `processes.get(id)` reattaches | | A retry starts the task twice | Every write carries an idempotency key; pass your own to work across restarts | | One task spends too much | `maxTotalCostMicros` on the sandbox and a daily spending limit on the key | | An agent reaches somewhere it should not | Per-sandbox `network` allow and deny lists, enforced on the host | | The agent needs a token | A [secret](/docs/security#secrets-sandboxes-never-see) the sandbox never holds | | A failure goes unnoticed | A webhook on `sandbox.stopped` and `sandbox.start_failed` | A paid account runs 100 sandboxes at the same time. It runs up to 200 vCPUs and 400 GiB of memory across the running sandboxes. Support raises these limits on request ([pricing](/docs/pricing#how-many-at-once)). An account without credit runs four at the same time. ## See what is running Labels make the fleet queryable. List the running background tasks, or all of them, from any process: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); const page = await runtime.sandboxes.list({ labels: { kind: "background" }, state: ["running", "paused"], }); for await (const sbx of page) console.log(sbx.info.name, sbx.state, sbx.info.chargedMicros); ``` ```python check from withruntime import Runtime runtime = Runtime() for sbx in runtime.sandboxes.list(labels={"kind": "background"}, state=["running", "paused"]): print(sbx.info["name"], sbx.state, sbx.info["chargedMicros"]) ``` `chargedMicros` is the cost of each sandbox so far, in millionths of a dollar. From a terminal, `runtime sandboxes ls` shows the same list. ## Tell the user when the task is done A background agent is useful when the person who asked does not have to wait. Send lifecycle events to your own endpoint. Notify the user from there: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); const hook = await runtime.webhooks.create({ url: "https://example.com/hooks/runtime", events: ["sandbox.stopped", "sandbox.start_failed"], }); console.log(hook.secret); // shown once; verify each delivery with verifyWebhook ``` Each event carries the sandbox as it was, labels included. The handler can find the task from `labels.task`. Runtime keeps events for 14 days. Read them with `runtime.events.list()` ([metrics and webhooks](/docs/observability#events)). ## Keep a finished task to look at Some tasks end with a person reviewing the result. Instead of stopping the sandbox, pause it. The repository, the agent's scratch files and any dev server stay as they were. Share a port with `previews.create(port)` so the reviewer can open it. Stop the sandbox when the review closes. ## What it costs Assume 500 tasks a month. Each keeps a 2 vCPU, 4 GiB sandbox running for 20 minutes. In that time, each uses 180 CPU-seconds of work, which is three minutes of one core: ``` CPU: 500 × 180 s / 3,600 × $0.025 = $0.63 Memory: 500 × 20 min / 60 × 4 GiB × $0.0075 = $5.00 Total: $5.63 ``` That is a little over a cent a task. Runtime charges $0.025 per vCPU-hour of measured CPU, never less than a twentieth of a vCPU. It charges $0.0075 per GiB-hour of memory ([pricing](/docs/pricing)). You pay your model provider for model calls, not Runtime. ## Start ```bash no-run npx withruntime sandboxes run -- git --version ``` The command asks you to approve the connection in a browser, once per machine. Then point one worker at the queue with the code above. Use the [JavaScript](/docs/javascript#errors-and-retries) or [Python](/docs/python#errors-and-retries) SDK. Related: [long-running agents](/use-cases/long-running-agents), [coding agent sandbox](/use-cases/coding-agent-sandbox), [Claude Code in a sandbox](/integrations/claude-code), [egress control](/glossary/egress-control). Facts on this page were checked on 26 October 2026.