Runtime (withruntime.com) bills an agent sandbox on the CPU it actually uses, $0.025 per active vCPU-hour, and pauses it after 300 seconds with nothing happening, so the most common runaway, a machine nobody is using, costs almost nothing by default. The other four need a limit you set. This post covers each way an agent can spend money it should not. It calculates the worst case for one day and shows the limit that stops it. It also covers which limits an agent can change on its own, and the one it cannot change.
Why are agents worse at spending than people?
An agent does not feel that something is taking too long. A person notices a laptop fan, a slow terminal or a bill alert. An agent retries what failed, starts another sandbox when one is busy, and keeps going until something stops it. Each failure below is normal agent behavior, not a bug. Each one has caused a surprise bill somewhere.
The worst cases in this post use a 2 vCPU, 4 GiB sandbox, which costs $0.08 an hour with both CPUs working and $0.03125 an hour while it waits, at Runtime's published rates (pricing).
1. What does a forgotten sandbox cost?
On most clouds, the bill covers a full day of running time. On Runtime, it
covers a minute of running time, then storage. An agent creates a sandbox. The
task ends or the process crashes. Nothing calls stop(). The machine keeps its
memory reserved, and the meter keeps running.
- Without a limit 24 hours waiting cost $0.75, and a fleet of a hundred forgotten ones $75 a day.
- With the default idle pause after 300 seconds with no request, no running command, no open connection and no CPU use, the sandbox pauses itself, and you pay only for its saved state. Two GB of it cost $0.0053 a day.
The limit: keep idlePauseSeconds low, from 60 seconds up.
Create sandboxes in a scope that deletes them: await using in TypeScript, or
with in Python. Give every sandbox a task creates a label, so a cleanup job
can find any sandbox the scope missed (pause when idle).
2. What happens when an agent loops on create?
It fills your account. An agent whose tool call times out tries again. A planner that thinks a sandbox is busy starts another. A framework retries a failed step on a new machine each time. Each attempt creates a new sandbox, and each one spends its time installing the same dependencies.
- Without a limit a paid account runs 100 sandboxes at the same time. If all of them are busy for a day, they cost $192.
- With limits an idempotency key built from the task id makes a retried create return the sandbox it already made, not a second one. The size of each sandbox is a constant in your code, not a tool argument. So an agent cannot request 16 vCPUs and 64 GiB because a build was slow. A daily spending limit on the agent's key (below) caps a loop you did not expect.
The Runtime SDKs already send an idempotency key with every write, so a retry inside one process never duplicates a create. Pass your own key to cover a process that restarts (retry safely).
3. What does a runaway process cost?
The machine can burn as much as it has. A test that loops forever, a crawler that never reaches its stop condition, or a compile in a loop: the sandbox is not idle, so it never pauses. On a busy machine, billing for CPU use does not help.
- Without a limit the largest sandbox, 16 vCPUs and 64 GiB, fully busy for a day costs $21.
- With limits
timeoutMson each command ends that command.timeoutSecondsat create sets a time limit for the whole sandbox. When the limit is reached, the sandbox stops running, keeps its files, and compute billing stops with it. A 2 vCPU, 4 GiB sandbox with a 30-minute limit costs at most $0.04, whatever runs inside it.
Give every command a timeout that fits its step. A test run that normally takes two minutes gets ten minutes, not the 24 hours a streamed command may run. Give every task sandbox a time limit that fits the task.
4. How does storage pile up?
Storage grows one snapshot at a time, and people rarely check it, because no
single item is expensive. Suppose an agent takes a snapshot after each of 50
steps in a task, and runs 20 tasks a day. It keeps every snapshot for 7 days
with retentionDays: 7. With no value set, a snapshot is kept as long as you
have credit. Each snapshot stores 2 GB of its own:
- Snapshots kept
- 50 × 20 × 7 days
- 7,000
- Stored
- 7,000 × 2 GB
- 14,000 GB
- Monthly cost
- 14,000 × $0.08 per GB
- $1,120
- With retentionDays
- 1 instead of 7
- $160
- Deleted when each task ends
- ≈ nothing
The limit: pass retentionDays: 1 to checkpoint snapshots. Delete a task's
snapshots when the task finishes. Give each volume a size you choose, not one
the agent guesses. A volume is charged for its full size from the moment it
exists. A block shared by two of your snapshots counts once, so real snapshot
chains usually cost less than this sum (storage prices).
5. Can an agent run up a bill with network traffic?
Yes, by sending data out. Inbound traffic is free on Runtime. Each account gets its first 100 GiB of outbound traffic free each month. Traffic beyond that costs $0.02 per GB. An agent that uploads build artifacts in a loop spends it. So does a compromised agent that sends a dataset out.
- Without a limit a paid sandbox moves at most 500 GiB a day, in and out combined. If all of it goes out, beyond the allowance, that costs $10.74 for that one sandbox.
- With limits an
allowlist in the sandbox's network rules limits traffic to the hosts the task needs, such as your package registry and your model provider. Data cannot leave for any other host, so it cannot be billed either (allow only some hosts).
Which limits can the agent raise by itself?
An agent can change every limit that its own key can reach. Most budget designs
miss this. Network rules can be widened with network.set(). Idle pause can be
turned off with update(). A paused sandbox can be woken with a new time limit.
Any key that may run the sandbox can do all of these. A limit the agent can
change is only advice.
| Limit | Stops | Set with | Who can change it |
|---|---|---|---|
| Idle pause | Forgotten sandboxes | idlePauseSeconds at create |
Any key that runs the sandbox |
| Size in your code | Oversized machines | vcpu, memoryMiB at create |
Whoever creates |
Command timeoutMs |
Hung and looping commands | Each exec |
Whoever runs the command |
| Sandbox time limit | One runaway sandbox | timeoutSeconds at create |
Any key that runs the sandbox |
Network allow list |
Traffic out, exfiltration | Create or network.set() |
Any key that runs the sandbox |
| Snapshot retention | Storage pile-up | retentionDays |
Whoever snapshots |
| Account concurrency | Fan-out | Your account | Support, on request |
| Daily spending limit on a key | Everything that key spends | The website, by a person | Only a person; no key, ever |
Two rules follow. First, keep the Runtime key in your orchestrator, the code
that calls the model and runs its tools. Do not put it in the sandbox where the
model's commands run. The model then cannot call update() at all. Second,
give each agent its own key with a daily spending limit. The limit counts what
that key's sandboxes cost in any 24 hours. Past the limit, a create or wake
fails with spending_limit_reached and charges nothing
(set a daily spending limit).
What does the code look like with every limit in place?
Create each task's sandboxes through one function that applies the limits. Check the task's total before each new sandbox. This code runs as written:
TypeScriptimport { randomUUID } from "node:crypto";import { Runtime } from "withruntime";const runtime = new Runtime();const TASK_BUDGET_MICROS = 2_000_000; // $2 for everything one task startsasync function spentOn(task: string) { let micros = 0; const page = await runtime.sandboxes.list({ labels: { task }, includeStopped: true }); for await (const sbx of page) micros += Number(sbx.info.chargedMicros); return micros;}async function sandboxFor(task: string, attempt: number) { const spent = await spentOn(task); if (spent >= TASK_BUDGET_MICROS) { await runtime.sandboxes.stopAll({ labels: { task } }); throw new Error(`task ${task} has spent its budget`); } return runtime.sandboxes.create( { labels: { task }, vcpu: 2, // oversized: the size is yours, never the agent's memoryMiB: 4096, idlePauseSeconds: 60, // forgotten: pauses a minute after the last use timeoutSeconds: 1800, // runaway: the sandbox stops after 30 minutes network: { internet: true, allow: ["pypi.org", "*.pythonhosted.org"] }, // traffic out }, { idempotencyKey: `${task}-${attempt}` }, // a retried create answers the same sandbox );}const taskId = `task-${randomUUID()}`; // your task's own id, the same on every retryawait using sbx = await sandboxFor(taskId, 1);const run = await sbx.exec("python3 -c 'print(sum(range(10)))'", { timeoutMs: 120_000 });console.log(run.exitCode, run.stdout.trim(), await spentOn(taskId));Pythonfrom uuid import uuid4from withruntime import Runtimeruntime = Runtime()TASK_BUDGET_MICROS = 2_000_000 # $2 for everything one task startsdef spent_on(task: str) -> int: page = runtime.sandboxes.list(labels={"task": task}, include_stopped=True) return sum(int(sbx.info["chargedMicros"]) for sbx in page)def sandbox_for(task: str, attempt: int): spent = spent_on(task) if spent >= TASK_BUDGET_MICROS: runtime.sandboxes.stop_all(labels={"task": task}) raise RuntimeError(f"task {task} has spent its budget") return runtime.sandboxes.create( labels={"task": task}, vcpu=2, # oversized: the size is yours, never the agent's memory_mib=4096, idle_pause_seconds=60, # forgotten: pauses a minute after the last use timeout_seconds=1800, # runaway: the sandbox stops after 30 minutes network={"internet": True, "allow": ["pypi.org", "*.pythonhosted.org"]}, # traffic out idempotency_key=f"{task}-{attempt}", # a retried create answers the same sandbox )task_id = f"task-{uuid4()}" # your task's own id, the same on every retrywith sandbox_for(task_id, 1) as sbx: run = sbx.exec("python3 -c 'print(sum(range(10)))'", timeout_ms=120_000) print(run.exit_code, run.stdout.strip(), spent_on(task_id))chargedMicros is what each sandbox has cost so far, in millionths of a
dollar. The task's total includes the sandboxes it already stopped. The check
runs before each new sandbox. Each sandbox's time limit caps what it can add
after the check: at most $0.04 for 30 minutes with both CPUs busy.
So the task can pass its budget by at most one sandbox's cost, however many
sandboxes it starts.
An agent can also read its key's limit before it plans a large fan-out. It can then ask a person, instead of being refused partway through:
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();const { daily } = await runtime.limits.get();const left = daily.remainingMicros === null ? Infinity : Number(daily.remainingMicros);if (left < 5_000_000) console.log("Under $5 left today: ask before starting the fan-out.");How do you catch spending you did not plan for?
Watch lifecycle events, not invoices. A webhook on sandbox.stopped reports the
moment a sandbox reaches its time limit. The stopReason field says why it
stopped. A burst of sandbox.created events from one key means a loop is
running. Usage & billing shows what each product cost each day, and how long
your credit lasts at this month's pace. Every owner gets an email when the
balance will last under five days. Credit is prepaid. The balance never goes
below zero, and you never owe more than you added
(metrics and webhooks).
In short
- Forgotten sandboxes are handled by idle pause; keep it on and keep it short.
- Loops, oversized machines and runaway processes need idempotency keys, a size set in your code, a time limit on every sandbox and a timeout on every command.
- Storage and traffic out grow quietly; set snapshot retention to a day and narrow the network to the hosts a task needs.
- An agent holding a key can raise most limits. Keep the key in your orchestrator, and give each agent's key a daily limit only a person can change.
Run it on Runtime
Runtime costs 42% to 88% less than fourteen other sandbox providers for an agent that mostly waits on a model (compare costs). Runtime charges $0.025 per vCPU-hour of CPU actually used and $0.0075 per GiB-hour of memory, with no plan fee, and every limit in this post is built in. Start free, with 100 free hours of a 1 GB machine every month and no card at withruntime.com, and set the daily spending limit on your first key before an agent ever uses it.