---
title: Compute sandboxes
description: The cloud run every routine uses, and the optional external sandboxes (Daytona, Modal, Cloudflare, E2B, Vercel) for work it cannot do. What each is, when a routine would use one, how a routine reaches it, limits and cost, and why none is tested from a routine yet.
sidebar:
  order: 5
---

## What it is

The default runtime for every routine is the Claude Code cloud session the scheduled task starts. The registry records each run as a fresh VM with 4 vCPU, 16 GB RAM and 30 GB disk, Python 3, and the repo cloned each run (`run-vm-spec`, docs-stated; a pilot saw the CPU and memory, not the disk).

An **external sandbox** is a second machine a run hands work to: a provider's isolated container or VM for jobs the cloud run cannot do. The run stays in charge. It starts the job, waits for or collects the result, and writes the store through the write contract as usual.

> No pilot has run a call from a routine to any sandbox provider (`cloudflare-supabase-modal-daytona-reach`, unverified). Everything below about reaching a provider from a routine is a design, not a tested path. Vendor facts are from each vendor's own docs on the date in the source comment.

## Why it exists

Most routines never need one. The cloud run has a shell, Python and the repo, and a [code routine](/routine-kinds/code) that sticks to the standard library runs there with no setup. An external sandbox is for the exceptions:

- **A job longer than a run should take.** The maximum run time of a routine is not stated in the docs (`max-run-time`, unverified), so a job of many minutes is a risk to the write at the end.
- **An install too heavy for a setup script.** A setup script must finish in about five minutes to be cached, and it runs before the environment's variables are set (`setup-script-rules`, docs-stated).
- **A host the run's network blocks.** The run's network behaves like an allowlist; a pilot saw `api.cloudflare.com`, `gateway.ai.cloudflare.com`, `developers.cloudflare.com` and `huggingface.co` refused with a proxy 403 (`sandbox-allowlist`, pilot-verified).
- **Untrusted or generated code.** Code a model wrote, or a page a crawler renders, can run on a machine that holds nothing of the person's.

### When to choose one

Try these first, in order:

1. The cloud run with standard-library scripts.
2. A setup script that installs what you need, if it fits its limits.
3. A connector that already does the job, since connector traffic does not depend on the run's network level (`connector-traffic-bypasses-sandbox-network`, docs-stated).

Reach for an external sandbox only when all three fail, and only after a report-only run has shown the provider's host is reachable.

## How it works

### Three ways a routine can reach a sandbox

| Path | How it works | Credential | Fits |
|---|---|---|---|
| **MCP connector** | The provider's remote MCP server is added as a connector; the model calls its tools | The connector holds it; OAuth on the provider's side | A model stage that starts and reads a sandbox. Remote servers only |
| **Script plus network secret** | A `lib/` script calls the provider's HTTP API with no key in the VM; a network secret adds the key on the way out | A network secret, one per host | A code stage, when the provider has a plain HTTP API |
| **Endpoint you own** | A Worker or service on your domain holds the provider key and runs the job; the routine calls your endpoint | A network secret for your one host | Any provider, including SDK-only ones |

How a network secret works, per the Claude Code environments docs: the key is added by Anthropic's agent proxy after the request leaves the VM, so it never sits in the session. By default it is sent as a bearer token in the `Authorization` header, and a custom header such as `X-Api-Key` is possible. A session can reach a secret's hosts even when the environment's network level would not otherwise allow them. A secret is never attached to requests to GitHub, `api.anthropic.com`, package registries, setup-script requests or telemetry. Without admin rights a secret names one host with no wildcard. Network secrets are available on Pro, Max, Team and Enterprise. <!-- verified: https://code.claude.com/docs/en/cloud-environments 2026-10-31 --> RoutineOS records this as docs-stated (`network-secrets`); no pilot has called an API through one yet.

A local MCP server (one started as a command on your machine) is not a connector a cloud run has. Installing a provider's CLI in a setup script so the run can start its MCP server locally is possible in principle, but needs a login inside the run and is not verified.

### The rules

From `skills/routineos/references/runtimes.md`:

1. **Test the host from a run first.** Probe it in a report-only run and write the result to `risks.md`.
2. **Credentials never go in the store or the prompt.** Prefer a connector, or an endpoint you own, with a network secret for its single host. `lint` refuses a `[[secret]]` with `where = "env-var"`.
3. **Keep the contract.** The remote job returns results to the run through a path that carries a version, and the run writes state after the report. A sandbox never writes the store directly.
4. **Mark the routine's README** with the runtime and the date it was last tested.

Declare the provider in `app.toml` so the permission screen shows it:

```toml
[[integration]]
name = "daytona"
kind = "secret-host"        # connector, mcp, secret-host or http
direction = "read"          # write needs gate = true
hosts = ["app.daytona.io", "proxy.app.daytona.io"]
what = "Runs the site crawler in a Daytona sandbox"

[[secret]]
name = "daytona-api"
where = "network-secret"    # connector or network-secret, never env-var
host = "app.daytona.io"

[[secret]]
name = "daytona-toolbox"
where = "network-secret"
host = "proxy.app.daytona.io"
```

### Budget

An external sandbox has its own bill. RoutineOS lists anything with a monetary cost on the permission screen, and each needs its own yes. `[budget] max_runs_per_day` caps runs, not a provider's spend: set a spend limit with the provider, and make every sandbox the routine starts short-lived, so a failed run does not leave one billing.

## Providers

| Provider | What it is | Routine path | Free allowance | Source |
|---|---|---|---|---|
| Daytona | Sandboxes with file system, git, process and code execution | Script plus network secret (REST), or endpoint you own | $200 in free compute | [docs](https://www.daytona.io/docs/en/sandboxes), [pricing](https://www.daytona.io/pricing) |
| Modal | Containers for untrusted code, driven from its SDKs | Endpoint you own | $30 a month on Starter | [docs](https://modal.com/docs/guide/sandboxes), [pricing](https://modal.com/pricing) |
| Cloudflare Sandbox SDK and Containers | Linux containers started from a Worker | Endpoint you own (a Worker), or the Containers MCP server as a connector | Needs Workers Paid ($5 a month), with usage included | [docs](https://developers.cloudflare.com/sandbox/), [pricing](https://developers.cloudflare.com/containers/pricing/) |
| Cloudflare Workers | Serverless functions on your domain | Endpoint you own | 100,000 requests a day on Free | [pricing](https://developers.cloudflare.com/workers/platform/pricing/) |
| E2B | Linux VMs created on demand, with pause and resume | Endpoint you own | $100 one-time on Hobby | [docs](https://docs.e2b.dev/), [pricing](https://e2b.dev/pricing) |
| Vercel Sandbox | Firecracker microVMs with snapshots; JS and Python SDKs and a CLI | Endpoint you own | Active CPU pricing | [docs](https://vercel.com/docs/vercel-sandbox) (fetched 2026-10-30, not re-checked) |

None is reachable-from-a-routine verified.

### Daytona

**What it is.** A platform for running code in isolated sandboxes, with SDKs for Python, TypeScript, Ruby, Go and Java. <!-- verified: https://www.daytona.io/docs/en/sandboxes 2026-10-31 --> A default sandbox has 1 vCPU, 1 GB RAM and 3 GiB disk; an organization's maximum per sandbox is 4 vCPU, 8 GB RAM and 10 GB disk. <!-- verified: https://www.daytona.io/docs/en/sandboxes 2026-10-31 -->

**When a routine would use it.** A crawl, a build or a test run that needs installs or more time than the run should spend, where the job is a list of shell commands.

**How a routine reaches it.**

- *REST, from a code stage.* Daytona authenticates every request with an API key sent as a bearer token. Create a sandbox with `POST https://app.daytona.io/api/sandbox`, and run a command with `POST https://proxy.app.daytona.io/toolbox/{sandboxId}/process/execute`, body `{"command", "cwd", "timeout"}`. <!-- verified: https://www.daytona.io/docs/en/authentication 2026-10-31 --> <!-- verified: https://www.daytona.io/docs/en/process-code-execution.md 2026-10-31 --> Because the key is a bearer token, a network secret on each of the two hosts can carry it, and a `lib/` script can make the calls with `urllib` and no key in the VM. Not verified from a run.
- *SDK.* The Python SDK reads `DAYTONA_API_KEY` from the environment (with `DAYTONA_API_URL` and `DAYTONA_TARGET` optional). <!-- verified: https://www.daytona.io/docs/en/python-sdk/sync/daytona/ 2026-10-31 --> A routine should not hold the key in an environment variable, so use the SDK only from an endpoint you own.
- *MCP.* Daytona's MCP server runs locally from its CLI: install with `brew install daytonaio/cli/daytona`, run `daytona login`, then `daytona mcp init claude` (or `daytona mcp config` for the JSON, a server started with `daytona mcp start`). Its tools cover sandbox management, files, git, processes and code execution, computer use and previews. <!-- verified: https://www.daytona.io/docs/en/mcp 2026-10-31 --> It is a local server, so it is a fit for a person's own machine, not for a cloud routine. Not verified from a run.

**Limits.**

- Auto-stop defaults to 15 minutes of inactivity and triggers even if processes are running inside; 0 disables it. Auto-archive defaults to 7 days. A sandbox with auto-delete 0 is ephemeral: it is deleted when it stops. <!-- verified: https://www.daytona.io/docs/en/sandboxes 2026-10-31 --> For a routine, create ephemeral sandboxes so nothing outlives the run.
- On tiers 1 and 2 a sandbox's own network access is restricted and cannot be changed per sandbox; tiers 3 and 4 have full access by default, with per-sandbox allow lists or a block-all setting. <!-- verified: https://www.daytona.io/docs/en/network-limits 2026-10-31 --> Check your tier if the job fetches from the internet.

**Cost.** Billed per second: $0.0504 per vCPU-hour, $0.0162 per GiB-hour of memory, and $0.000108 per GiB-hour of storage after the first 5 GiB. $200 of compute is included. <!-- verified: https://www.daytona.io/pricing 2026-10-31 -->

### Modal

**What it is.** Modal Sandboxes are secure containers for running untrusted code, created and driven from Modal's Python, JavaScript and Go SDKs:

```python
sb_app = modal.App.lookup("my-app", create_if_missing=True)
sb = modal.Sandbox.create(app=sb_app)
p = sb.exec("python", "-c", "print('hello')", timeout=3)
print(p.stdout.read())
sb.terminate()
```

<!-- verified: https://modal.com/docs/guide/sandboxes 2026-10-31 -->

**When a routine would use it.** Python-heavy jobs, or jobs that need a GPU or a large image, where you already use Modal.

**How a routine reaches it.** Modal authenticates with a token id and secret (`MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET`). <!-- verified: https://modal.com/docs/reference/modal.config 2026-10-31 --> That pair does not fit a single bearer header, and no official MCP server for controlling Modal was found (Modal's docs have an example of hosting your own MCP server on Modal, which is a different thing). <!-- verified: https://modal.com/docs/examples/mcp_server_stateless 2026-10-31 --> So the routine path is an endpoint you own that holds the token and calls the SDK; the routine calls that endpoint with a network secret.

**Limits.** A sandbox lives 5 minutes by default; `timeout` raises it to at most 24 hours, and Modal recommends filesystem snapshots beyond that. <!-- verified: https://modal.com/docs/guide/sandboxes 2026-10-31 --> A sandbox's network can be blocked entirely (`block_network=True`) or limited with a CIDR allowlist, and a domain allowlist is in beta (TLS on port 443 only). <!-- verified: https://modal.com/docs/guide/sandbox-networking 2026-10-31 -->

**Cost.** Starter is $0 plus compute, with $30 of free credits a month; Team is $250 a month with $100 of credits. Sandbox CPU is listed at $0.00003942 per core per second and memory at $0.00000667 per GiB per second. <!-- verified: https://modal.com/pricing 2026-10-31 -->

### Cloudflare Sandbox SDK and Containers

**What it is.** The Sandbox SDK (`@cloudflare/sandbox`) runs untrusted code in isolation from a Worker. Each sandbox runs on Cloudflare Containers, with a Durable Object starting the container and sending it commands. It needs the Workers Paid plan. <!-- verified: https://developers.cloudflare.com/sandbox/ 2026-10-31 --> The SDK's docs currently cover two API generations (0.x pages for existing apps, and a 1.0 migration), so check the version you install before copying code. <!-- verified: https://developers.cloudflare.com/sandbox/sdk/api/commands/ 2026-10-31 -->

**When a routine would use it.** You already run Workers on your own domain, or the job needs a headless browser or a full Linux toolchain behind an endpoint you control.

**How a routine reaches it.**

- *Endpoint you own.* Deploy a Worker on your domain that starts a sandbox, runs the job and returns JSON. The routine calls it with a network secret for that one host. This avoids `api.cloudflare.com`, which a pilot saw blocked from a run (`sandbox-allowlist`). Not verified from a run.
- *Containers MCP server.* Cloudflare lists a Container MCP server at `https://containers.mcp.cloudflare.com/mcp` ("spin up a sandbox development environment"), alongside its API server at `https://mcp.cloudflare.com/mcp`, with OAuth or an API token as a bearer. <!-- verified: https://developers.cloudflare.com/agents/model-context-protocol/mcp-servers-for-cloudflare/ 2026-10-31 --> As a remote server it could be added as a connector, and connector traffic does not use the run's network. Using it from a routine is not verified.

**Limits.** A sandbox sleeps after 10 minutes idle by default (`sleepAfter`). When the container stops, all files are deleted; state exists only while the container is active. <!-- verified: https://developers.cloudflare.com/sandbox/sdk/concepts/sandboxes/ 2026-10-31 --> Return everything the routine needs in the response.

**Cost.** Workers Paid is $5 a month and includes 25 GiB-hours of memory, 375 vCPU-minutes and 200 GB-hours of disk a month. Beyond that: $0.000020 per vCPU-second, $0.0000025 per GiB-second of memory and $0.00000007 per GB-second of disk, plus egress ($0.025 per GB in North America and Europe after 1 TB). Instance types run from `lite` (1/16 vCPU, 256 MiB, 2 GB disk) to `standard-4` (4 vCPU, 12 GiB, 20 GB). <!-- verified: https://developers.cloudflare.com/containers/pricing/ 2026-10-31 -->

### Cloudflare Workers

**What it is.** Serverless functions. For a routine, a Worker is the simplest **endpoint you own**: a small API on your domain that holds a provider key, calls the provider, and hands the routine plain JSON.

**When a routine would use it.** To put any SDK-only provider (Modal, E2B, Vercel) behind one host with one network secret, or to do a short fetch the run's network blocks.

**Limits and cost.** Free allows 100,000 requests a day with 10 ms of CPU per invocation; Paid starts at $5 a month with 10 million requests a month included. <!-- verified: https://developers.cloudflare.com/workers/platform/pricing/ 2026-10-31 --> 10 ms of CPU is enough to relay a call, not to do the work, so heavy jobs belong in a container or another provider.

### E2B

**What it is.** A Linux VM created on demand for an agent, with pause and resume, driven from the `e2b` package for JavaScript and Python (and `e2b-code-interpreter` for running code cells). The SDK reads `E2B_API_KEY`. <!-- verified: https://docs.e2b.dev/ 2026-10-31 -->

```python
from e2b import Sandbox

sandbox = Sandbox.create()
result = sandbox.commands.run('echo "Hello from E2B Sandbox!"')
print(result.stdout)
```

<!-- verified: https://docs.e2b.dev/ 2026-10-31 -->

**When a routine would use it.** Running generated code with a code interpreter, where you want pause and resume between runs.

**How a routine reaches it.** Through an endpoint you own. E2B's own MCP server repository says it is no longer actively maintained and was archived on 16 April 2026. <!-- verified: https://github.com/e2b-dev/mcp-server 2026-10-31 --> The MCP server on E2B's docs site is for searching the docs, not for running sandboxes. <!-- verified: https://docs.e2b.dev/ 2026-10-31 --> A plain HTTP API for sandboxes was not checked here (not verified).

**Limits.** A sandbox runs up to 24 hours on Pro or 1 hour on Hobby; set a shorter `timeout` on create (seconds in Python) or with `set_timeout`. Pausing resets the runtime window. <!-- verified: https://docs.e2b.dev/sandbox 2026-10-31 -->

**Cost.** Hobby is $0 with a one-time $100 credit, 1-hour sessions and 20 concurrent sandboxes. Pro is $150 a month, with 24-hour sessions and 100 concurrent. Usage is $0.000014 per vCPU-second and $0.0000045 per GiB-second. <!-- verified: https://e2b.dev/pricing 2026-10-31 -->

### A rough cost comparison

Ten minutes of 1 vCPU (or 1 core, for Modal) and 1 GiB of memory, at list price before free credits, from the rates above:

| Provider | CPU | Memory | Total |
|---|---|---|---|
| Daytona | $0.0084 | $0.0027 | about $0.011 |
| Modal | $0.0237 | $0.0040 | about $0.028 |
| Cloudflare Containers, past the included usage | $0.0120 | $0.0015 | about $0.014 |
| E2B | $0.0084 | $0.0027 | about $0.011 |

This is arithmetic on published rates, not a measured bill. Minimum sizes, rounding, storage, egress and plan fees are left out and differ by provider. For a routine that runs twice a week, every one of these is well inside its free allowance; the cost that matters is a sandbox left running.

## Worked example

Hiring Signals needs no sandbox. Its code stages are `routineos.py learn` and `write`, local Python scripts inside the cloud run.

Suppose a later version, `site-scan`, checks each starred company's site with a headless crawler too heavy for a setup script. A design on Daytona's REST API (illustrative; not run):

1. **Declare it.** Add the `[[integration]]` and two `[[secret]]` entries shown above to `app.toml`, and add the two network secrets in the claude.ai environment dialog, each with the Daytona API key, one per host. `lint` checks the manifest; it cannot see the environment.

2. **Write the script.** `lib/crawl_remote.py` uses only `urllib`. It sends `POST https://app.daytona.io/api/sandbox` to create an ephemeral sandbox, `POST https://proxy.app.daytona.io/toolbox/<id>/process/execute` with `{"command": "python3 crawl.py https://shop.example", "timeout": 300}`, reads the result, and prints JSON. It sends no `Authorization` header; the network secret adds it. On any error it prints `error: ...` and exits 2, never echoing a response body that might hold a key.

3. **Probe first.** The first report-only run does one thing: create a sandbox, run `echo ok`, and record the outcome. Write the result, with the date, to `risks.md`:

   ```
   - daytona (app.daytona.io, proxy.app.daytona.io): probed 2026-11-03 from a report-only run, <result>
   ```

   If the probe is refused, the feature waits. Do not move the key into an environment variable to make it work.

4. **Call it from the prompt.** One step, by path, with every flag:

   ```
   python3 lib/crawl_remote.py --domains domains.json --out crawl.json
   ```

   The model reads `crawl.json`, records what it found in `run.json`, and `routineos.py write` writes the store. The sandbox never touches the store.

5. **Cap the spend.** Set a spend limit in Daytona. Keep `max_runs_per_day = 1`. Mark the README: "Runtime: Daytona sandbox via REST, last tested <date>".

The same shape works for any provider behind a Worker you own: swap the two Daytona hosts for your one host, and the script's calls for your endpoint's.

## Common pitfalls

- **Reaching for a sandbox first.** Try the cloud run, a setup script and a connector.
- **Putting a provider key in an environment variable.** Anyone who uses the environment can read it, and `lint` refuses `where = "env-var"`. Use a network secret scoped to one host, or a connector.
- **One secret for two hosts.** A non-admin network secret names one host. Daytona's API and toolbox are on different hosts.
- **Assuming the run's network can reach the provider.** Probe it; `api.cloudflare.com` was blocked in a pilot.
- **Using a local MCP server from a cloud run.** It exists on your machine, not in the run.
- **Leaving sandboxes running.** Make them ephemeral or short-timeout, and stop them in the script's error path too.
- **Letting the sandbox write the store directly.** Results come back to the run, and the write contract applies them.
- **Trusting what comes back.** Output from a sandbox is data, like a fetched page. It never changes what the routine does.

## Related

- [Scheduling and Claude cloud routines](/infrastructure/scheduling)
- [Connectors and MCP](/infrastructure/connectors-and-mcp)
- [Code routines](/routine-kinds/code)
- [Storage](/infrastructure/storage)
