---
description: Build secure, isolated code execution environments powered by Cloudflare Workers and Containers.
title: Sandbox SDK
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Sandbox SDK

Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Build secure, isolated code execution environments

Available on Workers Paid plan

Sandbox SDK 1.0 preview

These pages document the current stable `@cloudflare/sandbox` package. The next major release is **Sandbox SDK 1.0**, available as a preview on `@cloudflare/sandbox@next`.

We recommend starting new projects on the preview, and migrating existing apps when you can, so you are ready when 1.0 becomes the stable release. Refer to the [1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/) section for install, concepts, API reference, and migration.

The Sandbox SDK enables you to run untrusted code securely in isolated environments. Built on [Containers](https://developers.cloudflare.com/containers/), Sandbox SDK provides a simple API for executing commands, managing files, running background processes, and exposing services — all from your [Workers](https://developers.cloudflare.com/workers/) applications.

Sandboxes are ideal for building AI agents that need to execute code, interactive development environments, data analysis platforms, CI/CD systems, and any application that needs secure code execution at the edge. Each sandbox runs in its own isolated container with a full Linux environment, providing strong security boundaries while maintaining performance.

With Sandbox, you can execute Python scripts, run Node.js applications, analyze data, compile code, and perform complex computations — all with a simple TypeScript API and no infrastructure to manage.

```typescript
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const sandbox = getSandbox(env.Sandbox, 'user-123');

		// Execute a command and get the result
		const result = await sandbox.exec('python --version');

		return Response.json({
			output: result.stdout,
			exitCode: result.exitCode,
			success: result.success
		});
	}
};
```

```typescript
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const sandbox = getSandbox(env.Sandbox, 'user-123');

		// Create a Python execution context
		const ctx = await sandbox.createCodeContext({ language: 'python' });

		// Execute Python code with automatic result capture
		const result = await sandbox.runCode(`
import pandas as pd
data = {'product': ['A', 'B', 'C'], 'sales': [100, 200, 150]}
df = pd.DataFrame(data)
df['sales'].sum()  # Last expression is automatically returned
	`, { context: ctx });

			return Response.json({
				result: result.results?.[0]?.text,
				logs: result.logs
			});
		}
	};
```

```typescript
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const sandbox = getSandbox(env.Sandbox, 'user-123');

		// Create a project structure
		await sandbox.mkdir('/workspace/project/src', { recursive: true });

		// Write files
		await sandbox.writeFile(
			'/workspace/project/package.json',
			JSON.stringify({ name: 'my-app', version: '1.0.0' })
		);

		// Read a file back
		const content = await sandbox.readFile('/workspace/project/package.json');

		return Response.json({ content });
	}
};
```

```typescript
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const sandbox = getSandbox(env.Sandbox, 'user-123');

		// Watch for file changes in real-time
		const watcher = await sandbox.watch('/workspace/src', {
			include: ['*.js', '*.ts'],
			onEvent: (event) => {
				console.log(`${event.type}: ${event.path}`);
				if (event.type === 'modify') {
					// Trigger rebuild or hot reload
					console.log('Code changed, recompiling...');
				}
			},
			onError: (error) => {
				console.error('Watch error:', error);
			}
		});

		// Stop watching when done
		setTimeout(() => watcher.stop(), 60000);

		return Response.json({ message: 'File watcher started' });
	}
};
```

```typescript
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		// Terminal WebSocket connection
		if (url.pathname === '/ws/terminal') {
			const sandbox = getSandbox(env.Sandbox, 'user-123');
			return sandbox.terminal(request, { cols: 80, rows: 24 });
		}

		return Response.json({ message: 'Terminal endpoint' });
	}
};
```

Connect browser terminals directly to sandbox shells via WebSocket. Learn more: [Browser terminals](https://developers.cloudflare.com/sandbox/guides/browser-terminals/).

```typescript
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		// Connect to WebSocket services in sandbox
		if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') {
			const sandbox = getSandbox(env.Sandbox, 'user-123');
			return await sandbox.wsConnect(request, 8080);
		}

		return Response.json({ message: 'WebSocket endpoint' });
	}
};
```

Connect to WebSocket servers running in sandboxes. Learn more: [WebSocket Connections](https://developers.cloudflare.com/sandbox/guides/websocket-connections/).

[Get started](https://developers.cloudflare.com/sandbox/get-started/) [API Reference](https://developers.cloudflare.com/sandbox/api/) 

---

## Features

[Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/)

Deploy your Worker and keep the npm package and container image on the same release line.

Deploy a Sandbox app

[Execute commands securely](https://developers.cloudflare.com/sandbox/guides/execute-commands/)

Run shell commands, Python scripts, Node.js applications, and more with streaming output support and automatic timeout handling.

Learn about command execution

[Manage files and processes](https://developers.cloudflare.com/sandbox/guides/manage-files/)

Read, write, and manipulate files in the sandbox filesystem. Run background processes, monitor output, and manage long-running operations.

Learn about file operations

[Expose services with preview URLs](https://developers.cloudflare.com/sandbox/guides/expose-services/)

Expose HTTP services running in your sandbox with automatically generated preview URLs, perfect for interactive development environments and application hosting.

Learn about preview URLs

[Execute code directly](https://developers.cloudflare.com/sandbox/guides/code-execution/)

Execute Python and JavaScript code with rich outputs including charts, tables, and images. Maintain persistent state between executions for AI-generated code and interactive workflows.

Learn about code execution

[Build interactive terminals](https://developers.cloudflare.com/sandbox/guides/browser-terminals/)

Create browser-based terminal interfaces that connect directly to sandbox shells via WebSocket. Build collaborative terminals, interactive development environments, and real-time shell access with automatic reconnection.

Learn about terminal UIs

[Persistent storage with object storage](https://developers.cloudflare.com/sandbox/guides/mount-buckets/)

Mount S3-compatible object storage (R2, S3, GCS, and more) as local filesystems. Access buckets using standard file operations with data that persists across sandbox lifecycles. Production deployment required.

Learn about bucket mounting

[Watch files for real-time changes](https://developers.cloudflare.com/sandbox/guides/file-watching/)

Monitor files and directories for changes using native filesystem events. Perfect for building hot reloading development servers, build automation systems, and configuration monitoring tools.

Learn about file watching

[Handle outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/)

Block, allow, and intercept outbound HTTP from sandboxes. Keep credentials in your Worker by injecting authorization headers in outbound handlers.

Learn about outbound traffic

---

## Use Cases

Build powerful applications with Sandbox:

### AI Code Execution

Execute code generated by Large Language Models safely and reliably. Native integration with [Workers AI](https://developers.cloudflare.com/workers-ai/) models like GPT-OSS enables function calling with sandbox execution. Perfect for AI agents, code assistants, and autonomous systems that need to run untrusted code.

### Data Analysis & Notebooks

Create interactive data analysis environments with pandas, NumPy, and Matplotlib. Generate charts, tables, and visualizations with automatic rich output formatting.

### Interactive Development Environments

Build cloud IDEs, coding playgrounds, and collaborative development tools with full Linux environments and preview URLs.

### CI/CD & Build Systems

Run tests, compile code, and execute build pipelines in isolated environments with parallel execution and streaming logs.

---

## Related products

[Containers](https://developers.cloudflare.com/containers/)

Serverless container runtime that powers Sandbox, enabling you to run any containerized workload on the edge.

[Workers AI](https://developers.cloudflare.com/workers-ai/)

Run machine learning models and LLMs on the network. Combine with Sandbox for secure AI code execution workflows.

[Durable Objects](https://developers.cloudflare.com/durable-objects/)

Stateful coordination layer that enables Sandbox to maintain persistent environments with strong consistency.

---

## More resources

## Coding agents

Install [Cloudflare Skills ↗](https://github.com/cloudflare/skills) for your agent ([Agent setup](https://developers.cloudflare.com/agent-setup/)). Use **`sandbox-stable`** with the main docs on this site while you are on the current stable package. Use **`sandbox-next`** for `@cloudflare/sandbox@next` (recommended for new projects). When you are ready to port an existing app, use **`sandbox-migrate-to-next`**.

### [Tutorials](https://developers.cloudflare.com/sandbox/tutorials/)

Explore complete examples including AI code execution, data analysis, and interactive environments.

### [Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/)

Deploy and keep package and image aligned.

### [How-to Guides](https://developers.cloudflare.com/sandbox/guides/)

Learn how to solve specific problems and implement features with the Sandbox SDK.

### [1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/)

Install `@cloudflare/sandbox@next` and prepare for the Sandbox SDK 1.0 release.

### [API reference](https://developers.cloudflare.com/sandbox/api/)

Explore the complete API documentation for the Sandbox SDK.

### [Concepts](https://developers.cloudflare.com/sandbox/concepts/)

Learn about the key concepts and architecture of the Sandbox SDK.

### [Configuration](https://developers.cloudflare.com/sandbox/configuration/)

Learn about the configuration options for the Sandbox SDK.

### [GitHub Repository](https://github.com/cloudflare/sandbox-sdk)

View the SDK source code, report issues, and contribute to the project.

### [Pricing](https://developers.cloudflare.com/sandbox/platform/pricing/)

Understand Sandbox pricing based on the underlying Containers platform.

### [Limits](https://developers.cloudflare.com/sandbox/platform/limits/)

Learn about resource limits, quotas, and best practices for working within them.

### [Discord Community](https://discord.cloudflare.com)

Connect with the community on Discord. Ask questions, share what you're building, and get help from other developers.

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/sandbox/#page","headline":"Overview · Cloudflare Sandbox SDK docs","description":"Build secure, isolated code execution environments powered by Cloudflare Workers and Containers.","url":"https://developers.cloudflare.com/sandbox/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Create your first Sandbox SDK Worker to execute Python code in isolated containers.
title: Getting started
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Getting started

Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/get-started/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Build your first application with Sandbox SDK - a secure code execution environment. In this guide, you'll create a Worker that can execute Python code and work with files in isolated containers.

Coming soon: Sandbox SDK 1.0

This guide uses today's stable `@cloudflare/sandbox` package.

For **new projects**, we recommend the [1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/) on `@cloudflare/sandbox@next` so you start on the APIs that become Sandbox SDK 1.0\. Refer to [Get started with the 1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/get-started/).

Coding agents: install [Cloudflare Skills ↗](https://github.com/cloudflare/skills) ([Agent setup](https://developers.cloudflare.com/agent-setup/)). Use **`sandbox-stable`** with this guide; use **`sandbox-next`** for `@next`; use **`sandbox-migrate-to-next`** when porting.

What you're building

A simple API that can safely execute Python code and perform file operations in isolated sandbox environments.

## Prerequisites

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

Node.js version manager

Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), discussed later in this guide, requires a Node version of `16.17.0` or later.

### Ensure Docker is running locally

Sandbox SDK uses [Docker ↗](https://www.docker.com/) to build container images alongside your Worker.

You must have Docker running locally when you run `wrangler deploy`. For most people, the best way to install Docker is to follow the [docs for installing Docker Desktop ↗](https://docs.docker.com/desktop/). Other tools like [Colima ↗](https://github.com/abiosoft/colima) may also work.

You can check that Docker is running properly by running the `docker info` command in your terminal. If Docker is running, the command will succeed. If Docker is not running, the `docker info` command will hang or return an error including the message "Cannot connect to the Docker daemon".

## 1\. Create a new project

Create a new Sandbox SDK project:

npmyarnpnpm

```
npm create cloudflare@latest -- my-sandbox --template=cloudflare/sandbox-sdk/examples/minimal
```

```
yarn create cloudflare my-sandbox --template=cloudflare/sandbox-sdk/examples/minimal
```

```
pnpm create cloudflare@latest my-sandbox --template=cloudflare/sandbox-sdk/examples/minimal
```

This creates a `my-sandbox` directory with everything you need:

* `src/index.ts` \- Worker with sandbox integration
* `wrangler.jsonc` \- Configuration for Workers and Containers
* `Dockerfile` \- Container environment definition

```sh
cd my-sandbox
```

## 2\. Explore the template

The template provides a minimal Worker that demonstrates core sandbox capabilities:

```typescript
import { getSandbox, proxyToSandbox, type Sandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

type Env = {
	Sandbox: DurableObjectNamespace<Sandbox>;
};

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		// Get or create a sandbox instance. For user-facing apps,
		// derive this ID from the authenticated user.
		const sandbox = getSandbox(env.Sandbox, "my-sandbox");

		// Execute Python code
		if (url.pathname === "/run") {
			const result = await sandbox.exec('python3 -c "print(2 + 2)"');
			return Response.json({
				output: result.stdout,
				error: result.stderr,
				exitCode: result.exitCode,
				success: result.success,
			});
		}

		// Work with files
		if (url.pathname === "/file") {
			await sandbox.writeFile("/workspace/hello.txt", "Hello, Sandbox!");
			const file = await sandbox.readFile("/workspace/hello.txt");
			return Response.json({
				content: file.content,
			});
		}

		return new Response("Try /run or /file");
	},
};
```

**Key concepts**:

* `getSandbox()` \- Gets or creates a sandbox instance by ID. Use a stable ID to reconnect to the same sandbox. In user-facing apps, scope IDs to a single user.
* `sandbox.exec()` \- Execute shell commands in the sandbox and capture stdout, stderr, and exit codes.
* `sandbox.writeFile()` / `readFile()` \- Write and read files in the sandbox filesystem.

## 3\. Test locally

Start the development server:

```sh
npm run dev
# If you expect to have multiple sandbox instances, you can increase `max_instances`.
```

Note

First run builds the Docker container (2-3 minutes). Subsequent runs are much faster due to caching.

Test the endpoints:

```sh
# Execute Python code
curl http://localhost:8787/run

# File operations
curl http://localhost:8787/file
```

You should see JSON responses with the command output and file contents.

## 4\. Deploy to production

Deploy your Worker and container:

```sh
npx wrangler deploy
```

This will:

1. Build your container image using Docker
2. Push it to Cloudflare's Container Registry
3. Deploy your Worker globally

Wait for provisioning

After the first deployment, wait several minutes before you expect sandbox requests to succeed. The Worker deploys immediately, but the container image still has to provision.

Check deployment status:

```sh
npx wrangler containers list
```

## 5\. Test your deployment

Visit your Worker URL (shown in deploy output):

```sh
# Replace with your actual URL
curl https://my-sandbox.YOUR_SUBDOMAIN.workers.dev/run
```

Your sandbox is now deployed and can execute code in isolated containers.

## Understanding the configuration

Your `wrangler.jsonc` connects three pieces together:

```jsonc
{
	"containers": [
		{
			"class_name": "Sandbox",
			"image": "./Dockerfile",
			"instance_type": "lite",
			"max_instances": 1,
		},
	],
	"durable_objects": {
		"bindings": [
			{
				"class_name": "Sandbox",
				"name": "Sandbox",
			},
		],
	},
	"migrations": [
		{
			"new_sqlite_classes": ["Sandbox"],
			"tag": "v1",
		},
	],
}
```

```toml
[[containers]]
class_name = "Sandbox"
image = "./Dockerfile"
instance_type = "lite"
max_instances = 1

[[durable_objects.bindings]]
class_name = "Sandbox"
name = "Sandbox"

[[migrations]]
new_sqlite_classes = [ "Sandbox" ]
tag = "v1"
```

* **containers** \- Defines the [container image, instance type, and resource limits](https://developers.cloudflare.com/workers/wrangler/configuration/#containers) for your sandbox environment. If you expect to have multiple sandbox instances, you can increase `max_instances`.
* **durable\_objects** \- You need not be familiar with [Durable Objects](https://developers.cloudflare.com/durable-objects) to use Sandbox SDK, but if you'd like, you can [learn more about Cloudflare Containers and Durable Objects](https://developers.cloudflare.com/containers/get-started/#each-container-is-backed-by-its-own-durable-object). This configuration creates a [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings#what-is-a-binding) that makes the `Sandbox` Durable Object accessible in your Worker code.
* **migrations** \- Registers the `Sandbox` class, implemented by the Sandbox SDK, with [SQLite storage backend](https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage) (required once)

For detailed configuration options including environment variables, secrets, and custom images, see the [Wrangler configuration reference](https://developers.cloudflare.com/sandbox/configuration/wrangler/).

## Next steps

Now that you have a working sandbox, explore more capabilities:

* [Code interpreter with Workers AI](https://developers.cloudflare.com/sandbox/tutorials/workers-ai-code-interpreter/) \- Build an AI-powered code execution system
* [Execute commands](https://developers.cloudflare.com/sandbox/guides/execute-commands/) \- Run shell commands and stream output
* [Manage files](https://developers.cloudflare.com/sandbox/guides/manage-files/) \- Work with files and directories
* [Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/) \- Deploy and keep package and image aligned
* [Expose services](https://developers.cloudflare.com/sandbox/guides/expose-services/) \- Get public URLs for services running in your sandbox
* [Quick tunnels](https://developers.cloudflare.com/sandbox/api/tunnels/) \- Zero-config `*.trycloudflare.com` URLs for development and `.workers.dev` deployments
* [Configure preview URLs on a custom domain](https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/) \- Wildcard DNS and TLS for `exposePort()`
* [API reference](https://developers.cloudflare.com/sandbox/api/) \- Complete API documentation

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/get-started/#page","headline":"Getting started · Cloudflare Sandbox SDK docs","description":"Create your first Sandbox SDK Worker to execute Python code in isolated containers.","url":"https://developers.cloudflare.com/sandbox/get-started/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Install @cloudflare/sandbox@next — a thinner Sandbox SDK on Cloudflare Containers — and migrate when you are ready for Sandbox SDK 1.0.
title: 1.0 preview
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# 1.0 preview

Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

**Sandbox SDK 1.0** is the next major release of the SDK. It is available now as a preview on the npm `@next` tag. The current stable package remains published for existing apps.

Sandbox still runs isolated work on [Cloudflare Containers](https://developers.cloudflare.com/containers/). The 1.0 preview is a **thinner** SDK on that foundation: one process handle for short and long-running work, no session-based command state, no transport picker, terminals as first-class PTYs, and the code interpreter as an opt-in extension.

We recommend that **new projects** start on `@cloudflare/sandbox@next` and follow this section. **Existing apps** should migrate when you can, so you are ready when 1.0 becomes the stable release. Follow [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/).

The main [Sandbox documentation](https://developers.cloudflare.com/sandbox/) still documents today's stable package. Use **this** section for preview APIs and the migration path.

Self-deployed bridge

The self-deployed Sandbox bridge is not part of the 1.0 preview. Use the [stable bridge](https://developers.cloudflare.com/sandbox/bridge/) with the matching stable package and container image.

## Install the preview

npmyarnpnpmbun

```
npm i @cloudflare/sandbox@next
```

```
yarn add @cloudflare/sandbox@next
```

```
pnpm add @cloudflare/sandbox@next
```

```
bun add @cloudflare/sandbox@next
```

Deploy the Worker package and the sandbox container image from the **same** preview line. Do not mix a preview Worker package with a stable container image (or the reverse). For ongoing deploys, refer to [Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/). For a breaking cutover, refer to [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/).

## What 1.0 is aiming at

The stable package grew several ways to run commands (`exec`, `startProcess`, `execStream`), optional session state across launches, and selectable transports between the Durable Object and the container. That surface worked, but it duplicated ideas and hid how sandboxes actually behave on containers.

The preview collapses that toward a smaller contract:

| You want…                                      | In the preview                                                                                                            |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Run a program                                  | exec(argv) → process handle when **launch** succeeds                                                                      |
| See output or wait for readiness               | output(), logs(), waitForExit(), waitForLog(), waitForPort() on the handle                                                |
| Stop a process                                 | kill(signal?) (numeric signal; default 15)                                                                                |
| Keep shell state across many interactive steps | A [terminal](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/) (PTY), not a hidden default session        |
| Run Python / JS cells                          | [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/) extension on your Sandbox subclass |
| Talk to the container control plane            | Always RPC — no transport setting                                                                                         |

Procedures: [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/). Mental model: [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/) and [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/).

## What changes from the stable package

### Command execution

**Stable:** `sandbox.exec(string)` resolves when the command **finishes** with buffered output. Long-running services and streaming use separate APIs (`startProcess`, `execStream`).

**Preview:** `sandbox.exec()` takes **argv** and resolves when the process **starts**. The same handle covers short commands and long-running services.

```js
// Current stable package
const result = await sandbox.exec("npm test");
console.log(result.stdout, result.exitCode);

// 1.0 preview
const process = await sandbox.exec(["npm", "test"]);
const result = await process.output({ encoding: "utf8" });
console.log(result.stdout, result.exitCode);
```

```ts
// Current stable package
const result = await sandbox.exec("npm test");
console.log(result.stdout, result.exitCode);

// 1.0 preview
const process = await sandbox.exec(["npm", "test"]);
const result = await process.output({ encoding: "utf8" });
console.log(result.stdout, result.exitCode);
```

Shell features such as pipes and `&&` need an explicit shell, for example `['/bin/bash', '-lc', 'cd app && npm test']`. Pass `cwd` and `env` on each `exec()` when the process needs them. Details: [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/), [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/).

### Sessions

**Stable:** a default session can preserve working directory and environment variables across `exec()` calls. Apps can also create named sessions with `createSession()`.

**Preview:** no session execution on the SDK. Each `exec()` is independent. Pass `cwd` and `env` on each launch, or put multi-step shell syntax in one explicit shell argv. Isolate end users with **separate sandboxes**, not sessions inside one sandbox. Environment model: [Environment variables](https://developers.cloudflare.com/sandbox/1-0-preview/environment/).

### Terminals

**Stable:** browser shells often use `sandbox.terminal(request)` with session helpers and xterm `sessionId`.

**Preview:** terminals are PTY resources — `createTerminal`, `getTerminal`, `listTerminals`, and `terminal.connect(request)`. The xterm helper uses `terminalId`. Refer to [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/).

### Code interpreter

**Stable:** interpreter methods live on `Sandbox`.

**Preview:** attach the interpreter on your subclass, then call `sandbox.interpreter.*`. Refer to [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/).

### Transport configuration

**Stable:** apps can select HTTP, WebSocket, or RPC between the Durable Object and the container.

**Preview:** the SDK always uses RPC. Remove `SANDBOX_TRANSPORT`, the `transport` option on `getSandbox()`, and `setTransport()`. No replacement setting is required.

## Same platform model, clearer handles

This is **not** a new container product. You still address a sandbox with a stable **sandbox ID**:

```js
const sandbox = getSandbox(env.Sandbox, "user-123");
```

```ts
const sandbox = getSandbox(env.Sandbox, "user-123");
```

That sandbox runs in a **container**. The ID is stable. The container instance behind it is not always the same one. Processes and terminals you start exist only in the **current** container. When that container stops or is replaced, those processes and terminals are gone — old handles fail closed instead of quietly attaching to a new container for the same sandbox ID.

Container stop and replace already happened on the stable line. The preview makes process and terminal APIs honest about that lifetime. Full model: [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/). Process detail: [How long a process lives](https://developers.cloudflare.com/sandbox/1-0-preview/processes/#how-long-a-process-lives). Recovery: [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

## What usually stays the same

These remain available. Use the main Sandbox documentation for signatures, and ignore session or transport options where those pages still mention them:

* [Files](https://developers.cloudflare.com/sandbox/api/files/) and [file watching](https://developers.cloudflare.com/sandbox/api/file-watching/)
* [Storage](https://developers.cloudflare.com/sandbox/api/storage/) and [backups](https://developers.cloudflare.com/sandbox/api/backups/)
* [Ports](https://developers.cloudflare.com/sandbox/api/ports/) and [tunnels](https://developers.cloudflare.com/sandbox/api/tunnels/)
* [Lifecycle options](https://developers.cloudflare.com/sandbox/api/lifecycle/) and [sandbox options](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/) (except removed session/transport fields)
* [Outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/) (credential injection and egress policy)

For process environment on `@next`, use [Environment variables](https://developers.cloudflare.com/sandbox/1-0-preview/environment/) in this section.

## Start here

### [Get started](https://developers.cloudflare.com/sandbox/1-0-preview/get-started/)

Install `@next` and run your first process handle.

### [Migrate from stable](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)

Update an existing app, including deploy cutover on `@next`.

### [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/)

Sandbox ID, container, stop, replace, and what your app should store.

### [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/)

How `exec()` works, process handles, and how long processes live.

### [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/)

Interactive PTYs, lifetime, and browser connect.

### [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/)

How to retry, inspect, and relaunch after common failures.

### [Environment variables](https://developers.cloudflare.com/sandbox/1-0-preview/environment/)

`setEnvVars`, per-launch `env`, and how processes get their environment.

### [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/)

Attach the interpreter extension and run Python or JS/TS.

### [API reference](https://developers.cloudflare.com/sandbox/1-0-preview/api/)

Process, terminal, error, and interpreter signatures for `@next`.

### [Extensions](https://developers.cloudflare.com/sandbox/1-0-preview/extensions/)

Attach the code interpreter and other optional capabilities.

### [Troubleshooting](https://developers.cloudflare.com/sandbox/1-0-preview/troubleshooting/)

Common `@next` failures and where to fix them.

## Coding agents

Install [Cloudflare Skills ↗](https://github.com/cloudflare/skills) for your agent ([Agent setup](https://developers.cloudflare.com/agent-setup/)). Use **`sandbox-next`** for work on `@next` (recommended for new projects). Existing apps on the current stable package should use **`sandbox-stable`** until you are ready to move, then **`sandbox-migrate-to-next`**. Deprecated-API cleanup while staying on stable is covered in the [2026 deprecation guide](https://developers.cloudflare.com/sandbox/guides/2026-deprecation/) and **`sandbox-stable`**.

## Stable documentation

While you remain on the current stable package, use the main docs:

* [Get started](https://developers.cloudflare.com/sandbox/get-started/)
* [Commands](https://developers.cloudflare.com/sandbox/api/commands/)
* [Sessions](https://developers.cloudflare.com/sandbox/concepts/sessions/)
* [2026 deprecation migration](https://developers.cloudflare.com/sandbox/guides/2026-deprecation/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/#page","headline":"Overview · Cloudflare Sandbox SDK docs","description":"Install @cloudflare/sandbox@next — a thinner Sandbox SDK on Cloudflare Containers — and migrate when you are ready for Sandbox SDK 1.0.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: API reference for @cloudflare/sandbox@next — processes, terminals, errors, and related preview surfaces.
title: API reference
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# API reference

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/api/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This section documents APIs on `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0\. For today's stable package, refer to [API reference](https://developers.cloudflare.com/sandbox/api/).

Reference for the preview public surface. Start with the mental model pages when you need _why_. Use these pages for signatures and types.

### [Processes](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/)

`exec`, process handles, logs, waits, and kill.

### [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/)

`createTerminal`, handles, output, connect, interrupt, and terminate.

### [Errors](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/)

Error classes, codes, and recommended fixes.

### [Interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/api/interpreter/)

`withInterpreter`, contexts, `runCode`, and results.

## Other API surfaces

Files, mounts, backups, ports, tunnels, and related options remain available. Use the main reference for signatures:

* [Files](https://developers.cloudflare.com/sandbox/api/files/) and [file watching](https://developers.cloudflare.com/sandbox/api/file-watching/)
* [Storage](https://developers.cloudflare.com/sandbox/api/storage/) and [backups](https://developers.cloudflare.com/sandbox/api/backups/)
* [Ports](https://developers.cloudflare.com/sandbox/api/ports/) and [tunnels](https://developers.cloudflare.com/sandbox/api/tunnels/)
* [Lifecycle](https://developers.cloudflare.com/sandbox/api/lifecycle/) and [sandbox options](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/)
* [Outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/)

Where those pages still describe sessions or transport selection, that guidance does not apply on `@next`.

## Related concepts and guides

* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/)
* [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/)
* [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/)
* [Environment variables](https://developers.cloudflare.com/sandbox/1-0-preview/environment/)
* [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/)
* [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/)
* [Troubleshooting](https://developers.cloudflare.com/sandbox/1-0-preview/troubleshooting/)
* [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/api/#page","headline":"API reference · Cloudflare Sandbox SDK docs","description":"API reference for @cloudflare/sandbox@next — processes, terminals, errors, and related preview surfaces.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/api/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Error classes, codes, and context fields for @cloudflare/sandbox@next.
title: Errors
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Errors

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page is the error reference for `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0\. When to retry or relaunch: [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

Error classes and codes returned by the Sandbox SDK 1.0 preview, with short recommended actions. For full recovery procedures, refer to [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

## How errors are returned

Operations throw exceptions you can catch. Prefer `instanceof` on classes from `@cloudflare/sandbox`. Use `code` and `context` for metrics and stable field access.

```js
import {
	ContainerUnavailableError,
	OperationInterruptedError,
	RPCTransportError,
} from "@cloudflare/sandbox";

try {
	await sandbox.exec(["npm", "test"], { cwd: "/workspace/app" });
} catch (error) {
	if (error instanceof ContainerUnavailableError) {
		// error.code === "CONTAINER_UNAVAILABLE"
		// error.context.reason, error.context.retryAfterMs
	}
	if (error instanceof OperationInterruptedError) {
		// Convenience getters: error.reason, error.retryable, error.operationName
		// admitted is only on context: error.context.admitted
	}
	if (error instanceof RPCTransportError) {
		// Convenience getters: error.kind, error.originalMessage
	}
}
```

```ts
import {
	ContainerUnavailableError,
	OperationInterruptedError,
	RPCTransportError,
} from "@cloudflare/sandbox";

try {
	await sandbox.exec(["npm", "test"], { cwd: "/workspace/app" });
} catch (error) {
	if (error instanceof ContainerUnavailableError) {
		// error.code === "CONTAINER_UNAVAILABLE"
		// error.context.reason, error.context.retryAfterMs
	}
	if (error instanceof OperationInterruptedError) {
		// Convenience getters: error.reason, error.retryable, error.operationName
		// admitted is only on context: error.context.admitted
	}
	if (error instanceof RPCTransportError) {
		// Convenience getters: error.kind, error.originalMessage
	}
}
```

### Imports

Common lifecycle, process, terminal, and backup errors are available from the package root:

```js
import {
	ContainerUnavailableError,
	OperationInterruptedError,
	RPCTransportError,
	StaleProcessHandleError,
	// ...
} from "@cloudflare/sandbox";
```

```ts
import {
	ContainerUnavailableError,
	OperationInterruptedError,
	RPCTransportError,
	StaleProcessHandleError,
	// ...
} from "@cloudflare/sandbox";
```

The full module also exports `ErrorCode`, `SandboxError`, `createErrorFromResponse`, and other domain errors (files, ports, interpreter, mounts, and related context types):

```js
import {
	ErrorCode,
	SandboxError,
	createErrorFromResponse,
	FileNotFoundError,
	// ...
} from "@cloudflare/sandbox/errors";
```

```ts
import {
	ErrorCode,
	SandboxError,
	createErrorFromResponse,
	FileNotFoundError,
	// ...
} from "@cloudflare/sandbox/errors";
```

Platform helpers (not `SandboxError` subclasses):

```js
import {
	isPlatformTransientError,
	isDurableObjectCodeUpdateReset,
} from "@cloudflare/sandbox";
```

```ts
import {
	isPlatformTransientError,
	isDurableObjectCodeUpdateReset,
} from "@cloudflare/sandbox";
```

### `SandboxError` shape

Most SDK errors extend `SandboxError`:

| Member     | Description                                                  |
| ---------- | ------------------------------------------------------------ |
| name       | Class name (for example ContainerUnavailableError)           |
| message    | Human-readable message                                       |
| code       | Stable ErrorCode string (for example CONTAINER\_UNAVAILABLE) |
| context    | Structured fields for the error type                         |
| httpStatus | Mapped HTTP status when applicable                           |
| operation  | Operation label when provided                                |
| suggestion | Optional actionable suggestion                               |
| timestamp  | ISO timestamp when provided                                  |
| toJSON()   | Serializes the error fields for logs                         |

`RuntimeIdentityInactiveError` extends `Error` directly (not `SandboxError`). It means the current container is no longer the active one for this handle or call.

Tables include a **Recommended fix** column. For longer recovery procedures, refer to [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

Availability errors and deployment mismatch errors are listed in separate sections. Do not use the same retry loop for both.

---

## Container availability and interrupted calls

These errors come from ordinary start, idle stop, replace, or lost contact while a call is running.

| Class                        | Code                   | Key context                                  | Details                                                                                       | Recommended fix                                                                                 |
| ---------------------------- | ---------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| ContainerUnavailableError    | CONTAINER\_UNAVAILABLE | reason, retryable: true, retryAfterMs?       | Container not ready before the operation started.                                             | Back off (honor retryAfterMs when set), then try the same kind of work again.                   |
| OperationInterruptedError    | OPERATION\_INTERRUPTED | reason, operation, admitted, retryable       | Container or sandbox changed after the operation may have started.                            | Read reason and retryable. Check sandbox or app state before repeating work that changes state. |
| RPCTransportError            | RPC\_TRANSPORT\_ERROR  | kind, originalMessage, errorName, closeCode? | SDK lost contact with the container during a call.                                            | A later call may work. This call may already have changed something.                            |
| RuntimeIdentityInactiveError | —                      | —                                            | Current container is no longer active for this call or handle. Plain Error, not SandboxError. | Check whether the resource still exists; if not, start the work again from stored state.        |

### `ContainerUnavailableError` reasons

`context.reason`:

| Reason               | Meaning                                              |
| -------------------- | ---------------------------------------------------- |
| container\_starting  | Container is still starting                          |
| container\_unhealthy | Container is not healthy                             |
| container\_replaced  | Container was replaced                               |
| rpc\_upgrade\_failed | Could not establish communication with the container |

### `OperationInterruptedError` reasons

`reason` / `context.reason`:

| Reason                     | Meaning                                    |
| -------------------------- | ------------------------------------------ |
| runtime\_replaced          | Underlying container instance was replaced |
| container\_stopped         | Container stopped                          |
| transport\_disposed        | Communication session was disposed         |
| sandbox\_destroyed         | Sandbox was destroyed                      |
| sandbox\_lifetime\_changed | Sandbox lifetime configuration changed     |
| recovery\_exhausted        | Recovery attempts were exhausted           |
| unknown                    | Unclassified interruption                  |

Convenience getters on the error: `reason`, `retryable`, and `operationName`. Other fields such as `admitted`, `operationId`, `phase`, and backup-related metadata are on `error.context` only (`admitted` is `true | "unknown"`).

### `RPCTransportError` kinds

`kind` / `context.kind`:

| Kind               | Meaning                        |
| ------------------ | ------------------------------ |
| peer\_closed       | Peer closed the connection     |
| connection\_failed | Connection failed              |
| upgrade\_failed    | Connection setup failed        |
| invalid\_frame     | Unexpected frame               |
| protocol\_error    | Frame rejected by the protocol |
| session\_disposed  | Session disposed               |
| unknown            | Unclassified failure           |

---

## Worker and container image mismatch

These failures usually mean the Worker package and container image do not match, the image cannot start, or setup metadata does not match what the SDK expects. Fix the deployment. Do not treat them like a slow container start.

Deploy the Worker package and the sandbox container image from the same `@cloudflare/sandbox@next` line. A preview Worker with a stable image (or the reverse) often fails here.

| Class                       | Code            | Key context | Details                                                                                                                                                                                                                                                              | Recommended fix                                                                                        |
| --------------------------- | --------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| RuntimeControlProtocolError | INTERNAL\_ERROR | reason      | Worker and container could not complete setup together (metadata or protocol mismatch). The code is the shared INTERNAL\_ERROR value — identify this class with instanceof RuntimeControlProtocolError or by pairing code === "INTERNAL\_ERROR" with context.reason. | Deploy the Worker package and container image from the same release line. Fix configuration if needed. |

### `RuntimeControlProtocolError` reasons

`context.reason`:

| Reason                       | Meaning                                             | Notes                                                                                     |
| ---------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| unsupported-protocol-version | Worker and container protocol versions do not match | Worker package and container image are not from the same release                          |
| missing-metadata             | Required setup metadata missing from the container  | Bad or incomplete image/build                                                             |
| malformed-metadata           | Setup metadata could not be parsed                  | Bad or incomplete image/build                                                             |
| activation-mismatch          | Activation did not match the expected container     | Can appear after container replace; if it keeps happening, check Worker and image pairing |

The following permanent problems are related and return the same response:

| Problem                                                 | Recommended fix                                                                                        |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Wrong or missing container image in wrangler / registry | Deploy the Worker package and container image from the same release line. Fix configuration if needed. |
| Container exits before it becomes ready                 | Fix the image or entrypoint and redeploy. Do not only retry the app call.                              |
| Account or location capacity limits                     | Refer to [Production capacity limits](#production-capacity-limits)                                     |

---

## Process

| Class                          | Code                           | Key context                             | Details                                             | Recommended fix                                                                                    |
| ------------------------------ | ------------------------------ | --------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| ProcessNotFoundError           | PROCESS\_NOT\_FOUND            | processId                               | Unknown process ID in the current container.        | Use the correct ID, or start the process again from stored state.                                  |
| StaleProcessHandleError        | STALE\_PROCESS\_HANDLE         | processId, pid, operation               | Handle or ID from a previous container.             | Start the work again from stored state. Do not reuse the old handle.                               |
| ProcessSpawnFailedError        | PROCESS\_SPAWN\_FAILED         | processId, command, cwd?, stderr?       | Process could not start.                            | Correct the path, environment, command, or other arguments. Do not retry the same invalid request. |
| InvalidProcessCwdError         | INVALID\_PROCESS\_CWD          | cwd, reason                             | Invalid working directory.                          | Correct the path, environment, command, or other arguments. Do not retry the same invalid request. |
| InvalidProcessEnvironmentError | INVALID\_PROCESS\_ENVIRONMENT  | name?, reason                           | Invalid environment overlay.                        | Correct the path, environment, command, or other arguments. Do not retry the same invalid request. |
| InvalidProcessCursorError      | INVALID\_PROCESS\_CURSOR       | processId, cursor?, reason              | Bad log cursor.                                     | Correct the cursor or other arguments. Do not retry the same invalid value.                        |
| ProcessWaitTimeoutError        | PROCESS\_WAIT\_TIMEOUT         | processId, operation, timeout           | Local output, waitForExit, or waitForLog timed out. | The wait ended. The process or terminal may still be running.                                      |
| ProcessAbortedError            | PROCESS\_ABORTED               | processId, operation                    | Local AbortSignal ended a wait or stream.           | The wait ended. The process or terminal may still be running.                                      |
| ProcessReadyTimeoutError       | PROCESS\_READY\_TIMEOUT        | processId, command, condition, timeout  | Readiness wait timed out.                           | Check whether the process is still running before starting another.                                |
| ProcessExitedBeforeReadyError  | PROCESS\_EXITED\_BEFORE\_READY | processId, command, condition, exitCode | Process exited before readiness.                    | Correct the command or environment, then start again if needed.                                    |
| ProcessExitedBeforeLogError    | PROCESS\_EXITED\_BEFORE\_LOG   | processId, pid, exit                    | Process exited before a log match.                  | Correct the command or environment, then start again if needed.                                    |
| ProcessError                   | PROCESS\_ERROR                 | processId, pid?, exitCode?, stderr?     | General process failure.                            | Check sandbox or app state before repeating work that changes state.                               |

`getProcess` and `listProcesses` returning `null` or `[]` is not an error.

---

## Terminal

| Class                      | Code                      | Key context                    | Details                                                  | Recommended fix                                                                                    |
| -------------------------- | ------------------------- | ------------------------------ | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| TerminalNotFoundError      | TERMINAL\_NOT\_FOUND      | terminalId                     | Unknown terminal ID in the current container.            | Use the correct ID, or start the terminal again from stored state.                                 |
| StaleTerminalHandleError   | STALE\_TERMINAL\_HANDLE   | terminalId, operation          | Handle or ID from a previous container.                  | Start the work again from stored state. Do not reuse the old handle.                               |
| InvalidTerminalCwdError    | INVALID\_TERMINAL\_CWD    | terminalId, cwd, reason        | Invalid working directory at create.                     | Correct the path, environment, command, or other arguments. Do not retry the same invalid request. |
| InvalidTerminalCursorError | INVALID\_TERMINAL\_CURSOR | terminalId, cursor?, reason    | Bad output cursor.                                       | Correct the cursor or other arguments. Do not retry the same invalid value.                        |
| TerminalControlError       | TERMINAL\_CONTROL\_ERROR  | terminalId, operation, reason? | Interrupt, terminate, resize, or related control failed. | Check sandbox or app state before repeating work that changes state.                               |

`getTerminal` and `listTerminals` returning `null` or `[]` is not an error.

---

## Backup

| Class                    | Code                    | Details                 | Recommended fix                                                                                    |
| ------------------------ | ----------------------- | ----------------------- | -------------------------------------------------------------------------------------------------- |
| BackupCreateError        | BACKUP\_CREATE\_FAILED  | Backup create failed.   | Check failure details; correct options if they are invalid.                                        |
| BackupRestoreError       | BACKUP\_RESTORE\_FAILED | Backup restore failed.  | Check failure details; correct options if they are invalid.                                        |
| BackupNotFoundError      | BACKUP\_NOT\_FOUND      | Unknown backup ID.      | Correct the path, environment, command, or other arguments. Do not retry the same invalid request. |
| BackupExpiredError       | BACKUP\_EXPIRED         | Backup past validity.   | Correct options, or create a new backup.                                                           |
| InvalidBackupConfigError | INVALID\_BACKUP\_CONFIG | Invalid backup options. | Correct the path, environment, command, or other arguments. Do not retry the same invalid request. |

---

## Other domains

These classes are available from `@cloudflare/sandbox/errors` (and some mount helpers from the package root). Confirm details against your installed package. Preview-specific guides for every domain are not all published yet.

| Domain                  | Examples                                                                                                                             | Recommended fix                                                                                    |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| Filesystem              | FileNotFoundError, FileExistsError, PermissionDeniedError, FileTooLargeError, FileSystemError                                        | Correct the path or handle a missing file.                                                         |
| Ports / preview         | PortAlreadyExposedError, PortNotExposedError, InvalidPortError, PortInUseError, ServiceNotRespondingError, CustomDomainRequiredError | Correct port options or expose settings.                                                           |
| Interpreter (extension) | InterpreterNotReadyError, ContextNotFoundError, CodeExecutionError                                                                   | If the interpreter is not ready, back off and try again. Otherwise correct the request.            |
| Mounts                  | BucketMountError, BucketUnmountError, S3FSMountError, MissingCredentialsError, InvalidMountConfigError                               | Correct mount options or credentials.                                                              |
| Validation              | ValidationFailedError                                                                                                                | Correct the path, environment, command, or other arguments. Do not retry the same invalid request. |

Other domain classes may exist on `@cloudflare/sandbox/errors` in your installed package. Confirm against that package before depending on undocumented surfaces.

Mount-related errors are also exported from `@cloudflare/sandbox` next to the mount APIs.

---

## Platform helpers

| Helper                                | Details                                                                                                                                              | Recommended fix                                                                         |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| isPlatformTransientError(error)       | True for some transient platform signals (for example connection lost, certain Durable Object storage startup resets, or retryable platform errors). | Prefer a new request or operation.                                                      |
| isDurableObjectCodeUpdateReset(error) | True when the Durable Object isolate was replaced by a code update or deploy.                                                                        | Do not keep retrying inside the same request. Let a new request run on the new isolate. |

These helpers complement `SandboxError` subclasses. They do not replace the recovery rules on [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

---

## Production capacity limits

In production, the Containers platform may reject work when account or deployment limits are exceeded (for example `SURPASSED_BASE_LIMITS`, `SURPASSED_TOTAL_LIMITS`, `LOCATION_SURPASSED_BASE_LIMITS`). Retrying the same overload does not fix that. Reduce concurrency, raise limits, or fail to an operator path. These limits usually do not appear in local `wrangler dev`.

Refer to [Platform limits](https://developers.cloudflare.com/sandbox/platform/limits/).

---

## Related

* [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/)
* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/)
* [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/)
* [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/)
* [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/#page","headline":"Errors · Cloudflare Sandbox SDK docs","description":"Error classes, codes, and context fields for @cloudflare/sandbox@next.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Reference for the code interpreter extension on @cloudflare/sandbox@next.
title: Interpreter
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Interpreter

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/api/interpreter/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page documents `@cloudflare/sandbox/interpreter` on `@cloudflare/sandbox@next`. For the current stable package, refer to [Interpreter API](https://developers.cloudflare.com/sandbox/api/interpreter/).

Methods live on `sandbox.interpreter` after you attach `withInterpreter` on your `Sandbox` subclass. Method names match the stable interpreter; `runCode` returns plain serializable data. Attach and first run: [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/).

## `createCodeContext()`

```ts
createCodeContext(options?: CreateContextOptions): Promise<CodeContext>
```

### `CreateContextOptions`

`createCodeContext` accepts the following options:

| Field    | Type                     | Description                             |                                        |
| -------- | ------------------------ | --------------------------------------- | -------------------------------------- |
| language | "python" \| "javascript" | "typescript"                            | Interpreter language. Default: python. |
| cwd      | string                   | Working directory. Default: /workspace. |                                        |

### `CodeContext`

A created context has the following fields:

| Field     | Type   | Description                         |
| --------- | ------ | ----------------------------------- |
| id        | string | Context id in the current container |
| language  | string | Language of the context             |
| cwd       | string | Working directory                   |
| createdAt | Date   | Created time                        |
| lastUsed  | Date   | Last used time                      |

## `runCode()`

```ts
runCode(code: string, options?: RunCodeOptions): Promise<ExecutionResult>
```

### `RunCodeOptions`

`runCode` accepts the following options. The callback fields apply to `runCode` only.

| Field    | Type                                             | Description                                                         |                                                                     |
| -------- | ------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------- |
| context  | CodeContext                                      | Context to use. If omitted, a default context for language is used. |                                                                     |
| language | "python" \| "javascript"                         | "typescript"                                                        | Used when creating or selecting a default context. Default: python. |
| onStdout | (output: OutputMessage) => void \| Promise<void> | Called for stdout chunks while running                              |                                                                     |
| onStderr | (output: OutputMessage) => void \| Promise<void> | Called for stderr chunks while running                              |                                                                     |
| onResult | (result: ResultData) => void \| Promise<void>    | Called for rich results (plain data)                                |                                                                     |
| onError  | (error: ExecutionError) => void \| Promise<void> | Called when the run reports an execution error                      |                                                                     |

### `OutputMessage`

```ts
interface OutputMessage {
	text: string;
	timestamp: number;
}
```

### `ExecutionResult`

```ts
interface ExecutionResult {
	code: string;
	logs: {
		stdout: string[];
		stderr: string[];
	};
	error?: ExecutionError;
	executionCount?: number;
	results: ResultData[];
}
```

`ResultData` may include plain fields such as `text`, `html`, `png`, `jpeg`, `svg`, `latex`, `markdown`, `json`, and `chart` when the runtime produces them.

### `ExecutionError`

```ts
interface ExecutionError {
	name: string;
	message: string;
	traceback: string[];
	lineNumber?: number;
}
```

## `runCodeStream()`

```ts
runCodeStream(
	code: string,
	options?: RunCodeOptions,
): Promise<ReadableStream<Uint8Array>>
```

Returns an SSE byte stream of execution events. The TypeScript type reuses `RunCodeOptions` for `context` and `language`, but the stream path does **not** invoke `onStdout`, `onStderr`, `onResult`, or `onError` — consume the SSE body instead. Canceling the stream may interrupt the in-flight run.

## `listCodeContexts()`

```ts
listCodeContexts(): Promise<CodeContext[]>
```

## `deleteCodeContext()`

```ts
deleteCodeContext(contextId: string): Promise<void>
```

## Errors

Interpreter failures may surface as `InterpreterNotReadyError`, `ContextNotFoundError`, or `CodeExecutionError`. Refer to [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/) and [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

Python requires the **`-python`** container image variant. Deploy the Worker package and container image from the same preview line.

## Related

* [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/)
* [Extensions](https://developers.cloudflare.com/sandbox/1-0-preview/extensions/)
* [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/)
* Stable: [Interpreter API](https://developers.cloudflare.com/sandbox/api/interpreter/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/api/interpreter/#page","headline":"Interpreter · Cloudflare Sandbox SDK docs","description":"Reference for the code interpreter extension on @cloudflare/sandbox@next.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/api/interpreter/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Reference for argv exec, SandboxProcess handles, logs, waits, and related types in the Sandbox SDK 1.0 preview.
title: Processes
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Processes

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page documents the process API on `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0\. For today's stable command surface, refer to [Commands](https://developers.cloudflare.com/sandbox/api/commands/).

Launch and observe supervised processes in the current container for a sandbox.

For the mental model, refer to [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/). For interactive PTY input and browser terminals, refer to [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/) and the [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/).

Process handles have **no standard input**. Use `cwd`, `env`, and argv (or an explicit shell script) for non-interactive work. Use a [terminal](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/) when you need an interactive PTY.

## `exec()`

Start a process from **argv** (executable, then arguments). Resolves when launch succeeds, not when the process exits. The SDK does not run a shell and does not shell-escape argv — each entry is one process argument.

```ts
exec(command: SandboxCommand, options?: ExecOptions): Promise<SandboxProcess>
```

### `SandboxCommand`

```ts
type SandboxCommand = readonly [executable: string, ...args: string[]];
```

* `command[0]` must be a non-empty executable path or name.
* Later arguments may be empty strings.
* Entries are passed through as-is (no shell escaping of argv).
* Shell syntax requires an explicit shell, for example `['/bin/bash', '-lc', script]`.

### `ExecOptions`

| Field   | Type                   | Description                                                                                                         |
| ------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------- |
| cwd     | string                 | Working directory for this launch. Defaults to /workspace when unset.                                               |
| env     | Record<string, string> | Environment overlay for this launch. Does not mutate later launches. Sandbox-level env still applies.               |
| timeout | number                 | Remote process lifetime in milliseconds. The supervisor may stop the process; completion can report timedOut: true. |

### Returns

`Promise<SandboxProcess>`

```js
const process = await sandbox.exec(["node", "--version"]);
const output = await process.output({ encoding: "utf8" });

console.log(process.id, process.pid, output.stdout, output.exitCode);
```

```ts
const process = await sandbox.exec(["node", "--version"]);
const output = await process.output({ encoding: "utf8" });

console.log(process.id, process.pid, output.stdout, output.exitCode);
```

## `getProcess()`

Return a handle for a process running in the **current container** for this sandbox, or `null`.

Does not start a container if none is running. Returns `null` when no container is up, when the process ID is unknown in the current container, or when that process belonged to a previous container for the same sandbox ID.

```ts
getProcess(id: string): Promise<SandboxProcess | null>
```

Process IDs are not durable across container stop or replace. Refer to [How long a process lives](https://developers.cloudflare.com/sandbox/1-0-preview/processes/#how-long-a-process-lives).

## `listProcesses()`

List processes in the current container for this sandbox. Does not start a container if none is running. Returns an empty list when no container is up.

```ts
listProcesses(): Promise<ProcessStatus[]>
```

Each entry is a [ProcessStatus](#processstatus) value (the same shape as `status()`).

## `SandboxProcess`

| Member                        | Description                                                              |
| ----------------------------- | ------------------------------------------------------------------------ |
| id                            | Process ID in the current container.                                     |
| pid                           | Container pid at launch.                                                 |
| exitCode                      | Promise<number> that resolves when the supervised process group settles. |
| status()                      | Current discriminated status.                                            |
| logs(options?)                | Cursor-based log stream.                                                 |
| output(options?)              | Buffered stdout and stderr plus exit metadata.                           |
| waitForExit(options?)         | Wait until the supervised process group settles.                         |
| waitForLog(pattern, options?) | Wait until stdout or stderr matches.                                     |
| waitForPort(port, options?)   | Wait until a port is ready or readiness fails.                           |
| kill(signal?)                 | Send a numeric signal. Default 15 (SIGTERM).                             |

There is no process stdin API on this handle.

### `status()`

```ts
status(): Promise<ProcessStatus>
```

Refer to [ProcessStatus](#processstatus). A process stays `running` until the supervised **process group** has settled, even if the root pid exits while descendants continue.

### `output()`

Buffer stdout and stderr until the process completes (or the local wait ends), then return exit metadata.

```ts
output(options?: ProcessOutputOptions): Promise<ProcessOutput<Uint8Array>>
output(
	options: ProcessOutputOptions & { encoding: "utf8" },
): Promise<ProcessOutput<string>>
```

#### `ProcessOutput`

```ts
interface ProcessOutput<T = Uint8Array> {
	stdout: T;
	stderr: T;
	exitCode: number;
	signal?: number;
	timedOut: boolean;
	truncated: boolean;
}
```

Default body encoding is binary (`Uint8Array`) unless you pass `encoding: "utf8"`. Prefer `logs()` when output may exceed what you want to buffer.

#### `ProcessOutputOptions`

| Field    | Type        | Description                                                                                             |
| -------- | ----------- | ------------------------------------------------------------------------------------------------------- |
| encoding | "utf8"      | Decode stdout/stderr as strings.                                                                        |
| maxBytes | number      | Cap buffered bytes per stream side of the result; may set truncated: true. No default cap when omitted. |
| timeout  | number      | Local wait deadline in milliseconds only; does not kill the process.                                    |
| signal   | AbortSignal | Cancel this wait only; does not kill the process.                                                       |

`maxBytes` must be a non-negative finite number when set.

```js
const process = await sandbox.exec(["/bin/bash", "-lc", "npm test"], {
	cwd: "/workspace/app",
});
const result = await process.output({ encoding: "utf8", timeout: 120_000 });

console.log(result.exitCode, result.stdout, result.timedOut, result.truncated);
```

```ts
const process = await sandbox.exec(["/bin/bash", "-lc", "npm test"], {
	cwd: "/workspace/app",
});
const result = await process.output({ encoding: "utf8", timeout: 120_000 });

console.log(result.exitCode, result.stdout, result.timedOut, result.truncated);
```

### `logs()`

Stream replayable log events with an opaque cursor.

```ts
logs(options?: ProcessLogsOptions): Promise<ReadableStream<ProcessLogEvent>>
```

#### `ProcessLogsOptions`

| Field  | Type        | Description                                               |
| ------ | ----------- | --------------------------------------------------------- |
| since  | string      | Opaque cursor; resume after a previous event.             |
| replay | boolean     | Include buffered history when resuming.                   |
| follow | boolean     | Keep the stream open for live output.                     |
| signal | AbortSignal | Cancel this subscription only; the process keeps running. |

#### `ProcessLogEvent`

```ts
type ProcessLogEvent =
	| {
			type: "stdout" | "stderr";
			cursor: string;
			timestamp: string;
			data: Uint8Array;
	  }
	| {
			type: "terminal";
			state: "exited";
			cursor: string;
			timestamp: string;
			exit: ProcessExit;
	  }
	| {
			type: "terminal";
			state: "error";
			cursor: string;
			timestamp: string;
			error: ProcessFailure;
	  }
	| {
			type: "truncated";
			cursor?: string;
			timestamp: string;
	  };
```

Retain the latest `cursor` from delivered events if a later Worker request resumes with `logs({ since: cursor, replay: true, follow: true })` on the **same** process in the **same** container.

```js
const process = await sandbox.exec(["/bin/bash", "-lc", "npm run dev"], {
	cwd: "/workspace/app",
});

const stream = await process.logs({ follow: true, replay: true });
const reader = stream.getReader();
const decoder = new TextDecoder();

for (;;) {
	const { done, value } = await reader.read();
	if (done) break;

	if (value.type === "stdout" || value.type === "stderr") {
		// Keep value.cursor if you will resume later
		console.log(value.type, decoder.decode(value.data, { stream: true }));
		continue;
	}

	if (value.type === "terminal") {
		console.log("done", value.state);
		break;
	}
}
```

```ts
const process = await sandbox.exec(["/bin/bash", "-lc", "npm run dev"], {
	cwd: "/workspace/app",
});

const stream = await process.logs({ follow: true, replay: true });
const reader = stream.getReader();
const decoder = new TextDecoder();

for (;;) {
	const { done, value } = await reader.read();
	if (done) break;

	if (value.type === "stdout" || value.type === "stderr") {
		// Keep value.cursor if you will resume later
		console.log(value.type, decoder.decode(value.data, { stream: true }));
		continue;
	}

	if (value.type === "terminal") {
		console.log("done", value.state);
		break;
	}
}
```

### `waitForExit()`

Wait until the supervised process group settles.

```ts
waitForExit(options?: {
	timeout?: number;
	signal?: AbortSignal;
}): Promise<ProcessExit>
```

| Field   | Type        | Description                                          |
| ------- | ----------- | ---------------------------------------------------- |
| timeout | number      | Local wait deadline only; does not kill the process. |
| signal  | AbortSignal | Cancel this wait only; does not kill the process.    |

Returns [ProcessExit](#processexit). Local timeout surfaces as `ProcessWaitTimeoutError`. Local abort surfaces as `ProcessAbortedError`.

```js
const build = await sandbox.exec(["/bin/bash", "-lc", "npm run build"], {
	cwd: "/workspace/app",
});
const exit = await build.waitForExit({ timeout: 600_000 });
console.log(exit.code, exit.signal, exit.timedOut);
```

```ts
const build = await sandbox.exec(["/bin/bash", "-lc", "npm run build"], {
	cwd: "/workspace/app",
});
const exit = await build.waitForExit({ timeout: 600_000 });
console.log(exit.code, exit.signal, exit.timedOut);
```

### `waitForLog()`

Wait until stdout and/or stderr matches a pattern.

```ts
waitForLog(
	pattern: string | RegExp,
	options?: WaitForLogOptions,
): Promise<WaitForLogResult>
```

#### `WaitForLogOptions`

| Field   | Type                 | Description                                          |                                          |
| ------- | -------------------- | ---------------------------------------------------- | ---------------------------------------- |
| stream  | "stdout" \| "stderr" | "both"                                               | Which streams to match. Default: "both". |
| timeout | number               | Local wait deadline only; does not kill the process. |                                          |
| signal  | AbortSignal          | Cancel this wait only; does not kill the process.    |                                          |

#### `WaitForLogResult`

```ts
interface WaitForLogResult {
	stream: "stdout" | "stderr";
	text: string;
	match: string;
	cursor?: string;
}
```

* `text` is the matching window of decoded output for that stream.
* `match` is the matched substring.
* `cursor` is the log cursor at the match when available.

If the process exits before a match, the SDK throws `ProcessExitedBeforeLogError`. A local wait timeout throws `ProcessWaitTimeoutError`.

```js
const server = await sandbox.exec(["/bin/bash", "-lc", "npm run dev"], {
	cwd: "/workspace/app",
});

const ready = await server.waitForLog(/listening on/i, {
	stream: "both",
	timeout: 60_000,
});
console.log(ready.stream, ready.match);
```

```ts
const server = await sandbox.exec(["/bin/bash", "-lc", "npm run dev"], {
	cwd: "/workspace/app",
});

const ready = await server.waitForLog(/listening on/i, {
	stream: "both",
	timeout: 60_000,
});
console.log(ready.stream, ready.match);
```

### `waitForPort()`

Wait until a port is ready, or fail if the process exits first or the local wait ends.

```ts
waitForPort(port: number, options?: WaitForPortOptions): Promise<void>
```

#### `WaitForPortOptions`

| Field    | Type                                   | Description                                                                                   |
| -------- | -------------------------------------- | --------------------------------------------------------------------------------------------- |
| mode     | "tcp" \| "http"                        | Readiness check. Default: "tcp" (accepts a TCP connection).                                   |
| path     | string                                 | HTTP path to request when mode is "http". Default: "/".                                       |
| status   | number \| { min: number; max: number } | Expected HTTP status or inclusive range when mode is "http". Default: { min: 200, max: 399 }. |
| interval | number                                 | Milliseconds between checks. Default: 500.                                                    |
| timeout  | number                                 | Local wait deadline only; does not kill the process. No default timeout when omitted.         |
| signal   | AbortSignal                            | Cancel this wait only; does not kill the process.                                             |

**TCP mode** (default) succeeds when the port accepts a connection:

```js
const db = await sandbox.exec(["redis-server"]);

await db.waitForPort(6379, {
	mode: "tcp",
	timeout: 10_000,
});
```

```ts
const db = await sandbox.exec(["redis-server"]);

await db.waitForPort(6379, {
	mode: "tcp",
	timeout: 10_000,
});
```

**HTTP mode** issues an HTTP request and checks the response status:

```js
const server = await sandbox.exec(["/bin/bash", "-lc", "npm run dev"], {
	cwd: "/workspace/app",
});

await server.waitForPort(3000, {
	mode: "http",
	path: "/health",
	status: { min: 200, max: 299 },
	timeout: 60_000,
});
```

```ts
const server = await sandbox.exec(["/bin/bash", "-lc", "npm run dev"], {
	cwd: "/workspace/app",
});

await server.waitForPort(3000, {
	mode: "http",
	path: "/health",
	status: { min: 200, max: 299 },
	timeout: 60_000,
});
```

Typical failures:

* `ProcessReadyTimeoutError` — port not ready before the local timeout
* `ProcessExitedBeforeReadyError` — process exited before the port was ready
* `ProcessAbortedError` — local `AbortSignal` cancelled the wait (process may still run)

### `kill()`

Send a numeric signal to the process.

```ts
kill(signal?: number): Promise<void>
```

Default `signal` is `15` (`SIGTERM`). Pass a numeric signal only (for example `9` for `SIGKILL`). String signal names are not accepted.

Stopping the process is separate from cancelling a local wait or log subscription.

### `exitCode`

```ts
readonly exitCode: Promise<number>
```

Resolves to the exit code when the supervised process group has settled (the same completion boundary as `waitForExit()`). Prefer `waitForExit()` when you also need `signal` or `timedOut`.

## ProcessStatus

```ts
type ProcessStatus =
	| {
			state: "running";
			id: string;
			pid: number;
			command: SandboxCommand;
			cwd?: string;
			startedAt: string;
	  }
	| {
			state: "exited";
			id: string;
			pid: number;
			command: SandboxCommand;
			cwd?: string;
			startedAt: string;
			endedAt: string;
			exit: ProcessExit;
	  }
	| {
			state: "error";
			id: string;
			pid: number;
			command: SandboxCommand;
			cwd?: string;
			startedAt: string;
			endedAt: string;
			error: ProcessFailure;
	  };
```

`listProcesses()` returns `ProcessStatus[]` using this shape.

### `ProcessExit`

Outcome observed for the **root** subprocess when the supervised group settles.

```ts
interface ProcessExit {
	code: number;
	signal?: number;
	timedOut: boolean;
}
```

Signals delivered only to descendants do not rewrite this outcome. Refer to [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/).

### `ProcessFailure`

```ts
interface ProcessFailure {
	code: string;
	message: string;
}
```

## Common errors

`getProcess` and `listProcesses` do not throw for missing work. They return `null` or `[]` when no container is up, the ID is unknown in the current container, or the process belonged to a previous container. The following error classes apply to operations on a process handle (and to launch), not to those lookups.

| Situation                                                               | Class / outcome                                        |
| ----------------------------------------------------------------------- | ------------------------------------------------------ |
| getProcess / listProcesses while no container is running                | null / \[\] (not an error; does not start a container) |
| getProcess for an unknown ID or a process from a previous container     | null                                                   |
| Operation on a handle after the container was replaced                  | StaleProcessHandleError                                |
| Operation on a handle when the process is gone in the current container | ProcessNotFoundError                                   |
| Local wait timed out (output / waitForExit / waitForLog)                | ProcessWaitTimeoutError                                |
| Local AbortSignal on a wait or stream                                   | ProcessAbortedError                                    |
| Port not ready before local timeout                                     | ProcessReadyTimeoutError                               |
| Process exited before port readiness                                    | ProcessExitedBeforeReadyError                          |
| Process exited before a log match                                       | ProcessExitedBeforeLogError                            |
| Invalid working directory at launch                                     | InvalidProcessCwdError                                 |
| Invalid environment at launch                                           | InvalidProcessEnvironmentError                         |
| Invalid log cursor                                                      | InvalidProcessCursorError                              |
| Process failed to start                                                 | ProcessSpawnFailedError                                |
| Container not ready; work did not start                                 | ContainerUnavailableError                              |
| Work interrupted after it may have started                              | OperationInterruptedError                              |

Recovery guidance: [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/). Full catalog: [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/). Lifetime: [How long a process lives](https://developers.cloudflare.com/sandbox/1-0-preview/processes/#how-long-a-process-lives).

## Related

* [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/)
* [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/)
* [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/)
* [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/)
* [API reference](https://developers.cloudflare.com/sandbox/1-0-preview/api/)
* [Migrate from the stable SDK](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)
* Stable: [Commands](https://developers.cloudflare.com/sandbox/api/commands/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/#page","headline":"Processes · Cloudflare Sandbox SDK docs","description":"Reference for argv exec, SandboxProcess handles, logs, waits, and related types in the Sandbox SDK 1.0 preview.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Reference for createTerminal, Terminal handles, output streams, connect, and control methods in the Sandbox SDK 1.0 preview.
title: Terminals
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Terminals

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page documents the terminal API on `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0\. For today's stable `sandbox.terminal()` helper, refer to [Terminal](https://developers.cloudflare.com/sandbox/api/terminal/).

Create and control interactive PTY terminals in the current container for a sandbox.

For the mental model and browser connect walkthrough, refer to [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/).

## `createTerminal()`

Start a terminal from **argv** (usually a shell). Resolves when the terminal resource is created. Same rules as process `exec`: no implicit shell wrapping, and argv entries are not shell-escaped.

```ts
createTerminal(options: CreateTerminalOptions): Promise<Terminal>
```

### `CreateTerminalOptions`

| Field      | Type                   | Description                                                                  |
| ---------- | ---------------------- | ---------------------------------------------------------------------------- |
| command    | SandboxCommand         | Argv to run under the PTY. Required. Example: \['bash'\] or \['/bin/bash'\]. |
| cwd        | string                 | Working directory for the terminal process.                                  |
| env        | Record<string, string> | Environment overlay for this terminal. Does not mutate later launches.       |
| cols       | number                 | Initial width in columns.                                                    |
| rows       | number                 | Initial height in rows.                                                      |
| bufferSize | number                 | Output buffer sizing for replay (when supported by the runtime).             |

`SandboxCommand` is the same argv type as process `exec`: `readonly [executable: string, ...args: string[]]`.

### Returns

`Promise<Terminal>` — a handle for the terminal in the **current container**.

```js
const terminal = await sandbox.createTerminal({
	command: ["bash"],
	cwd: "/workspace",
	env: { TERM: "xterm-256color" },
	cols: 120,
	rows: 40,
});

console.log(terminal.id);
```

```ts
const terminal = await sandbox.createTerminal({
	command: ["bash"],
	cwd: "/workspace",
	env: { TERM: "xterm-256color" },
	cols: 120,
	rows: 40,
});

console.log(terminal.id);
```

## `getTerminal()`

Return a handle for a terminal in the **current container**, or `null`.

Does not start a container if none is running. Returns `null` when no container is up, when the terminal ID is unknown in the current container, or when that terminal belonged to a previous container for the same sandbox ID.

```ts
getTerminal(id: string): Promise<Terminal | null>
```

## `listTerminals()`

List terminals in the current container for this sandbox. Does not start a container if none is running. Returns an empty list when no container is up.

```ts
listTerminals(): Promise<Terminal[]>
```

## `Terminal`

| Member                  | Description                                                               |
| ----------------------- | ------------------------------------------------------------------------- |
| id                      | Terminal ID in the current container.                                     |
| getSnapshot()           | Current snapshot (running / exited / error).                              |
| write(data)             | Write bytes to the PTY (stdin).                                           |
| resize(cols, rows)      | Resize the PTY.                                                           |
| output(options?)        | Cursor-based output event stream.                                         |
| waitForExit(options?)   | Wait until the terminal completes.                                        |
| interrupt()             | Send an interrupt to the terminal session (for example Ctrl-C semantics). |
| terminate()             | End the terminal resource.                                                |
| connect(request, opts?) | Accept a browser WebSocket upgrade and attach it to this terminal.        |

### `getSnapshot()`

```ts
interface TerminalSnapshot {
	id: string;
	pid?: number;
	command: SandboxCommand;
	cwd?: string;
	status: "running" | "exited" | "error";
	exit?: ProcessExit;
	error?: ProcessFailure;
}
```

### `write()`

```ts
write(data: Uint8Array): Promise<void>
```

Write bytes to the PTY. Browser keystrokes normally arrive through `connect()` instead.

### `resize()`

```ts
resize(cols: number, rows: number): Promise<void>
```

### `output()`

```ts
output(options?: TerminalOutputOptions): Promise<ReadableStream<TerminalOutputEvent>>
```

#### `TerminalOutputOptions`

| Field  | Type        | Description                                                |
| ------ | ----------- | ---------------------------------------------------------- |
| since  | string      | Opaque cursor; resume after a previous event.              |
| replay | boolean     | Include buffered history when resuming.                    |
| follow | boolean     | Keep the stream open for live output.                      |
| signal | AbortSignal | Cancel this subscription only. The terminal keeps running. |

#### `TerminalOutputEvent`

```ts
type TerminalOutputEvent =
	| {
			type: "data";
			terminalId: string;
			cursor: string;
			timestamp: string;
			data: Uint8Array;
	  }
	| {
			type: "terminal";
			terminalId: string;
			cursor: string;
			timestamp: string;
			state: "exited";
			exit: ProcessExit;
	  }
	| {
			type: "terminal";
			terminalId: string;
			cursor: string;
			timestamp: string;
			state: "error";
			error: ProcessFailure;
	  }
	| {
			type: "truncated";
			terminalId: string;
			cursor?: string;
			timestamp: string;
	  };
```

Retain the latest `cursor` from delivered events if you reconnect or call `output({ since, replay: true })` later on the **same** terminal in the **same** container.

### `waitForExit()`

```ts
waitForExit(options?: {
	timeout?: number;
	signal?: AbortSignal;
}): Promise<ProcessExit>
```

Local `timeout` / `signal` cancel only the wait. They do not terminate the terminal. Call `terminate()` or `interrupt()` when you intend to stop it.

### `interrupt()` and `terminate()`

```ts
interrupt(): Promise<void>
terminate(): Promise<void>
```

These are terminal control operations. They are not the same as process `kill(signal)` on an `exec` handle.

### `connect()`

Attach a browser (or other) WebSocket upgrade request to this terminal.

```ts
connect(
	request: Request,
	options?: {
		cursor?: string;
		cols?: number;
		rows?: number;
	},
): Promise<Response>
```

* `request` must be a WebSocket upgrade request.
* `cursor` resumes output replay after a previous disconnect when the client has one.
* `cols` / `rows` set the PTY size for this attachment when provided.

Returns the WebSocket upgrade `Response` your Worker should return to the client.

```js
const url = new URL(request.url);
const terminalId = url.searchParams.get("terminalId");
if (!terminalId) {
	return new Response("terminalId is required", { status: 400 });
}

const terminal = await sandbox.getTerminal(terminalId);
if (!terminal) {
	return new Response("Terminal not found", { status: 404 });
}

return terminal.connect(request, {
	cursor: url.searchParams.get("cursor") ?? undefined,
	cols: 120,
	rows: 40,
});
```

```ts
const url = new URL(request.url);
const terminalId = url.searchParams.get("terminalId");
if (!terminalId) {
	return new Response("terminalId is required", { status: 400 });
}

const terminal = await sandbox.getTerminal(terminalId);
if (!terminal) {
	return new Response("Terminal not found", { status: 404 });
}

return terminal.connect(request, {
	cursor: url.searchParams.get("cursor") ?? undefined,
	cols: 120,
	rows: 40,
});
```

For the full Worker + xterm.js path, refer to [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/#browser-connect).

## Client helper: `@cloudflare/sandbox/xterm`

`SandboxAddon` integrates [xterm.js ↗](https://xtermjs.org/) with preview terminals.

```js
import { SandboxAddon } from "@cloudflare/sandbox/xterm";

const addon = new SandboxAddon({
	// `origin` is already a WebSocket origin (`wss://` or `ws://`).
	getWebSocketUrl: ({ sandboxId, terminalId, cursor, origin }) => {
		const params = new URLSearchParams({ sandboxId });
		if (terminalId) params.set("terminalId", terminalId);
		if (cursor) params.set("cursor", cursor);
		return `${origin}/ws/terminal?${params}`;
	},
	reconnect: true,
	onStateChange: (state, error) => {
		/* update UI */
	},
});
```

```ts
import { SandboxAddon } from "@cloudflare/sandbox/xterm";

const addon = new SandboxAddon({
	// `origin` is already a WebSocket origin (`wss://` or `ws://`).
	getWebSocketUrl: ({ sandboxId, terminalId, cursor, origin }) => {
		const params = new URLSearchParams({ sandboxId });
		if (terminalId) params.set("terminalId", terminalId);
		if (cursor) params.set("cursor", cursor);
		return `${origin}/ws/terminal?${params}`;
	},
	reconnect: true,
	onStateChange: (state, error) => {
		/* update UI */
	},
});
```

| Item                   | Preview detail                          |
| ---------------------- | --------------------------------------- |
| Connection target      | { sandboxId, terminalId? }              |
| getWebSocketUrl params | sandboxId, terminalId?, cursor?, origin |
| Properties             | state, sandboxId, terminalId            |

`@xterm/xterm` is an optional peer dependency of the preview package.

## Common errors

| Situation                                                 | Class / outcome                                        |
| --------------------------------------------------------- | ------------------------------------------------------ |
| Unknown terminal ID in the current container              | TerminalNotFoundError                                  |
| getTerminal / listTerminals while no container is running | null / \[\] (not an error; does not start a container) |
| Handle or terminal ID from a previous container           | StaleTerminalHandleError                               |
| Invalid working directory at create                       | InvalidTerminalCwdError                                |
| Invalid output cursor                                     | InvalidTerminalCursorError                             |
| Control operation failed                                  | TerminalControlError                                   |

Recovery guidance: [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/). Full catalog: [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/). Lifetime: [How long a process lives](https://developers.cloudflare.com/sandbox/1-0-preview/processes/#how-long-a-process-lives).

## Related

* [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/)
* [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/)
* [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/)
* [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/)
* [API reference](https://developers.cloudflare.com/sandbox/1-0-preview/api/)
* [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)
* Stable: [Terminal](https://developers.cloudflare.com/sandbox/api/terminal/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/#page","headline":"Terminals · Cloudflare Sandbox SDK docs","description":"Reference for createTerminal, Terminal handles, output streams, connect, and control methods in the Sandbox SDK 1.0 preview.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: How processes and terminals get environment variables in the Sandbox SDK 1.0 preview.
title: Environment variables
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Environment variables

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/environment/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page documents environment variables on `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0\. For the current stable package, refer to [Environment variables](https://developers.cloudflare.com/sandbox/configuration/environment-variables/).

Each `exec()` and `createTerminal()` starts an independent process. Shell `export` in one process does not apply to the next launch. Configure process environment with the container image, `setEnvVars`, and per-launch `env`.

Use environment variables for **non-secret** configuration (paths, feature flags, `NODE_ENV`, and similar). Do not put live API keys or other long-lived credentials into the sandbox. To call external services that need credentials, use [outbound traffic handlers](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/) so secrets stay in the Worker.

## How a process gets its environment

When a process starts, the runtime builds its environment from:

1. The **container** environment (image `ENV` and defaults).
2. Names from **`setEnvVars`**, when you use `exec()` (described in the next section).
3. The **`env` option** on that launch, if you pass one.

Later launches do not keep overlays from earlier launches. A command that runs `export FOO=bar` inside one process does not change the next `exec()`.

Worker bindings in your `fetch` handler are not process environment variables. Only values you pass through `setEnvVars` or launch `env` appear inside the process (and those should not be long-lived secrets).

## `setEnvVars()`

```ts
setEnvVars(envVars: Record<string, string | undefined>): Promise<void>
```

| Value     | Effect                                                  |
| --------- | ------------------------------------------------------- |
| string    | Set this environment variable for later exec() launches |
| undefined | Remove a previously stored variable                     |

On each `exec()`, the SDK merges stored names into that process’s environment at launch.

Stored names live in the sandbox Durable Object’s memory. They are not written to the container filesystem and are not part of a backup. After the Durable Object is evicted or replaced, call `setEnvVars` again if you still need those names, or pass `env` on each `exec()`.

```js
const sandbox = getSandbox(env.Sandbox, "user-123");

await sandbox.setEnvVars({
	NODE_ENV: "production",
	APP_HOME: "/workspace/app",
	LOG_LEVEL: "info",
});

const migrate = await sandbox.exec(["python", "migrate.py"], {
	cwd: "/workspace/app",
});
await migrate.output({ encoding: "utf8" });

const seed = await sandbox.exec(["python", "seed.py"], {
	cwd: "/workspace/app",
});
await seed.output({ encoding: "utf8" });

await sandbox.setEnvVars({
	LOG_LEVEL: "debug",
	TEMP_FLAG: undefined,
});
```

```ts
const sandbox = getSandbox(env.Sandbox, "user-123");

await sandbox.setEnvVars({
	NODE_ENV: "production",
	APP_HOME: "/workspace/app",
	LOG_LEVEL: "info",
});

const migrate = await sandbox.exec(["python", "migrate.py"], {
	cwd: "/workspace/app",
});
await migrate.output({ encoding: "utf8" });

const seed = await sandbox.exec(["python", "seed.py"], {
	cwd: "/workspace/app",
});
await seed.output({ encoding: "utf8" });

await sandbox.setEnvVars({
	LOG_LEVEL: "debug",
	TEMP_FLAG: undefined,
});
```

## `env` on `exec()`

```js
const process = await sandbox.exec(["node", "app.js"], {
	cwd: "/workspace/app",
	env: {
		NODE_ENV: "production",
		PORT: "3000",
	},
});
```

```ts
const process = await sandbox.exec(["node", "app.js"], {
	cwd: "/workspace/app",
	env: {
		NODE_ENV: "production",
		PORT: "3000",
	},
});
```

| Behavior     | Detail                                                |
| ------------ | ----------------------------------------------------- |
| Scope        | This launch only                                      |
| Merge order  | Container environment, then setEnvVars, then this env |
| Side effects | Does not update setEnvVars storage                    |

Omit `env` when sandbox-wide names (and the container environment) are enough.

## `env` on `createTerminal()`

```js
const terminal = await sandbox.createTerminal({
	command: ["bash"],
	cwd: "/workspace",
	env: {
		TERM: "xterm-256color",
		APP_HOME: "/workspace/app",
	},
});
```

```ts
const terminal = await sandbox.createTerminal({
	command: ["bash"],
	cwd: "/workspace",
	env: {
		TERM: "xterm-256color",
		APP_HOME: "/workspace/app",
	},
});
```

The terminal’s launch `env` overlays the container environment for that terminal only. Pass the names the terminal needs on `createTerminal`.

Inside an interactive shell, `export` applies for the life of that terminal. It does not apply to later `exec()` calls. Refer to [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/).

## External APIs and credentials

Code inside the sandbox should not hold live provider credentials. Keep secrets in the Worker and intercept outbound HTTP(S) with `outboundByHost` (and related policy such as `enableInternet` / `allowedHosts`). The sandbox can send ordinary requests—or placeholders client libraries require—while the Worker attaches real credentials before the request leaves your account.

Refer to [Handle outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/), including securely injecting credentials. For Workers bindings (KV, R2, and similar) reached by hostname from the sandbox, refer to [Connect to Workers bindings](https://developers.cloudflare.com/sandbox/guides/workers-connections/).

## Related

* [Handle outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/)
* [Connect to Workers bindings](https://developers.cloudflare.com/sandbox/guides/workers-connections/)
* [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/)
* [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/)
* [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/)
* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/)
* [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/environment/#page","headline":"Environment variables · Cloudflare Sandbox SDK docs","description":"How processes and terminals get environment variables in the Sandbox SDK 1.0 preview.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/environment/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Retry and recover from Sandbox SDK 1.0 preview failures when containers start, stop, or interrupt work.
title: Errors and recovery
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Errors and recovery

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/errors/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page documents error handling on `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0\. Class names, codes, and context fields: [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/).

Some failures mean the container never started your work. Others mean the work may already have started. Those cases need different recovery.

The same **sandbox ID** can later use a **new container**. Processes, terminals, and local files from the previous container do not return on their own. Refer to [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/) and [How long a process lives](https://developers.cloudflare.com/sandbox/1-0-preview/processes/#how-long-a-process-lives).

Class catalog: [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/). Symptom table: [Troubleshooting](https://developers.cloudflare.com/sandbox/1-0-preview/troubleshooting/).

## Before the container starts the work

If the container is not ready, the SDK may throw `ContainerUnavailableError` (`CONTAINER_UNAVAILABLE`). The operation did not run inside the container.

That often happens on cold start, after idle stop, or during a deploy.

The error context includes `retryable: true`, a `reason` (for example `container_starting`), and optional `retryAfterMs`. Back off (use `retryAfterMs` when present), then try the same kind of work again.

Do not use that same “always retry” rule for failures that occur after the container may already have started the work.

```js
import { ContainerUnavailableError } from "@cloudflare/sandbox";

try {
	const process = await sandbox.exec(["npm", "install"], {
		cwd: "/workspace/app",
	});
	await process.waitForExit();
} catch (error) {
	if (error instanceof ContainerUnavailableError) {
		// Safe to retry the whole operation after backoff.
	}
}
```

```ts
import { ContainerUnavailableError } from "@cloudflare/sandbox";

try {
	const process = await sandbox.exec(["npm", "install"], {
		cwd: "/workspace/app",
	});
	await process.waitForExit();
} catch (error) {
	if (error instanceof ContainerUnavailableError) {
		// Safe to retry the whole operation after backoff.
	}
}
```

## After the container may have started the work

Once the container has accepted an operation, a failure can leave partial results: a process may be running, a file may exist, a backup may have begun.

### Container replaced or sandbox ended during a call

`OperationInterruptedError` means the container or sandbox changed while the call was already underway. The work may have started.

Use `reason` and `retryable` on the error (refer to [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/) for fields). If the steps change state, check the sandbox or your own records before running the same steps again.

### SDK lost contact during a call

`RPCTransportError` means the SDK lost contact with the current container during a call. A later call can succeed against the container again.

That does **not** mean the interrupted call did nothing. Prefer checkpoints and steps that are safe to run twice, or inspect state, before repeating the same work. Diagnostic `kind` values are listed on the [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/).

### Stale process or terminal handles

Process and terminal IDs belong to the **current** container for a sandbox ID. After stop or replace, calls on an old handle throw `StaleProcessHandleError` or `StaleTerminalHandleError`. `getProcess`, `getTerminal`, `listProcesses`, and `listTerminals` do not start a container. They return `null` or `[]` when no container is running, or when the ID is unknown in the current container. That is not an exception.

Store the job (command, `cwd`, `env`, checkpoint), not only the resource ID. Then start a new `exec` or `createTerminal` when the old handle is gone.

### Local waits and aborts

Timeouts and `AbortSignal` on `output()`, `waitForExit()`, `waitForLog()`, `waitForPort()`, and `logs()` end **that wait or stream only**. They do not kill the process. Canceling terminal output does not terminate the PTY.

Use `process.kill()` or `terminal.interrupt()` / `terminal.terminate()` when you intend to stop the resource. Typical errors: `ProcessWaitTimeoutError`, `ProcessAbortedError`.

### Invalid arguments

Invalid `cwd` or environment variables, a missing executable, or similar request problems fail until you change those values. Do not retry the same invalid request. Typical classes: `InvalidProcessCwdError`, `InvalidProcessEnvironmentError`, `ProcessSpawnFailedError`.

### Worker and container image mismatch

Some failures mean the Worker package and container image do not match, the image cannot start, or setup between Worker and container failed.

| Signal                                                                                                | Response                                                                                                                           |
| ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| RuntimeControlProtocolError (for example unsupported-protocol-version, missing or malformed metadata) | Deploy the Worker package and container image from the same @cloudflare/sandbox@next line. Do not mix preview and stable packages. |
| Wrong or missing image, or the container exits before it is ready                                     | Fix wrangler, the image, or the entrypoint. Retrying the same application call will not help.                                      |
| Account or location capacity limits                                                                   | Lower concurrency or raise limits. Refer to [Platform limits](https://developers.cloudflare.com/sandbox/platform/limits/).         |

Catalog detail: [Worker and container image mismatch](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/#worker-and-container-image-mismatch).

These are not the same as a slow start (`ContainerUnavailableError`). Do not use the same backoff-and-retry loop for both.

## Common recovery paths

### First use or wake after idle

**Error:** `ContainerUnavailableError`

Back off, then run the full unit of work again (for example setup plus `exec`), not an arbitrary middle step without a checkpoint.

### Long job across Worker requests

1. Persist the job and checkpoint (and a process or terminal ID while it is useful).
2. On a later request, call `getProcess` or `getTerminal` if you still have an ID.
3. If you get a handle, continue (logs, connect, wait).
4. If you get `null` or a stale-handle error, start again from the checkpoint.
5. If you get `ContainerUnavailableError`, backoff and continue with a new operation.

### Deploy or replace while a call is in flight

**Error:** `OperationInterruptedError`

Read `reason` and `retryable`. If the call may have changed something, inspect before repeating it.

### Lost contact during a call

**Error:** `RPCTransportError`

Log `kind` if you need diagnostics. Assume in-flight work may have run. Continue from checkpoints or inspection, then start a new operation if the job still needs it.

### You only stopped waiting

**Errors:** `ProcessWaitTimeoutError`, `ProcessAbortedError`

Either keep observing (`getProcess` and `logs({ since })`) or stop the process with `kill`. Do not assume the process exited because the wait ended.

### Invalid arguments

**Errors:** `InvalidProcessCwdError`, `InvalidProcessEnvironmentError`, `ProcessSpawnFailedError`, and similar

Correct the path, environment, or command (or the files in the image if the binary is missing). Do not retry unchanged values.

### Worker and container image mismatch

**Situation:** After a deploy, calls fail with protocol or setup errors, or the container never becomes usable.

**Errors / signals:** `RuntimeControlProtocolError`; wrong image; container exits before it is ready

**Do:** Redeploy the Worker package and container image from the same `@cloudflare/sandbox@next` line. Confirm the image name and entrypoint.

**Do not:** Treat this like a slow container start and only back off.

## Example

```js
import {
	ContainerUnavailableError,
	OperationInterruptedError,
	RPCTransportError,
	StaleProcessHandleError,
	ProcessWaitTimeoutError,
	ProcessAbortedError,
} from "@cloudflare/sandbox";

try {
	const process = await sandbox.exec(["npm", "test"], {
		cwd: "/workspace/app",
	});
	const result = await process.output({ encoding: "utf8" });
	console.log(result.exitCode, result.stdout);
} catch (error) {
	if (error instanceof ContainerUnavailableError) {
		// Container never started the work — back off, then try the work again.
	} else if (error instanceof StaleProcessHandleError) {
		// Previous container — start again from what you stored about the work.
	} else if (error instanceof OperationInterruptedError) {
		// Work may have started — read reason/retryable and check state before repeating.
	} else if (error instanceof RPCTransportError) {
		// Lost contact during the call — a later call may work; this call may already have run.
	} else if (
		error instanceof ProcessWaitTimeoutError ||
		error instanceof ProcessAbortedError
	) {
		// Wait ended only — process may still be running.
	} else {
		throw error;
	}
}
```

```ts
import {
	ContainerUnavailableError,
	OperationInterruptedError,
	RPCTransportError,
	StaleProcessHandleError,
	ProcessWaitTimeoutError,
	ProcessAbortedError,
} from "@cloudflare/sandbox";

try {
	const process = await sandbox.exec(["npm", "test"], {
		cwd: "/workspace/app",
	});
	const result = await process.output({ encoding: "utf8" });
	console.log(result.exitCode, result.stdout);
} catch (error) {
	if (error instanceof ContainerUnavailableError) {
		// Container never started the work — back off, then try the work again.
	} else if (error instanceof StaleProcessHandleError) {
		// Previous container — start again from what you stored about the work.
	} else if (error instanceof OperationInterruptedError) {
		// Work may have started — read reason/retryable and check state before repeating.
	} else if (error instanceof RPCTransportError) {
		// Lost contact during the call — a later call may work; this call may already have run.
	} else if (
		error instanceof ProcessWaitTimeoutError ||
		error instanceof ProcessAbortedError
	) {
		// Wait ended only — process may still be running.
	} else {
		throw error;
	}
}
```

Prefer `instanceof` with classes from `@cloudflare/sandbox`. Full tables: [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/).

## Related

* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/)
* [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/)
* [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/)
* [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/)
* [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)
* [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/) · [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/errors/#page","headline":"Errors and recovery · Cloudflare Sandbox SDK docs","description":"Retry and recover from Sandbox SDK 1.0 preview failures when containers start, stop, or interrupt work.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/errors/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Attach optional Sandbox capabilities on @cloudflare/sandbox@next.
title: Extensions
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Extensions

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/extensions/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page documents extensions on `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0.

Extensions add optional capabilities to your `Sandbox` subclass as nested namespaces (for example `sandbox.interpreter.*`). They are not free-floating globals on every app.

## Attach pattern

```js
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withInterpreter } from "@cloudflare/sandbox/interpreter";

export class Sandbox extends BaseSandbox {
	interpreter = withInterpreter(this);
}
```

```ts
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withInterpreter } from "@cloudflare/sandbox/interpreter";

export class Sandbox extends BaseSandbox<Env> {
	interpreter = withInterpreter(this);
}
```

Export that class from your Worker. Call extension methods through the nested property from application code.

## First-party extensions

The following first-party extensions are available on the preview package:

| Extension        | Package                         | Docs                                                                                                                                                                              |
| ---------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Code interpreter | @cloudflare/sandbox/interpreter | [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/), [Interpreter API](https://developers.cloudflare.com/sandbox/1-0-preview/api/interpreter/) |
| OpenCode         | @cloudflare/sandbox/opencode    | Confirm exports in your installed @next version (for example withOpenCode and client/proxy helpers).                                                                              |

For the interpreter, attach once, then use the same method names as the stable package (`createCodeContext`, `runCode`, and related calls) on `sandbox.interpreter`. Python needs the **`-python`** image variant. For the full how-to, refer to [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/).

## Custom extensions

Application-defined extensions are experimental. Helpers exist under `@cloudflare/sandbox/extensions`, but preview documentation does not yet cover authoring or publishing a custom extension. Prefer the first-party extensions in the table, or keep any custom code inside your application until a supported authoring guide ships.

## Related

* [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/)
* [Interpreter API](https://developers.cloudflare.com/sandbox/1-0-preview/api/interpreter/)
* [API reference](https://developers.cloudflare.com/sandbox/1-0-preview/api/)
* [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/extensions/#page","headline":"Extensions · Cloudflare Sandbox SDK docs","description":"Attach optional Sandbox capabilities on @cloudflare/sandbox@next.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/extensions/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Install @cloudflare/sandbox@next and run your first process handle in a sandbox.
title: Get started
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Get started

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/get-started/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page uses `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0\. Prefer this path for new projects. For today's stable package, refer to [Getting started](https://developers.cloudflare.com/sandbox/get-started/).

## 1\. Install the preview package

In a Workers project that already uses Sandbox, or a new project from the Sandbox template:

npmyarnpnpmbun

```
npm i @cloudflare/sandbox@next
```

```
yarn add @cloudflare/sandbox@next
```

```
pnpm add @cloudflare/sandbox@next
```

```
bun add @cloudflare/sandbox@next
```

Build and deploy the Worker **and** the sandbox container image from the same preview line.

## 2\. Export your Sandbox class

```js
import { Sandbox } from "@cloudflare/sandbox";

export { Sandbox };
```

```ts
import { Sandbox } from "@cloudflare/sandbox";

export { Sandbox };
```

Keep your `wrangler` Durable Object binding and container configuration. Preview-specific transport variables are not required.

## 3\. Run a process

`exec()` starts a program from **argv** — an array of the executable path or name, then its arguments. It waits until the sandbox can start the process, then returns a **process handle**. It does **not** wait for the process to exit.

Collect results with handle methods such as `output()`, or stream with `logs()`.

```js
import { getSandbox, proxyToSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		const proxy = await proxyToSandbox(request, env);
		if (proxy) return proxy;

		const sandbox = getSandbox(env.Sandbox, "preview-demo");
		const process = await sandbox.exec(["python3", "-c", "print(2 + 2)"]);
		const output = await process.output({ encoding: "utf8" });

		return Response.json({
			id: process.id,
			pid: process.pid,
			stdout: output.stdout,
			exitCode: output.exitCode,
		});
	},
};
```

```ts
import { getSandbox, proxyToSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

type Env = {
	Sandbox: DurableObjectNamespace<import("@cloudflare/sandbox").Sandbox>;
};

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const proxy = await proxyToSandbox(request, env);
		if (proxy) return proxy;

		const sandbox = getSandbox(env.Sandbox, "preview-demo");
		const process = await sandbox.exec(["python3", "-c", "print(2 + 2)"]);
		const output = await process.output({ encoding: "utf8" });

		return Response.json({
			id: process.id,
			pid: process.pid,
			stdout: output.stdout,
			exitCode: output.exitCode,
		});
	},
};
```

Each argv entry is one argument to the process. The SDK does **not** run a shell and does **not** shell-escape argv. Spaces and special characters in an entry stay inside that argument.

Shell syntax (`&&`, pipes, redirects, globs) needs an explicit shell, with the script as its own argument:

```js
const process = await sandbox.exec([
	"/bin/bash",
	"-lc",
	"echo hello && uname -a",
]);
const { stdout } = await process.output({ encoding: "utf8" });
```

```ts
const process = await sandbox.exec([
	"/bin/bash",
	"-lc",
	"echo hello && uname -a",
]);
const { stdout } = await process.output({ encoding: "utf8" });
```

## 4\. How this differs from the stable package

* `await sandbox.exec(...)` creates a process. It does **not** wait for exit. Use `output()`, `waitForExit()`, or other handle methods for completion.
* Each `exec()` is independent. A `cd` or `export` in one call is not remembered in the next.
* Pass `cwd` and `env` on each `exec()` when you need them, or use `setEnvVars` for sandbox-wide values. Refer to [Environment variables](https://developers.cloudflare.com/sandbox/1-0-preview/environment/).
* A process runs only in the **current container** for that sandbox. After the container stops or is replaced, start a new process. Model: [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/).
* Before production traffic, learn which failures are safe to retry: [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

## Next

* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/) — sandbox ID, container, stop, and replace
* [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/) — `exec()`, handles, and continuing work across requests
* [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/) — retries, interrupted calls, and stale handles
* [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/) — update an existing stable app
* [API reference](https://developers.cloudflare.com/sandbox/1-0-preview/api/) — processes, terminals, and errors
* [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/) — interactive PTY and browser connections
* [Extensions](https://developers.cloudflare.com/sandbox/1-0-preview/extensions/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/get-started/#page","headline":"Get started · Cloudflare Sandbox SDK docs","description":"Install @cloudflare/sandbox@next and run your first process handle in a sandbox.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/get-started/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Run Python, JavaScript, or TypeScript in a sandbox with the interpreter extension on @cloudflare/sandbox@next.
title: Code interpreter
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Code interpreter

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page uses the interpreter extension on `@cloudflare/sandbox@next`. On today's stable package, interpreter methods live on `Sandbox` — refer to [Use code interpreter](https://developers.cloudflare.com/sandbox/guides/code-execution/).

On `@next`, the code interpreter is an opt-in extension, not methods on bare `Sandbox`. Method names match the stable interpreter. You attach once, then call `sandbox.interpreter.*`. `runCode` returns plain serializable data across the Worker and Durable Object boundary.

Signatures and types: [Interpreter API](https://developers.cloudflare.com/sandbox/1-0-preview/api/interpreter/).

## Attach

```js
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withInterpreter } from "@cloudflare/sandbox/interpreter";

export class Sandbox extends BaseSandbox {
	interpreter = withInterpreter(this);
}
```

```ts
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withInterpreter } from "@cloudflare/sandbox/interpreter";

export class Sandbox extends BaseSandbox<Env> {
	interpreter = withInterpreter(this);
}
```

Export that class from your Worker. The sidecar provisions on first use.

## Image

| Language                | Image                                                    |
| ----------------------- | -------------------------------------------------------- |
| JavaScript / TypeScript | Default sandbox image (or any variant with a JS runtime) |
| Python                  | **\-python** image variant                               |

Use the same preview Worker package and container image line. Refer to [Dockerfile](https://developers.cloudflare.com/sandbox/configuration/dockerfile/).

## Run code

A **context** keeps variables and imports until you delete it or the container is replaced.

```js
const sandbox = getSandbox(env.Sandbox, "user-123");

const context = await sandbox.interpreter.createCodeContext({
	language: "python",
	cwd: "/workspace",
});

await sandbox.interpreter.runCode("x = 2", { context });
const result = await sandbox.interpreter.runCode("x * 21", { context });

if (result.error) {
	console.error(result.error.name, result.error.message);
} else {
	console.log(result.results, result.logs.stdout);
}
```

```ts
const sandbox = getSandbox(env.Sandbox, "user-123");

const context = await sandbox.interpreter.createCodeContext({
	language: "python",
	cwd: "/workspace",
});

await sandbox.interpreter.runCode("x = 2", { context });
const result = await sandbox.interpreter.runCode("x * 21", { context });

if (result.error) {
	console.error(result.error.name, result.error.message);
} else {
	console.log(result.results, result.logs.stdout);
}
```

If you omit `context`, `runCode` uses a default context for the language (default language: `python`). Languages: `python`, `javascript`, `typescript`.

For result fields, streaming (`runCodeStream`), and list/delete context methods, refer to the [Interpreter API](https://developers.cloudflare.com/sandbox/1-0-preview/api/interpreter/).

Contexts exist only in the **current container**. After stop or replace, create new ones. Refer to [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/).

## Related

* [Interpreter API](https://developers.cloudflare.com/sandbox/1-0-preview/api/interpreter/)
* [Extensions](https://developers.cloudflare.com/sandbox/1-0-preview/extensions/)
* [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)
* Stable guide: [Use code interpreter](https://developers.cloudflare.com/sandbox/guides/code-execution/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/#page","headline":"Code interpreter · Cloudflare Sandbox SDK docs","description":"Run Python, JavaScript, or TypeScript in a sandbox with the interpreter extension on @cloudflare/sandbox@next.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Sandbox IDs, containers, and what survives stop, replace, and destroy in the Sandbox SDK 1.0 preview.
title: Sandbox lifecycle
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Sandbox lifecycle

Last updated Aug 28, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page documents sandbox lifecycle on `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0\. For the current stable package, also refer to [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/concepts/sandboxes/). Platform placement and shutdown details live in [Lifecycle of a Container](https://developers.cloudflare.com/containers/concepts/architecture/).

Your app addresses a sandbox with a **sandbox ID**. The Linux environment that runs commands and holds local files is a **container**. The ID can outlive any one container.

That distinction matters for processes, terminals, files, and recovery after idle stop or replace.

## Sandbox ID and container

Most apps use one sandbox per user or task:

```js
const sandbox = getSandbox(env.Sandbox, "user-123");
```

```ts
const sandbox = getSandbox(env.Sandbox, "user-123");
```

|                         | Meaning                                                                                                                |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Sandbox ID**          | The string you pass to getSandbox (for example "user-123"). Use the same ID to reach the same sandbox later.           |
| **Durable Object**      | The coordinator behind that ID. The same ID maps to the same Durable Object identity.                                  |
| **Container**           | The current [Containers](https://developers.cloudflare.com/containers/) instance that runs Linux work for the sandbox. |
| **Process or terminal** | A program or interactive PTY inside the **current** container.                                                         |
| **Local files**         | Files on that container’s disk (for example under /workspace).                                                         |

**Same sandbox ID does not mean the same container.** After the container stops or is replaced, the next work for that ID may run in a new container.

## When the container starts

`getSandbox()` returns immediately. It does not start a container by itself.

The container starts when an operation needs it — for example `exec()`, `createTerminal()`, or writing a file. The first start after deploy or idle can take longer than a warm call. If the container is not ready yet, the SDK may throw `ContainerUnavailableError`. That error means the operation did not start inside the container. Refer to [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

## While the container is running

While a container is up for a sandbox ID:

* Processes and terminals keep running until they exit or you stop them.
* Local files stay available in that container.
* Later Worker requests can call `getProcess` or `getTerminal` and continue, as long as **that** container still has the resource.

Process detail: [How long a process lives](https://developers.cloudflare.com/sandbox/1-0-preview/processes/#how-long-a-process-lives). Terminals: [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/).

## When the container stops or is replaced

A container is not permanent. Cloudflare may stop it after idle time. It can also stop after failures, or when the platform replaces it during normal operations (for example after some deploys).

When that happens:

* Your app still uses the same sandbox ID.
* Processes and terminals from the old container are gone, including their IDs and live log buffers.
* Local files from the old container are gone unless your app restored them (for example with a [backup](https://developers.cloudflare.com/sandbox/guides/backup-restore/) or a mounted bucket).
* The next real work may start a **new** container for the same sandbox ID.
* Handles from the previous container fail closed. `getProcess` and `getTerminal` return `null` when the resource is not in the current container. Those lookups do not start a container only to answer the question.

To continue work later, store the **job** (what to run, `cwd`, `env`, and any app checkpoint), not only a process or terminal ID.

## Idle stop, replace, and destroy

| Event                                               | What stays                                    | What is gone                                                 |
| --------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------ |
| **Idle stop**                                       | Sandbox ID and Durable Object identity        | Processes, terminals, local files from the stopped container |
| **Replace** (failure, deploy, or other replacement) | Sandbox ID and Durable Object identity        | Same as idle stop for the previous container                 |
| **destroy()**                                       | The sandbox ID string can be used again later | Treat prior work for that generation as finished             |

After idle stop or replace, the next real work may start a **new** container for the same sandbox ID. Old process and terminal handles are invalid. Recovery procedures: [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

`keepAlive` and `sleepAfter` change idle behavior. They do not keep one container instance forever. Refer to the stable [Lifecycle API](https://developers.cloudflare.com/sandbox/api/lifecycle/) and [Sandbox options](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/) (ignore transport and default-session options on `@next`).

## State that outlives a container

Only what **your app** keeps (or restores) survives a new container:

| Need                             | What must live outside the container                                                                            |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Find the sandbox again           | The **sandbox ID**                                                                                              |
| Continue work on a later request | Resource ID **while** the current container still has it, plus enough job context to start again if it does not |
| Survive stop or replace          | The **job**: command or terminal setup, cwd, env, checkpoint                                                    |
| Keep files after a new container | Backup metadata, mount configuration, or another durable store                                                  |

## Related

* [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/)
* [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/)
* [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/)
* [Lifecycle of a Container](https://developers.cloudflare.com/containers/concepts/architecture/)
* Stable: [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/concepts/sandboxes/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/#page","headline":"Sandbox lifecycle · Cloudflare Sandbox SDK docs","description":"Sandbox IDs, containers, and what survives stop, replace, and destroy in the Sandbox SDK 1.0 preview.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-28","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Update an existing Sandbox SDK application from the stable package to @cloudflare/sandbox@next.
title: Migrate
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Migrate

Last updated Aug 28, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This guide moves a project onto `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0\. Migrate when you can so you are ready when 1.0 becomes the stable release. For the full preview section, refer to [1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/).

## Before you start

1. Work on a branch or staging deployment. Finish the code migration steps in this guide, then cut production over in one deploy.
2. Expect a short cutover window. Live processes, terminals, and other container work stop when the new image replaces the old one.
3. Inventory call sites in the Worker:  
  * Commands: `exec`, `execStream`, `startProcess`, string kill signals, process stdin
  * Sessions and transport: `createSession`, `enableDefaultSession`, `SANDBOX_TRANSPORT`, `setTransport`
  * Terminals: `sandbox.terminal`, session `terminal()`, xterm `sessionId`
  * Interpreter: `createCodeContext` / `runCode` on bare `Sandbox`
  * Git: `gitCheckout`

If you still need stable-line cleanup first (RPC transport, `exposePort`, stream helpers), complete the [2026 deprecation migration](https://developers.cloudflare.com/sandbox/guides/2026-deprecation/), then return here.

## What you will change

| Stable surface                                                | Preview action                                                                                                                                                                                          |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| SANDBOX\_TRANSPORT, transport on getSandbox(), setTransport() | Remove. The preview uses RPC automatically; no transport setting.                                                                                                                                       |
| await sandbox.exec(string) → buffered result                  | await sandbox.exec(argv) then await process.output(...).                                                                                                                                                |
| execStream, startProcess, process log helpers                 | Process handle: logs, kill, waitFor\*.                                                                                                                                                                  |
| Default session / enableDefaultSession                        | Gone. Each exec is independent.                                                                                                                                                                         |
| createSession / ExecutionSession                              | Gone from the core public surface. Pass cwd/env per exec, or one shell argv script.                                                                                                                     |
| Interpreter methods on Sandbox                                | Same method names on sandbox.interpreter after withInterpreter. runCode returns plain ExecutionResult. Refer to [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/). |
| String kill signals                                           | Numeric signals on process.kill.                                                                                                                                                                        |
| waitForPort default mode                                      | Preview default is **tcp**. Pass mode: "http" for HTTP checks.                                                                                                                                          |
| Process / stream **stdin**                                    | No process stdin on the handle. Non-interactive: argv/cwd/env. Interactive PTY: [terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/).                                          |
| sandbox.terminal(request) / session terminal()                | createTerminal, then terminal.connect(request).                                                                                                                                                         |
| xterm sessionId                                               | terminalId (and optional cursor).                                                                                                                                                                       |
| sandbox.gitCheckout(...)                                      | Removed. Run git with argv exec, for example \['git', 'clone', '--', url, dir\], then output() / waits as needed.                                                                                       |

Files, mounts, backups, ports, tunnels, `proxyToSandbox`, and most lifecycle options stay available. Use the main Sandbox docs for those signatures. Where a stable page still describes sessions, transport selection, string `exec` helpers, or `sandbox.terminal`, follow this preview section instead.

## Install the preview package and image

npmyarnpnpmbun

```
npm i @cloudflare/sandbox@next
```

```
yarn add @cloudflare/sandbox@next
```

```
pnpm add @cloudflare/sandbox@next
```

```
bun add @cloudflare/sandbox@next
```

Confirm the lockfile resolves `@cloudflare/sandbox` to a preview build. Point your Dockerfile at the matching preview image, for example `cloudflare/sandbox:next` (or the `-python` / other variant you use).

Do not mix a preview Worker package with a stable container image, or the reverse. Both sides must come from the same `@next` line.

## Remove transport selection

Delete `SANDBOX_TRANSPORT`, the `transport` option on `getSandbox()`, `SandboxTransport` types, and `sandbox.setTransport()`. No replacement setting is required.

## Migrate command execution

### Buffered commands

Stable:

```txt
const result = await sandbox.exec("npm test");
console.log(result.stdout, result.exitCode);
```

Preview:

```js
const process = await sandbox.exec(["/bin/bash", "-lc", "npm test"]);
const result = await process.output({ encoding: "utf8" });
console.log(result.stdout, result.exitCode);
```

```ts
const process = await sandbox.exec(["/bin/bash", "-lc", "npm test"]);
const result = await process.output({ encoding: "utf8" });
console.log(result.stdout, result.exitCode);
```

Rules:

* `await sandbox.exec(...)` means **launch succeeded**, not **command finished**.
* Prefer argv without a shell when you run a single binary: `['npm', 'test']` with `cwd` set.
* `output()` defaults to **byte** streams (`Uint8Array`). Pass `{ encoding: "utf8" }` for strings.
* There is no `sandbox.run()` compatibility helper on the current preview tip.

### Background processes and streaming

```js
const server = await sandbox.exec(["/bin/bash", "-lc", "npm run dev"], {
	cwd: "/workspace/app",
});

// Default readiness mode is TCP. Use mode: "http" when you need an HTTP check.
await server.waitForPort(3000, { timeout: 60_000 });
// await server.waitForPort(3000, { mode: "http", path: "/health", timeout: 60_000 });

const stream = await server.logs({ follow: true, replay: true });
// consume stream...

await server.kill(); // numeric signal; default 15
```

```ts
const server = await sandbox.exec(["/bin/bash", "-lc", "npm run dev"], {
	cwd: "/workspace/app",
});

// Default readiness mode is TCP. Use mode: "http" when you need an HTTP check.
await server.waitForPort(3000, { timeout: 60_000 });
// await server.waitForPort(3000, { mode: "http", path: "/health", timeout: 60_000 });

const stream = await server.logs({ follow: true, replay: true });
// consume stream...

await server.kill(); // numeric signal; default 15
```

Process handle details (waits, log events, `kill`, no stdin): [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/).

Across Worker requests, keep `server.id` and resume with `getProcess(id)` only while that process may still be running in the current container. If the container stopped, `getProcess` may return `null`. If you still hold a handle from a previous container, expect a stale-handle error. In both cases, start a new `exec` from the work you still need to run. Refer to [How long a process lives](https://developers.cloudflare.com/sandbox/1-0-preview/processes/#how-long-a-process-lives).

### Working directory and environment

| Stable                               | Preview                                                                                         |
| ------------------------------------ | ----------------------------------------------------------------------------------------------- |
| exec("cd /app"); exec("npm test");   | exec(\['/bin/bash', '-lc', 'cd /app && npm test'\]) or exec(\['npm', 'test'\], { cwd: '/app' }) |
| Exported vars in the default session | setEnvVars and/or env on each exec                                                              |
| createSession({ env })               | setEnvVars and/or env on each exec / createTerminal                                             |

Details: [Environment variables](https://developers.cloudflare.com/sandbox/1-0-preview/environment/).

Do not put live API keys or long-lived provider credentials into `setEnvVars` or launch `env`. Keep secrets in the Worker and inject them with [outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/) handlers when the process must call an external API.

### Timeouts and cancellation

| Goal                    | API                                                                             |
| ----------------------- | ------------------------------------------------------------------------------- |
| Limit process lifetime  | exec(argv, { timeout }) — may finish with timedOut: true                        |
| Limit how long you wait | Options or AbortSignal on output / waits / logs — does **not** kill the process |

## Drop session APIs

Remove `createSession`, `getSession`, `deleteSession`, and `sessionId` options on core calls.

User isolation remains **one sandbox per user** (or per trust boundary), not sessions inside one sandbox.

## Attach the interpreter

Refer to [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/). Minimum:

```js
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withInterpreter } from "@cloudflare/sandbox/interpreter";

export class Sandbox extends BaseSandbox {
	interpreter = withInterpreter(this);
}
```

```ts
import { Sandbox as BaseSandbox } from "@cloudflare/sandbox";
import { withInterpreter } from "@cloudflare/sandbox/interpreter";

export class Sandbox extends BaseSandbox<Env> {
	interpreter = withInterpreter(this);
}
```

Use the **`-python`** image variant when you run Python. Keep the Worker package and container image on the same `@next` line.

## Git

`sandbox.gitCheckout` is removed. Clone or fetch with argv `exec`, for example:

```js
const clone = await sandbox.exec(
	["git", "clone", "--depth", "1", "--", repoUrl, "/workspace/repo"],
	{ cwd: "/workspace" },
);
const result = await clone.output({ encoding: "utf8" });
```

```ts
const clone = await sandbox.exec(
	["git", "clone", "--depth", "1", "--", repoUrl, "/workspace/repo"],
	{ cwd: "/workspace" },
);
const result = await clone.output({ encoding: "utf8" });
```

## Terminals

Replace stable `sandbox.terminal(request)` (and session-scoped `terminal()`) with the preview terminal resource API:

1. `const terminal = await sandbox.createTerminal({ command: ['bash'], ... })`
2. Store `terminal.id` with the sandbox id.
3. On WebSocket upgrade: `getTerminal(id)` then `terminal.connect(request, { cursor?, cols?, rows? })`.
4. In the browser, `@cloudflare/sandbox/xterm` uses `terminalId`.

Details: [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/), [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/).

## Self-deployed bridge

This guide covers Worker SDK applications on `@next`.

The self-deployed [Sandbox bridge](https://developers.cloudflare.com/sandbox/bridge/) stays on the stable release line. Keep its Worker package, container image, and HTTP clients on matching stable versions. Do not pair a bridge deployment with `@cloudflare/sandbox@next`.

## Handle lifecycle the preview way

On `@next`, a **sandbox ID** stays stable, but the **container** behind it can be replaced. Processes and terminals live only in the current container. After replacement, old handles fail and you start the work again.

That is normal after idle time, restarts, and this migration cutover. Full model: [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/) and [How long a process lives](https://developers.cloudflare.com/sandbox/1-0-preview/processes/#how-long-a-process-lives). Recovery patterns: [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/). Catalog: [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/).

When you migrate long-running work:

1. Do not treat a stored `process.id` or `terminal.id` as enough to resume after an arbitrary delay or after deploy.
2. Persist the command, `cwd`, `env`, and any app checkpoint you need to relaunch.
3. On a later request, call `getProcess(id)` / `getTerminal(id)` only if that resource might still be running in the current container. If you get `null` or a stale-handle error, start again from the stored work.

Handle at least these errors as follows:

| Error                                                      | What to do                                                                                         |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| ContainerUnavailableError                                  | Container did not start the work — back off (retryAfterMs when set), then try the work again       |
| StaleProcessHandleError / StaleTerminalHandleError         | Previous container — start again from stored work state                                            |
| OperationInterruptedError                                  | Work may have started — read reason / retryable; check state before repeating                      |
| RPCTransportError                                          | Lost contact during the call — a later call may work; this call may already have run               |
| ProcessWaitTimeoutError / ProcessAbortedError              | Wait ended only — process may still be running                                                     |
| RuntimeControlProtocolError or unusable image after deploy | Worker package and container image must match on the same @next line; do not treat as a slow start |

`getProcess` / `getTerminal` / `list*` do not start a container. They return `null` or `[]` when none is running (not an exception).

## Deploy the cutover

Finish the code migration steps in this guide on a branch first. Production cutover is one deploy of the preview Worker package and the matching container image.

Stable Sandbox and `@next` use different control protocols. A mixed pair does not work in either direction: new Worker code against an old container image fails, and old Worker code against a new container image fails.

On a normal `wrangler deploy`, Worker code becomes active immediately while container instances can still update gradually. That leaves a window where new Worker code can reach old containers. For this migration, roll containers out in one step:

```sh
npx wrangler deploy --containers-rollout=immediate
```

`--containers-rollout=immediate` does not override [rollout\_active\_grace\_period](https://developers.cloudflare.com/workers/wrangler/configuration/#containers). Leave that setting at its default of `0` for the cutover (or set it to `0` if you raised it earlier). A nonzero grace period keeps active old containers eligible longer while the new Worker is already live.

Before production:

1. Finish or stop work you need to keep through the cutover.
2. Deploy with the immediate container rollout command from the previous section.
3. Wait until the new container image is serving traffic.
4. Treat process and terminal IDs from before the deploy as invalid. Start that work again and keep the new IDs.
5. Run the checks in [Verify](#verify).

For routine deploys after migration, refer to [Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/). For rollout options, refer to [Rollouts](https://developers.cloudflare.com/containers/configuration/rollouts/).

## Verify

1. Confirm the lockfile and Dockerfile are both on the same `@next` line, then deploy with `--containers-rollout=immediate`.
2. Run one argv `exec` and `output({ encoding: "utf8" })`.
3. Run one long-lived process with `waitForPort` or `logs`.
4. If the app uses a browser terminal: create, connect, and resume with `getTerminal` while the container still has it.
5. Exercise the interpreter only if your app uses that extension (Python needs `-python`).
6. Confirm error handling distinguishes unavailable, interrupted/RPC, stale handle, and local wait timeouts — [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).
7. Confirm secrets are not stored in sandbox env. Use outbound handlers where needed.
8. Grep again for removed APIs (transport, sessions, `execStream`, `startProcess`, `sandbox.terminal`, `gitCheckout`, xterm `sessionId`).

## Coding agents

Install [Cloudflare Skills ↗](https://github.com/cloudflare/skills) for your agent ([Agent setup](https://developers.cloudflare.com/agent-setup/)). The **`sandbox-migrate-to-next`** skill performs this migration. For new apps on `@next`, use **`sandbox-next`**. For day-to-day work on the current stable package, use **`sandbox-stable`**. Deprecated-API cleanup while staying on stable is in the [2026 deprecation guide](https://developers.cloudflare.com/sandbox/guides/2026-deprecation/) (and **`sandbox-stable`**) before or instead of this guide.

## Related

* [1.0 preview overview](https://developers.cloudflare.com/sandbox/1-0-preview/)
* [Get started](https://developers.cloudflare.com/sandbox/1-0-preview/get-started/)
* [Environment variables](https://developers.cloudflare.com/sandbox/1-0-preview/environment/)
* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/)
* [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/) (including [how long a process lives](https://developers.cloudflare.com/sandbox/1-0-preview/processes/#how-long-a-process-lives))
* [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/)
* [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/)
* [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/)
* [API reference](https://developers.cloudflare.com/sandbox/1-0-preview/api/)
* [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/)
* [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/)
* [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/)
* [Extensions](https://developers.cloudflare.com/sandbox/1-0-preview/extensions/)
* [Troubleshooting](https://developers.cloudflare.com/sandbox/1-0-preview/troubleshooting/)
* [Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/)
* [Deploy Containers](https://developers.cloudflare.com/containers/guides/deploy/)
* [Rollouts](https://developers.cloudflare.com/containers/configuration/rollouts/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/migrate/#page","headline":"Migrate · Cloudflare Sandbox SDK docs","description":"Update an existing Sandbox SDK application from the stable package to @cloudflare/sandbox@next.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/migrate/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-28","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: How the Sandbox SDK 1.0 preview runs commands — argv launches, process handles, and container lifetime.
title: Process execution
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Process execution

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/processes/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page documents process execution on `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0\. For today's stable command and session behavior, refer to [Commands](https://developers.cloudflare.com/sandbox/api/commands/) and [Sessions](https://developers.cloudflare.com/sandbox/concepts/sessions/).

In the 1.0 preview, treat the sandbox as a computer you drive with explicit programs.

Each `exec()` starts a **new supervised process** from **argv**. The call resolves when launch succeeds (you receive a process handle with `id` and `pid` properties), not when the process exits. Each launch is independent: pass `cwd` and `env` when the process needs them, or put multi-step shell syntax in one explicit shell argv. For an interactive PTY, use the [terminal](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/) API.

Long-running work often spans many short Worker requests. A process ID is enough only while the **same container** still has that process. Across idle stop, failure, or replace, store the full launch (command, options, and any app checkpoint) so you can start again. Refer to [Continue work across requests](#continue-work-across-requests).

## Sandbox ID, container, and process

Most applications use **one sandbox per user or task**:

```js
const sandbox = getSandbox(env.Sandbox, "user-123");
```

```ts
const sandbox = getSandbox(env.Sandbox, "user-123");
```

Three different things are in play:

| Term           | Meaning                                                                                                                                                                                                                               |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Sandbox ID** | The stable string your app uses to find that sandbox again (for example "user-123").                                                                                                                                                  |
| **Container**  | The [Containers](https://developers.cloudflare.com/containers/) instance currently running work for that sandbox. Sandboxes run on containers. The sandbox ID is stable. The container instance behind it is not always the same one. |
| **Process**    | A program you start with exec() **inside the current container**. The handle and process.id mean “this program in this container,” not “this sandbox ID forever.”                                                                     |

**Same sandbox ID does not mean the same container.** Processes live only in the container that started them. After a new container serves that ID, start new processes — you do not resume the old ones. Full sandbox model: [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/).

## Command model

The command model changes in the 1.0 preview compared with the current stable package:

| Current stable package                          | 1.0 preview                                           |
| ----------------------------------------------- | ----------------------------------------------------- |
| exec(string) resolves when the command finishes | exec(argv) resolves when the process starts           |
| Default session can preserve cd / export        | Each launch is independent                            |
| startProcess / execStream for other shapes      | One process handle covers short and long-running work |

Use argv for a single binary:

```js
const process = await sandbox.exec(["node", "--version"]);
const output = await process.output({ encoding: "utf8" });
```

```ts
const process = await sandbox.exec(["node", "--version"]);
const output = await process.output({ encoding: "utf8" });
```

Use an explicit shell when you need shell syntax:

```js
const process = await sandbox.exec([
	"/bin/bash",
	"-lc",
	"cd /workspace/app && npm test",
]);
```

```ts
const process = await sandbox.exec([
	"/bin/bash",
	"-lc",
	"cd /workspace/app && npm test",
]);
```

Or pass `cwd` and `env` on the launch instead of relying on a previous command:

```js
const process = await sandbox.exec(["npm", "test"], {
	cwd: "/workspace/app",
	env: { NODE_ENV: "test" },
});
```

```ts
const process = await sandbox.exec(["npm", "test"], {
	cwd: "/workspace/app",
	env: { NODE_ENV: "test" },
});
```

Sandbox-wide values use `setEnvVars`. Refer to [Environment variables](https://developers.cloudflare.com/sandbox/1-0-preview/environment/).

## Process handles

`await sandbox.exec(argv)` returns a **process handle**:

| Capability | Members                                                                          |
| ---------- | -------------------------------------------------------------------------------- |
| Identity   | id, pid                                                                          |
| Observe    | status(), logs(), output(), waitForExit(), waitForLog(), waitForPort(), exitCode |
| Control    | kill(signal?) with a numeric signal (default 15)                                 |

Observation timeouts and `AbortSignal` values cancel **only that wait or stream**. They do not stop the process. Call `kill()` when you intend to stop it.

`exec(argv, { timeout })` sets a **remote lifetime** deadline. When the supervisor stops the process for that deadline, completion can report `timedOut: true`.

For short commands, `output()` is enough. For large or long-running output, prefer `logs({ since, replay, follow })` and keep the latest **cursor** so a later request can resume the stream while the process is still in the current container. API details: [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/).

## How long a process lives

A process lives only as long as it keeps running **in the current container** for that sandbox. After the container stops, old process IDs are not valid on a later container that serves the same sandbox ID.

### When the container stops

The container for a sandbox is not meant to run forever. After a period with nothing to do, Cloudflare may stop it. The container can also stop after failures, or when the platform replaces it during normal operations (for example after some deploys).

When that happens:

* Your app still uses the same sandbox ID (`user-123`).
* Processes that were running in the old container have exited. Their process IDs and live log buffers from that container are gone.
* The next time you use the sandbox for real work, Cloudflare may start a **new** container for the same sandbox ID. You start new processes there. You do not reconnect to process IDs from the previous container. Files from the old container are not still there unless your app restored them (for example from a backup or a mounted bucket).

Container stop and replace are not new in 1.0\. The preview makes process handles fail closed after the container that owned them is gone: the SDK does not retarget an old process ID at a new container for the same sandbox ID.

### What you see in the API

| What you try                                              | What happens                                                                                                 |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| The process is still running in the current container     | getProcess(id) returns it; you can read logs and wait as usual                                               |
| No container is running for the sandbox yet               | getProcess and listProcesses return null / \[\]. They do **not** start a container just to answer the lookup |
| You still hold a handle from before the container stopped | Calls on that handle fail with StaleProcessHandleError                                                       |
| You need the same _job_ after a stop                      | Start a new exec() from the launch and checkpoint your app stored                                            |

Recovery procedures: [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

### Keep the container running

While a process or terminal is active, the container can stay running so work continues across requests. When nothing is active, the container may stop again after idle time. Long-running product flows should either keep meaningful work active or rely on checkpoints and relaunch.

## Continue work across requests

Worker requests are short. Sandbox processes often are not. Design the job so a later request can either **resume the same process** or **start the job again**.

### What to store

| Always useful                                         | When you stream logs                        |
| ----------------------------------------------------- | ------------------------------------------- |
| Sandbox ID                                            | Latest log **cursor** from delivered events |
| Full exec argv                                        |                                             |
| cwd and env if the launch needs them                  |                                             |
| Application checkpoint (repo path, step, agent state) |                                             |

A process ID is a resume key for the **current** container only. It is not enough to restart the job after the container may have stopped.

### Case 1: The container still has the process

Use this path when the work is still running and the container has not been replaced — for example another request arrives seconds later while a build or server is up.

```js
const process = await sandbox.getProcess(storedProcessId);
if (process) {
	const stream = await process.logs({
		since: storedCursor,
		replay: true,
		follow: true,
	});
	// consume events; keep the latest cursor from each event
	return;
}
```

```ts
const process = await sandbox.getProcess(storedProcessId);
if (process) {
	const stream = await process.logs({
		since: storedCursor,
		replay: true,
		follow: true,
	});
	// consume events; keep the latest cursor from each event
	return;
}
```

You can also call `status()`, `waitForPort()`, `waitForExit()`, or `kill()` on that handle. Log cursors apply only while this process still exists in this container.

### Case 2: The process is gone — start from the stored job

Use this path when `getProcess` returns `null`, a call throws `StaleProcessHandleError`, or enough time has passed that the container may have stopped or been replaced.

```js
const process = await sandbox.exec(storedCommand, {
	cwd: storedCwd,
	env: storedEnv,
});
// persist process.id (and clear any old cursor)
await process.waitForPort(3000, { timeout: 60_000 });
```

```ts
const process = await sandbox.exec(storedCommand, {
	cwd: storedCwd,
	env: storedEnv,
});
// persist process.id (and clear any old cursor)
await process.waitForPort(3000, { timeout: 60_000 });
```

If the job also depends on files that lived only in the previous container, [back up and restore](https://developers.cloudflare.com/sandbox/guides/backup-restore/) those directories or mount durable storage before relying on the tree again. Backup and restore replace filesystem state. They do not bring back old process IDs or log buffers.

If the container is not ready yet, you may get `ContainerUnavailableError` — back off and run the same unit of work again. Refer to [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

### Choose a path

```js
async function continueJob(sandbox, job) {
	if (job.processId) {
		const existing = await sandbox.getProcess(job.processId);
		if (existing) {
			return existing; // Case 1 — same container, same process
		}
		// null: no container, or this ID is not in the current container
	}

	// Case 2 — relaunch from stored command and checkpoint
	const process = await sandbox.exec(job.command, {
		cwd: job.cwd,
		env: job.env,
	});
	job.processId = process.id;
	job.cursor = undefined;
	return process;
}
```

```ts
async function continueJob(sandbox: Sandbox, job: StoredJob) {
	if (job.processId) {
		const existing = await sandbox.getProcess(job.processId);
		if (existing) {
			return existing; // Case 1 — same container, same process
		}
		// null: no container, or this ID is not in the current container
	}

	// Case 2 — relaunch from stored command and checkpoint
	const process = await sandbox.exec(job.command, {
		cwd: job.cwd,
		env: job.env,
	});
	job.processId = process.id;
	job.cursor = undefined;
	return process;
}
```

If you still hold a handle object from before the container stopped, calls on that handle throw `StaleProcessHandleError`. Prefer `getProcess(id)` on each new request instead of reusing an old handle across requests.

## Processes and terminals

|        | Process (exec)             | Terminal                            |
| ------ | -------------------------- | ----------------------------------- |
| Role   | Supervised argv process    | Interactive PTY                     |
| Input  | Launch-time argv           | PTY input (write / browser connect) |
| Stop   | kill(signal?)              | interrupt() / terminate()           |
| Lookup | getProcess / listProcesses | getTerminal / listTerminals         |

Both follow the same [container lifetime rules](#how-long-a-process-lives). Terminal docs: [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/). API: [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/).

## Logs and large output

`output()` buffers stdout and stderr and may set `truncated: true`. Prefer `logs()` when output may be large or the process runs longer than one Worker request.

```js
const stream = await process.logs({ follow: true, replay: true });
// each data/terminal event includes a cursor — store the latest
```

```ts
const stream = await process.logs({ follow: true, replay: true });
// each data/terminal event includes a cursor — store the latest
```

On a later request against the **same still-running container**, call `getProcess(id)` and resume with `logs({ since: cursor, replay: true, follow: true })`. After a new container starts for the sandbox, start a new process; the old cursor does not apply.

Event shapes, wait options, and readiness checks: [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/).

## Related

* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/)
* [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/)
* [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/)
* [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/)
* [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/)
* [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)
* [Get started](https://developers.cloudflare.com/sandbox/1-0-preview/get-started/)
* [1.0 preview overview](https://developers.cloudflare.com/sandbox/1-0-preview/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/processes/#page","headline":"Process execution · Cloudflare Sandbox SDK docs","description":"How the Sandbox SDK 1.0 preview runs commands — argv launches, process handles, and container lifetime.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/processes/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Interactive PTY terminals in the Sandbox SDK 1.0 preview — resource model and browser connect.
title: Terminals
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Terminals

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page documents terminals on `@cloudflare/sandbox@next`, the preview of Sandbox SDK 1.0\. For the current stable terminal helpers, refer to [Terminal connections](https://developers.cloudflare.com/sandbox/concepts/terminal/) and [Terminal API](https://developers.cloudflare.com/sandbox/api/terminal/).

A **terminal** is an interactive PTY in the current container for a sandbox. Use it for full-duplex terminal I/O: a browser shell, resize, interrupt, and reconnect.

Command execution uses [exec](https://developers.cloudflare.com/sandbox/1-0-preview/processes/) and process handles. Terminals are a separate resource type. API reference: [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/).

## Processes and terminals

|        | Process (exec)                                               | Terminal                                   |
| ------ | ------------------------------------------------------------ | ------------------------------------------ |
| Role   | Supervised argv process                                      | Interactive PTY                            |
| Input  | Launch-time argv (and whatever the program reads on its own) | PTY input via write() or browser connect() |
| Output | logs(), output(), waits                                      | output(), snapshot, waitForExit()          |
| Stop   | kill(signal?)                                                | interrupt() / terminate()                  |
| Lookup | getProcess / listProcesses                                   | getTerminal / listTerminals                |

Both kinds of resource live only in the current container for a sandbox ID. Lookup methods do not start a container. Refer to [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/) and [How long a process lives](https://developers.cloudflare.com/sandbox/1-0-preview/processes/#how-long-a-process-lives).

## Create a terminal

```js
const terminal = await sandbox.createTerminal({
	command: ["bash"],
	cwd: "/workspace",
	cols: 120,
	rows: 40,
});

console.log(terminal.id);
```

```ts
const terminal = await sandbox.createTerminal({
	command: ["bash"],
	cwd: "/workspace",
	cols: 120,
	rows: 40,
});

console.log(terminal.id);
```

You can write to the PTY from the Worker, resize it, stream output, or end it:

```js
await terminal.write(new TextEncoder().encode("uname -a\n"));
await terminal.resize(100, 30);
await terminal.terminate();
```

```ts
await terminal.write(new TextEncoder().encode("uname -a\n"));
await terminal.resize(100, 30);
await terminal.terminate();
```

## Lifetime

* A terminal exists only in the **current container** for that sandbox ID.
* `getTerminal` / `listTerminals` return `null` / `[]` when no container is running. They do not start one.
* After the container stops or is replaced, old terminal IDs are invalid. Create a new terminal if you need one again.
* An active terminal can keep the container alive across Worker requests, as an active process can.

Store `terminal.id` to resume the same PTY while that container is still up.

## Browser connect

1. Create a terminal and keep `terminal.id` with the sandbox id.
2. On each WebSocket upgrade, resolve the terminal with `getTerminal`, then return `terminal.connect(request)`.
3. In the browser, use `@cloudflare/sandbox/xterm` with **`terminalId`**.

### Worker

```js
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		if (
			url.pathname === "/ws/terminal" &&
			request.headers.get("Upgrade")?.toLowerCase() === "websocket"
		) {
			const sandboxId = url.searchParams.get("sandboxId");
			const terminalId = url.searchParams.get("terminalId");
			if (!sandboxId || !terminalId) {
				return new Response("sandboxId and terminalId are required", {
					status: 400,
				});
			}

			const sandbox = getSandbox(env.Sandbox, sandboxId);
			const terminal = await sandbox.getTerminal(terminalId);
			if (!terminal) {
				return new Response("Terminal not found", { status: 404 });
			}

			return terminal.connect(request, {
				cursor: url.searchParams.get("cursor") ?? undefined,
			});
		}

		return new Response("Not found", { status: 404 });
	},
};
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		if (
			url.pathname === "/ws/terminal" &&
			request.headers.get("Upgrade")?.toLowerCase() === "websocket"
		) {
			const sandboxId = url.searchParams.get("sandboxId");
			const terminalId = url.searchParams.get("terminalId");
			if (!sandboxId || !terminalId) {
				return new Response("sandboxId and terminalId are required", {
					status: 400,
				});
			}

			const sandbox = getSandbox(env.Sandbox, sandboxId);
			const terminal = await sandbox.getTerminal(terminalId);
			if (!terminal) {
				return new Response("Terminal not found", { status: 404 });
			}

			return terminal.connect(request, {
				cursor: url.searchParams.get("cursor") ?? undefined,
			});
		}

		return new Response("Not found", { status: 404 });
	},
};
```

Create the terminal from an application route when the UI needs one:

```js
const sandboxId = "user-123";
const sandbox = getSandbox(env.Sandbox, sandboxId);
const terminal = await sandbox.createTerminal({ command: ["bash"] });
return Response.json({ sandboxId, terminalId: terminal.id });
```

```ts
const sandboxId = "user-123";
const sandbox = getSandbox(env.Sandbox, sandboxId);
const terminal = await sandbox.createTerminal({ command: ["bash"] });
return Response.json({ sandboxId, terminalId: terminal.id });
```

### Browser (xterm.js)

npmyarnpnpmbun

```
npm install @xterm/xterm @xterm/addon-fit @cloudflare/sandbox@next
```

```
yarn install @xterm/xterm @xterm/addon-fit @cloudflare/sandbox@next
```

```
pnpm install @xterm/xterm @xterm/addon-fit @cloudflare/sandbox@next
```

```
bun install @xterm/xterm @xterm/addon-fit @cloudflare/sandbox@next
```

```js
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { SandboxAddon } from "@cloudflare/sandbox/xterm";
import "@xterm/xterm/css/xterm.css";

const term = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon();
const sandboxAddon = new SandboxAddon({
	// `origin` is already a WebSocket origin (`wss://` or `ws://`).
	getWebSocketUrl: ({ sandboxId, terminalId, cursor, origin }) => {
		const params = new URLSearchParams({ sandboxId });
		if (terminalId) params.set("terminalId", terminalId);
		if (cursor) params.set("cursor", cursor);
		return `${origin}/ws/terminal?${params}`;
	},
	reconnect: true,
});

term.loadAddon(fitAddon);
term.loadAddon(sandboxAddon);
term.open(document.getElementById("terminal"));
fitAddon.fit();

// Values returned by your create-terminal route
const sandboxId = "user-123";
const terminalId = "term_...";
sandboxAddon.connect({ sandboxId, terminalId });
```

```ts
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { SandboxAddon } from "@cloudflare/sandbox/xterm";
import "@xterm/xterm/css/xterm.css";

const term = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon();
const sandboxAddon = new SandboxAddon({
	// `origin` is already a WebSocket origin (`wss://` or `ws://`).
	getWebSocketUrl: ({ sandboxId, terminalId, cursor, origin }) => {
		const params = new URLSearchParams({ sandboxId });
		if (terminalId) params.set("terminalId", terminalId);
		if (cursor) params.set("cursor", cursor);
		return `${origin}/ws/terminal?${params}`;
	},
	reconnect: true,
});

term.loadAddon(fitAddon);
term.loadAddon(sandboxAddon);
term.open(document.getElementById("terminal")!);
fitAddon.fit();

// Values returned by your create-terminal route
const sandboxId = "user-123";
const terminalId = "term_...";
sandboxAddon.connect({ sandboxId, terminalId });
```

| Stable package            | Preview                                  |
| ------------------------- | ---------------------------------------- |
| sandbox.terminal(request) | createTerminal \+ getTerminal \+ connect |
| xterm / URL sessionId     | terminalId (and optional cursor)         |

## Related

* [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/)
* [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/)
* [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/)
* [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/)
* [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/)
* [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/terminals/#page","headline":"Terminals · Cloudflare Sandbox SDK docs","description":"Interactive PTY terminals in the Sandbox SDK 1.0 preview — resource model and browser connect.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/terminals/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Common failures on @cloudflare/sandbox@next and where to fix them.
title: Troubleshooting
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Troubleshooting

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/1-0-preview/troubleshooting/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Path to Sandbox SDK 1.0

This page is for `@cloudflare/sandbox@next`. Stable-package symptoms may differ.

Use this symptom-to-fix map. For deeper recovery, refer to [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/). For lifecycle behavior, refer to [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/).

## Deploy and image

| Symptom                                                                      | What to check                                                                                                                                                                         |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| RuntimeControlProtocolError, control/protocol failures after deploy          | Worker package and container image are on different lines. Use the same @next / cloudflare/sandbox:next (or the same exact prerelease) pair.                                          |
| Container never becomes ready, or you see repeated ContainerUnavailableError | Cold start or capacity. Back off using retryAfterMs when set, then retry the **work**. Refer to [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/). |
| Works in wrangler dev, fails in production only                              | Production-only limits and cold start. Still keep package/image matched.                                                                                                              |

## Processes

| Symptom                                         | What to check                                                                                                                                  |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| await exec “finished” but the command did not   | exec resolves on **launch**. Use output(), waitForExit(), or exitCode.                                                                         |
| No stdout as a string                           | output() defaults to bytes. Pass { encoding: "utf8" }.                                                                                         |
| getProcess is null / list is \[\]               | No container running, or ID unknown in the **current** container. Discovery does not wake a sandbox. Relaunch from stored job state if needed. |
| StaleProcessHandleError                         | Handle was from a previous container. Start a new exec from checkpointed work.                                                                 |
| Wait timed out / aborted but process still runs | Local wait only. Call kill() if you intend to stop it.                                                                                         |
| Port never becomes ready                        | Default waitForPort mode is **TCP**. Use mode: "http" for HTTP checks. Process may have exited — check status/logs.                            |
| Need interactive stdin                          | Not on the process handle. Use a [terminal](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/) or non-interactive argv/cwd/env. |

## Terminals

| Symptom                      | What to check                                                                 |
| ---------------------------- | ----------------------------------------------------------------------------- |
| Browser still uses sessionId | Preview xterm helper expects terminalId.                                      |
| getTerminal is null          | Same lifetime rules as processes. Create again if the container was replaced. |
| Reconnect has no history     | Pass the last cursor into connect / output options.                           |

## Environment and secrets

| Symptom                                | What to check                                                                                                                                              |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Env from an earlier exec “disappeared” | No session shell. Use setEnvVars and/or per-launch env. [Environment variables](https://developers.cloudflare.com/sandbox/1-0-preview/environment/).       |
| API keys leaked into the container     | Do not put live secrets in sandbox env. Use [outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/) handlers on the Worker. |

## Interpreter

| Symptom                                     | What to check                                              |
| ------------------------------------------- | ---------------------------------------------------------- |
| sandbox.createCodeContext is not a function | Attach withInterpreter and call sandbox.interpreter.\*.    |
| Python not available                        | Use the **\-python** image variant on the same @next line. |

## Bridge HTTP

| Symptom                                                                     | What to check                                                                                                                                                                         |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Bridge /exec, sessions, or /pty behavior differs from @next Worker SDK docs | The self-deployed bridge is not part of the 1.0 preview. Use the [stable bridge](https://developers.cloudflare.com/sandbox/bridge/) with matching stable package and container image. |

## Agents and long-running jobs

For agents and long-running tools on `@next`:

1. Launch with `exec(argv)` (often `['/bin/bash', '-lc', script]`).
2. Wait with `waitForLog`, `waitForPort`, or `logs` — not only `await exec`.
3. Persist **job state** (command, `cwd`, `env`, checkpoint), not only `process.id`.
4. On a later request: `getProcess(id)` while the same container may still hold it; otherwise `exec` again.
5. Use a [terminal](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/) only when you need a human PTY, not as a session substitute.

Refer to [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/), [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/), and examples in the [sandbox-sdk ↗](https://github.com/cloudflare/sandbox-sdk/tree/next/examples) repo (`claude-code`, `codex`, `opencode`, and others).

## Related

* [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/)
* [Errors API](https://developers.cloudflare.com/sandbox/1-0-preview/api/errors/)
* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/)
* [Process API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/)
* [Terminal API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/)
* [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/1-0-preview/troubleshooting/#page","headline":"Troubleshooting · Cloudflare Sandbox SDK docs","description":"Common failures on @cloudflare/sandbox@next and where to fix them.","url":"https://developers.cloudflare.com/sandbox/1-0-preview/troubleshooting/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Step-by-step Sandbox SDK tutorials for building AI agents, code executors, and testing pipelines.
title: Tutorials
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Tutorials

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Coming soon: Sandbox SDK 1.0

These tutorials target today's stable `@cloudflare/sandbox` package and may use sessions, string `exec`, `startProcess`, `gitCheckout`, or the stable bridge template.

For **`@next`** Worker SDK work, start from [1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/) and [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/). Bridge deployments stay on the [stable bridge](https://developers.cloudflare.com/sandbox/bridge/).

Learn how to build applications with Sandbox SDK through step-by-step tutorials. Each tutorial takes 20-30 minutes.

[**Build an AI coding agent with OpenAI Agents SDK**Use the OpenAI Agents SDK with Cloudflare Sandbox to build a Python agent that writes, tests, and delivers code in an isolated environment.](https://developers.cloudflare.com/sandbox/tutorials/openai-agents/)

[**Code interpreter with Workers AI**Build a code interpreter using Workers AI GPT-OSS model with the official workers-ai-provider package.](https://developers.cloudflare.com/sandbox/tutorials/workers-ai-code-interpreter/)

[**Data persistence with R2**Mount R2 buckets as local filesystem paths to persist data across sandbox lifecycles.](https://developers.cloudflare.com/sandbox/tutorials/persistent-storage/)

[**Run Claude Code on a Sandbox**Use Claude Code to implement a task in your GitHub repository.](https://developers.cloudflare.com/sandbox/tutorials/claude-code/)

[**Build an AI code executor**Use Claude to generate Python code from natural language and execute it securely in sandboxes.](https://developers.cloudflare.com/sandbox/tutorials/ai-code-executor/)

[**Analyze data with AI**Upload CSV files, generate analysis code with Claude, and return visualizations.](https://developers.cloudflare.com/sandbox/tutorials/analyze-data-with-ai/)

[**Automated testing pipeline**Build a testing pipeline that clones Git repositories, installs dependencies, runs tests, and reports results.](https://developers.cloudflare.com/sandbox/tutorials/automated-testing-pipeline/)

[**Build a code review bot**Clone repositories, analyze code with Claude, and post review comments to GitHub PRs.](https://developers.cloudflare.com/sandbox/tutorials/code-review-bot/)

[**Set up Claude Managed Agents**Run Claude Managed Agents on self-managed Cloudflare environments.](https://developers.cloudflare.com/sandbox/tutorials/claude-managed-agents/)

[**Run Cursor Cloud Agents on Cloudflare via self-hosted machines**Deploy Cursor self-hosted machines that run each assigned session in an isolated Cloudflare container.](https://developers.cloudflare.com/sandbox/tutorials/cursor-cloud-agents/)

[**Run Devin Outposts on Cloudflare**Deploy a Devin Outpost that runs each Devin session in an isolated Cloudflare container.](https://developers.cloudflare.com/sandbox/tutorials/devin-outposts/)

[**Run Codex with Cloudflare Containers using the OpenAI Agents API**Deploy a Cloudflare execution environment that can be used by Codex via the OpenAI Agents API.](https://developers.cloudflare.com/sandbox/tutorials/openai-agents-api/)

## Before you start

All tutorials assume you have:

* Completed the [Get Started guide](https://developers.cloudflare.com/sandbox/get-started/)
* Basic familiarity with [Workers](https://developers.cloudflare.com/workers/)
* [Docker ↗](https://www.docker.com/) installed and running

## Related resources

* [How-to guides](https://developers.cloudflare.com/sandbox/guides/) \- Solve specific problems
* [API reference](https://developers.cloudflare.com/sandbox/api/) \- Complete SDK reference

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/sandbox/tutorials/#page","headline":"Tutorials · Cloudflare Sandbox SDK docs","description":"Step-by-step Sandbox SDK tutorials for building AI agents, code executors, and testing pipelines.","url":"https://developers.cloudflare.com/sandbox/tutorials/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Use Claude to generate Python code from natural language and execute it securely in sandboxes.
title: Build an AI code executor
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Build an AI code executor

Last updated May 5, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/ai-code-executor/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Build an AI-powered code execution system using Sandbox SDK and Claude. Turn natural language questions into Python code, execute it securely, and return results.

**Time to complete:** 20 minutes

## What you'll build

An API that accepts questions like "What's the 100th Fibonacci number?", uses Claude to generate Python code, executes it in an isolated sandbox, and returns the results.

## Prerequisites

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

Node.js version manager

Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), discussed later in this guide, requires a Node version of `16.17.0` or later.

You'll also need:

* An [Anthropic API key ↗](https://console.anthropic.com/) for Claude
* [Docker ↗](https://www.docker.com/) running locally

## 1\. Create your project

Create a new Sandbox SDK project:

npmyarnpnpm

```
npm create cloudflare@latest -- ai-code-executor --template=cloudflare/sandbox-sdk/examples/minimal
```

```
yarn create cloudflare ai-code-executor --template=cloudflare/sandbox-sdk/examples/minimal
```

```
pnpm create cloudflare@latest ai-code-executor --template=cloudflare/sandbox-sdk/examples/minimal
```

```sh
cd ai-code-executor
```

## 2\. Install dependencies

Install the Anthropic SDK:

npmyarnpnpmbun

```
npm i @anthropic-ai/sdk
```

```
yarn add @anthropic-ai/sdk
```

```
pnpm add @anthropic-ai/sdk
```

```
bun add @anthropic-ai/sdk
```

## 3\. Build your code executor

Replace the contents of `src/index.ts`:

```typescript
import { getSandbox, type Sandbox } from '@cloudflare/sandbox';
import Anthropic from '@anthropic-ai/sdk';

export { Sandbox } from '@cloudflare/sandbox';

interface Env {
	Sandbox: DurableObjectNamespace<Sandbox>;
	ANTHROPIC_API_KEY: string;
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		if (request.method !== 'POST' || new URL(request.url).pathname !== '/execute') {
			return new Response('POST /execute with { "question": "your question" }');
		}

		try {
			const { question } = await request.json();

			if (!question) {
				return Response.json({ error: 'Question is required' }, { status: 400 });
			}

			// Use Claude to generate Python code
			const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
			const codeGeneration = await anthropic.messages.create({
				model: 'claude-sonnet-4-5',
				max_tokens: 1024,
				messages: [{
					role: 'user',
					content: `Generate Python code to answer: "${question}"

Requirements:
- Use only Python standard library
- Print the result using print()
- Keep code simple and safe

Return ONLY the code, no explanations.`
				}],
			});

			const generatedCode = codeGeneration.content[0]?.type === 'text'
				? codeGeneration.content[0].text
				: '';

			if (!generatedCode) {
				return Response.json({ error: 'Failed to generate code' }, { status: 500 });
			}

			// Strip markdown code fences if present
			const cleanCode = generatedCode
				.replace(/^```python?\n?/, '')
				.replace(/\n?```\s*$/, '')
				.trim();

			// Execute the code in a sandbox
			const sandbox = getSandbox(env.Sandbox, 'demo-user');
			await sandbox.writeFile('/tmp/code.py', cleanCode);
			const result = await sandbox.exec('python /tmp/code.py');

			return Response.json({
				success: result.success,
				question,
				code: generatedCode,
				output: result.stdout,
				error: result.stderr
			});

		} catch (error: any) {
			return Response.json(
				{ error: 'Internal server error', message: error.message },
				{ status: 500 }
			);
		}
	},
};
```

**How it works:**

1. Receives a question via POST to `/execute`
2. Uses Claude to generate Python code
3. Writes code to `/tmp/code.py` in the sandbox
4. Executes with `sandbox.exec('python /tmp/code.py')`
5. Returns both the code and execution results

## 4\. Set up local environment variables

Create a `.dev.vars` file in your project root for local development:

```sh
echo "ANTHROPIC_API_KEY=your_api_key_here" > .dev.vars
```

Replace `your_api_key_here` with your actual API key from the [Anthropic Console ↗](https://console.anthropic.com/).

Note

The `.dev.vars` file is automatically gitignored and only used during local development with `npm run dev`.

## 5\. Test locally

Start the development server:

```sh
npm run dev
```

Note

First run builds the Docker container (2-3 minutes). Subsequent runs are much faster.

Test with curl:

```sh
curl -X POST http://localhost:8787/execute \
  -H "Content-Type: application/json" \
  -d '{"question": "What is the 10th Fibonacci number?"}'
```

Response:

```json
{
  "success": true,
  "question": "What is the 10th Fibonacci number?",
  "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)\n\nprint(fibonacci(10))",
  "output": "55\n",
  "error": ""
}
```

## 6\. Deploy

Deploy your Worker:

```sh
npx wrangler deploy
```

Then set your Anthropic API key as a production secret:

```sh
npx wrangler secret put ANTHROPIC_API_KEY
```

Paste your API key from the [Anthropic Console ↗](https://console.anthropic.com/) when prompted.

Caution

After first deployment, wait 2-3 minutes for container provisioning. Check status with `npx wrangler containers list`.

## 7\. Test your deployment

Try different questions:

```sh
# Factorial
curl -X POST https://ai-code-executor.YOUR_SUBDOMAIN.workers.dev/execute \
  -H "Content-Type: application/json" \
  -d '{"question": "Calculate the factorial of 5"}'

# Statistics
curl -X POST https://ai-code-executor.YOUR_SUBDOMAIN.workers.dev/execute \
  -H "Content-Type: application/json" \
  -d '{"question": "What is the mean of [10, 20, 30, 40, 50]?"}'

# String manipulation
curl -X POST https://ai-code-executor.YOUR_SUBDOMAIN.workers.dev/execute \
  -H "Content-Type: application/json" \
  -d '{"question": "Reverse the string \"Hello World\""}'
```

## What you built

You created an AI code execution system that:

* Accepts natural language questions
* Generates Python code with Claude
* Executes code securely in isolated sandboxes
* Returns results with error handling

## Next steps

* [Code interpreter with Workers AI](https://developers.cloudflare.com/sandbox/tutorials/workers-ai-code-interpreter/) \- Use Cloudflare's native AI models with official packages
* [Analyze data with AI](https://developers.cloudflare.com/sandbox/tutorials/analyze-data-with-ai/) \- Add pandas and matplotlib for data analysis
* [Code Interpreter API](https://developers.cloudflare.com/sandbox/api/interpreter/) \- Use the built-in code interpreter instead of exec
* [Streaming output](https://developers.cloudflare.com/sandbox/guides/streaming-output/) \- Show real-time execution progress
* [API reference](https://developers.cloudflare.com/sandbox/api/) \- Explore all available methods

## Related resources

* [Anthropic Claude documentation ↗](https://docs.anthropic.com/)
* [Workers AI](https://developers.cloudflare.com/workers-ai/) \- Use Cloudflare's built-in models
* [workers-ai-provider package ↗](https://github.com/cloudflare/ai/tree/main/packages/workers-ai-provider) \- Official Workers AI integration

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/ai-code-executor/#page","headline":"Build an AI code executor · Cloudflare Sandbox SDK docs","description":"Use Claude to generate Python code from natural language and execute it securely in sandboxes.","url":"https://developers.cloudflare.com/sandbox/tutorials/ai-code-executor/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-05","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Upload CSV files, generate analysis code with Claude, and return visualizations.
title: Analyze data with AI
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Analyze data with AI

Last updated May 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/analyze-data-with-ai/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Build an AI-powered data analysis system that accepts CSV uploads, uses Claude to generate Python analysis code, executes it in sandboxes, and returns visualizations.

**Time to complete**: 25 minutes

## Prerequisites

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

Node.js version manager

Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), discussed later in this guide, requires a Node version of `16.17.0` or later.

You'll also need:

* An [Anthropic API key ↗](https://console.anthropic.com/) for Claude
* [Docker ↗](https://www.docker.com/) running locally

## 1\. Create your project

Create a new Sandbox SDK project:

npmyarnpnpm

```
npm create cloudflare@latest -- analyze-data --template=cloudflare/sandbox-sdk/examples/minimal
```

```
yarn create cloudflare analyze-data --template=cloudflare/sandbox-sdk/examples/minimal
```

```
pnpm create cloudflare@latest analyze-data --template=cloudflare/sandbox-sdk/examples/minimal
```

```sh
cd analyze-data
```

## 2\. Install dependencies

npmyarnpnpmbun

```
npm i @anthropic-ai/sdk
```

```
yarn add @anthropic-ai/sdk
```

```
pnpm add @anthropic-ai/sdk
```

```
bun add @anthropic-ai/sdk
```

## 3\. Build the analysis handler

Replace `src/index.ts`:

```typescript
import { getSandbox, proxyToSandbox, type Sandbox } from "@cloudflare/sandbox";
import Anthropic from "@anthropic-ai/sdk";

export { Sandbox } from "@cloudflare/sandbox";

interface Env {
	Sandbox: DurableObjectNamespace<Sandbox>;
	ANTHROPIC_API_KEY: string;
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const proxyResponse = await proxyToSandbox(request, env);
		if (proxyResponse) return proxyResponse;

		if (request.method !== "POST") {
			return Response.json(
				{ error: "POST CSV file and question" },
				{ status: 405 },
			);
		}

		try {
			const formData = await request.formData();
			const csvFile = formData.get("file") as File;
			const question = formData.get("question") as string;

			if (!csvFile || !question) {
				return Response.json(
					{ error: "Missing file or question" },
					{ status: 400 },
				);
			}

			// Upload CSV to sandbox
			const sandbox = getSandbox(env.Sandbox, `analysis-${Date.now()}`);
			const csvPath = "/workspace/data.csv";
			await sandbox.writeFile(csvPath, await csvFile.text());

			// Analyze CSV structure
			const structure = await sandbox.exec(
				`python3 -c "import pandas as pd; df = pd.read_csv('${csvPath}'); print(f'Rows: {len(df)}'); print(f'Columns: {list(df.columns)[:5]}')"`,
			);

			if (!structure.success) {
				return Response.json(
					{ error: "Failed to read CSV", details: structure.stderr },
					{ status: 400 },
				);
			}

			// Generate analysis code with Claude
			const code = await generateAnalysisCode(
				env.ANTHROPIC_API_KEY,
				csvPath,
				question,
				structure.stdout,
			);

			// Write and execute the analysis code
			await sandbox.writeFile("/workspace/analyze.py", code);
			const result = await sandbox.exec("python /workspace/analyze.py");

			if (!result.success) {
				return Response.json(
					{ error: "Analysis failed", details: result.stderr },
					{ status: 500 },
				);
			}

			async function streamToBase64(stream) {
			  const blob = await new Response(stream).blob();
			  const buffer = await blob.arrayBuffer();
			  const bytes = new Uint8Array(buffer);

			  // Convert to base64
			  let binary = '';
			  for (let i = 0; i < bytes.length; i++) {
			    binary += String.fromCharCode(bytes[i]);
			  }
			  return btoa(binary);
			}

			// Check for generated chart
			let chart = null;
			try {
				const { content, mimeType } = await sandbox.readFile("/workspace/chart.png", {
					encoding: "none"
				});
				chart = `data:${mimeType};base64,${await streamToBase64(content)}`;
			} catch {
				// No chart generated
			}

			await sandbox.destroy();

			return Response.json({
				success: true,
				output: result.stdout,
				chart,
				code,
			});
		} catch (error: any) {
			return Response.json({ error: error.message }, { status: 500 });
		}
	},
};

async function generateAnalysisCode(
	apiKey: string,
	csvPath: string,
	question: string,
	csvStructure: string,
): Promise<string> {
	const anthropic = new Anthropic({ apiKey });

	const response = await anthropic.messages.create({
		model: "claude-sonnet-4-5",
		max_tokens: 2048,
		messages: [
			{
				role: "user",
				content: `CSV at ${csvPath}:
${csvStructure}

Question: "${question}"

Generate Python code that:
- Reads CSV with pandas
- Answers the question
- Saves charts to /workspace/chart.png if helpful
- Prints findings to stdout

Use pandas, numpy, matplotlib.`,
			},
		],
		tools: [
			{
				name: "generate_python_code",
				description: "Generate Python code for data analysis",
				input_schema: {
					type: "object",
					properties: {
						code: { type: "string", description: "Complete Python code" },
					},
					required: ["code"],
				},
			},
		],
	});

	for (const block of response.content) {
		if (block.type === "tool_use" && block.name === "generate_python_code") {
			return (block.input as { code: string }).code;
		}
	}

	throw new Error("Failed to generate code");
}
```

## 4\. Set up local environment variables

Create a `.dev.vars` file in your project root for local development:

```sh
echo "ANTHROPIC_API_KEY=your_api_key_here\nSANDBOX_TRANSPORT=rpc" > .dev.vars
```

Replace `your_api_key_here` with your actual API key from the [Anthropic Console ↗](https://console.anthropic.com/).

The `SANDBOX_TRANSPORT` is required to use the new file streaming APIs.

Note

The `.dev.vars` file is automatically gitignored and only used during local development with `npm run dev`.

## 5\. Test locally

Download a sample CSV:

```sh
# Create a test CSV
echo "year,rating,title
2020,8.5,Movie A
2021,7.2,Movie B
2022,9.1,Movie C" > test.csv
```

Start the dev server:

```sh
npm run dev
```

Test with curl:

```sh
curl -X POST http://localhost:8787 \
  -F "file=@test.csv" \
  -F "question=What is the average rating by year?"
```

Response:

```json
{
	"success": true,
	"output": "Average ratings by year:\n2020: 8.5\n2021: 7.2\n2022: 9.1",
	"chart": "data:image/png;base64,...",
	"code": "import pandas as pd\nimport matplotlib.pyplot as plt\n..."
}
```

## 6\. Deploy

Deploy your Worker:

```sh
npx wrangler deploy
```

Then set your Anthropic API key as a production secret:

```sh
npx wrangler secret put ANTHROPIC_API_KEY
```

Paste your API key from the [Anthropic Console ↗](https://console.anthropic.com/) when prompted.

Caution

Wait 2-3 minutes after first deployment for container provisioning.

## What you built

An AI data analysis system that:

* Uploads CSV files to sandboxes
* Uses Claude's tool calling to generate analysis code
* Executes Python with pandas and matplotlib
* Returns text output and visualizations

## Next steps

* [Code Interpreter API](https://developers.cloudflare.com/sandbox/api/interpreter/) \- Use the built-in code interpreter
* [File operations](https://developers.cloudflare.com/sandbox/guides/manage-files/) \- Advanced file handling
* [Streaming output](https://developers.cloudflare.com/sandbox/guides/streaming-output/) \- Real-time progress updates

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/analyze-data-with-ai/#page","headline":"Analyze data with AI · Cloudflare Sandbox SDK docs","description":"Upload CSV files, generate analysis code with Claude, and return visualizations.","url":"https://developers.cloudflare.com/sandbox/tutorials/analyze-data-with-ai/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-13","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Build a testing pipeline that clones Git repositories, installs dependencies, runs tests, and reports results.
title: Automated testing pipeline
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Automated testing pipeline

Last updated Jul 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/automated-testing-pipeline/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Build a testing pipeline that clones Git repositories, installs dependencies, runs tests, and reports results.

**Time to complete**: 25 minutes

## Prerequisites

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

Node.js version manager

Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), discussed later in this guide, requires a Node version of `16.17.0` or later.

You'll also need a GitHub repository with tests (public or private with access token).

## 1\. Create your project

npmyarnpnpm

```
npm create cloudflare@latest -- test-pipeline --template=cloudflare/sandbox-sdk/examples/minimal
```

```
yarn create cloudflare test-pipeline --template=cloudflare/sandbox-sdk/examples/minimal
```

```
pnpm create cloudflare@latest test-pipeline --template=cloudflare/sandbox-sdk/examples/minimal
```

```sh
cd test-pipeline
```

## 2\. Build the pipeline

Replace `src/index.ts`:

```typescript
import { getSandbox, proxyToSandbox, parseSSEStream, type Sandbox, type ExecEvent } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

interface Env {
	Sandbox: DurableObjectNamespace<Sandbox>;
	GITHUB_TOKEN?: string;
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const proxyResponse = await proxyToSandbox(request, env);
		if (proxyResponse) return proxyResponse;

		if (request.method !== 'POST') {
			return new Response('POST { "repoUrl": "https://github.com/owner/repo", "branch": "main" }');
		}

		try {
			const { repoUrl, branch } = await request.json();

			if (!repoUrl) {
				return Response.json({ error: 'repoUrl required' }, { status: 400 });
			}

			const sandbox = getSandbox(env.Sandbox, `test-${Date.now()}`);

			try {
				// Clone repository
				console.log('Cloning repository...');
				let cloneUrl = repoUrl;
				
				if (env.GITHUB_TOKEN && cloneUrl.includes('github.com')) {
					cloneUrl = cloneUrl.replace('https://', `https://${env.GITHUB_TOKEN}@`);
				}

				await sandbox.gitCheckout(cloneUrl, {
					...(branch && { branch }),
					depth: 1,
					targetDir: 'repo'
				});
				console.log('Repository cloned');

				// Detect project type
				const projectType = await detectProjectType(sandbox);
				console.log(`Detected ${projectType} project`);

				// Install dependencies
				const installCmd = getInstallCommand(projectType);
				if (installCmd) {
					console.log('Installing dependencies...');
					const installStream = await sandbox.execStream(`cd /workspace/repo && ${installCmd}`);
					
					let installExitCode = 0;
					for await (const event of parseSSEStream<ExecEvent>(installStream)) {
						if (event.type === 'stdout' || event.type === 'stderr') {
							console.log(event.data);
						} else if (event.type === 'complete') {
							installExitCode = event.exitCode;
						}
					}
					
					if (installExitCode !== 0) {
						return Response.json({
							success: false,
							error: 'Install failed',
							exitCode: installExitCode
						});
					}
					console.log('Dependencies installed');
				}

				// Run tests
				console.log('Running tests...');
				const testCmd = getTestCommand(projectType);
				const testStream = await sandbox.execStream(`cd /workspace/repo && ${testCmd}`);
				
				let testExitCode = 0;
				for await (const event of parseSSEStream<ExecEvent>(testStream)) {
					if (event.type === 'stdout' || event.type === 'stderr') {
						console.log(event.data);
					} else if (event.type === 'complete') {
						testExitCode = event.exitCode;
					}
				}
				console.log(`Tests completed with exit code ${testExitCode}`);

				return Response.json({
					success: testExitCode === 0,
					exitCode: testExitCode,
					projectType,
					message: testExitCode === 0 ? 'All tests passed' : 'Tests failed'
				});

			} finally {
				await sandbox.destroy();
			}

		} catch (error: any) {
			return Response.json({ error: error.message }, { status: 500 });
		}
	},
};

async function detectProjectType(sandbox: any): Promise<string> {
	try {
		await sandbox.readFile('/workspace/repo/package.json');
		return 'nodejs';
	} catch {}

	try {
		await sandbox.readFile('/workspace/repo/requirements.txt');
		return 'python';
	} catch {}

	try {
		await sandbox.readFile('/workspace/repo/go.mod');
		return 'go';
	} catch {}

	return 'unknown';
}

function getInstallCommand(projectType: string): string {
	switch (projectType) {
		case 'nodejs': return 'npm install';
		case 'python': return 'pip install -r requirements.txt || pip install -e .';
		case 'go': return 'go mod download';
		default: return '';
	}
}

function getTestCommand(projectType: string): string {
	switch (projectType) {
		case 'nodejs': return 'npm test';
		case 'python': return 'python -m pytest || python -m unittest discover';
		case 'go': return 'go test ./...';
		default: return 'echo "Unknown project type"';
	}
}
```

## 3\. Test locally

Start the dev server:

```sh
npm run dev
```

Test with a repository:

```sh
curl -X POST http://localhost:8787 \
  -H "Content-Type: application/json" \
  -d '{
    "repoUrl": "https://github.com/cloudflare/sandbox-sdk"
  }'
```

You will see progress logs in the wrangler console, and receive a JSON response:

```json
{
  "success": true,
  "exitCode": 0,
  "projectType": "nodejs",
  "message": "All tests passed"
}
```

## 4\. Deploy

```sh
npx wrangler deploy
```

For private repositories, set your GitHub token:

```sh
npx wrangler secret put GITHUB_TOKEN
```

## What you built

An automated testing pipeline that:

* Clones Git repositories
* Detects project type (Node.js, Python, Go)
* Installs dependencies automatically
* Runs tests and reports results

## Next steps

* [Streaming output](https://developers.cloudflare.com/sandbox/guides/streaming-output/) \- Add real-time test output
* [Background processes](https://developers.cloudflare.com/sandbox/guides/background-processes/) \- Handle long-running tests
* [Sessions API](https://developers.cloudflare.com/sandbox/api/sessions/) \- Cache dependencies between runs

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/automated-testing-pipeline/#page","headline":"Automated testing pipeline · Cloudflare Sandbox SDK docs","description":"Build a testing pipeline that clones Git repositories, installs dependencies, runs tests, and reports results.","url":"https://developers.cloudflare.com/sandbox/tutorials/automated-testing-pipeline/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-21","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Use Claude Code to implement a task in your GitHub repository.
title: Run Claude Code on a Sandbox
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Run Claude Code on a Sandbox

Last updated May 5, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/claude-code/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Build a Worker that takes a repository URL and a task description and uses Sandbox SDK to run Claude Code to implement your task.

**Time to complete:** 5 minutes

## Prerequisites

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

Node.js version manager

Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), discussed later in this guide, requires a Node version of `16.17.0` or later.

You'll also need:

* An [Anthropic API key ↗](https://console.anthropic.com/) for Claude Code
* [Docker ↗](https://www.docker.com/) running locally

## 1\. Create your project

Create a new Sandbox SDK project:

npmyarnpnpm

```
npm create cloudflare@latest -- claude-code-sandbox --template=cloudflare/sandbox-sdk/examples/claude-code
```

```
yarn create cloudflare claude-code-sandbox --template=cloudflare/sandbox-sdk/examples/claude-code
```

```
pnpm create cloudflare@latest claude-code-sandbox --template=cloudflare/sandbox-sdk/examples/claude-code
```

```sh
cd claude-code-sandbox
```

## 2\. Set up local environment variables

Create a `.dev.vars` file in your project root for local development:

```sh
echo "ANTHROPIC_API_KEY=your_api_key_here" > .dev.vars
```

Replace `your_api_key_here` with your actual API key from the [Anthropic Console ↗](https://console.anthropic.com/).

Note

The `.dev.vars` file is automatically gitignored and only used during local development with `npm run dev`.

## 3\. Test locally

Start the development server:

```sh
npm run dev
```

Note

First run builds the Docker container (2-3 minutes). Subsequent runs are much faster.

Test with curl:

```sh
curl -X POST http://localhost:8787/ \
  -d '{
    "repo": "https://github.com/cloudflare/agents",
    "task": "remove the emojis from the readme"
  }'
```

Response:

```json
{
	"logs": "Done! I've removed the brain emoji from the README title. The heading now reads \"# Cloudflare Agents\" instead of \"# 🧠 Cloudflare Agents\".",
	"diff": "diff --git a/README.md b/README.md\nindex 9296ac9..027c218 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-# 🧠 Cloudflare Agents\n+# Cloudflare Agents\n \n ![npm install agents](assets/npm-install-agents.svg)\n "
}
```

## 4\. Deploy

Deploy your Worker:

```sh
npx wrangler deploy
```

Then set your Anthropic API key as a production secret:

```sh
npx wrangler secret put ANTHROPIC_API_KEY
```

Paste your API key from the [Anthropic Console ↗](https://console.anthropic.com/) when prompted.

Caution

After first deployment, wait 2-3 minutes for container provisioning. Check status with `npx wrangler containers list`.

## What you built

You created an API that:

* Accepts a repository URL and natural language task descriptions
* Creates a Sandbox and clones the repository into it
* Kicks off Claude Code to implement the given task
* Returns Claude's output and changes

## Next steps

* [Analyze data with AI](https://developers.cloudflare.com/sandbox/tutorials/analyze-data-with-ai/) \- Add pandas and matplotlib for data analysis
* [Code Interpreter API](https://developers.cloudflare.com/sandbox/api/interpreter/) \- Use the built-in code interpreter instead of exec
* [Streaming output](https://developers.cloudflare.com/sandbox/guides/streaming-output/) \- Show real-time execution progress
* [API reference](https://developers.cloudflare.com/sandbox/api/) \- Explore all available methods

## Related resources

* [Anthropic Claude documentation ↗](https://docs.anthropic.com/)
* [Workers AI](https://developers.cloudflare.com/workers-ai/) \- Use Cloudflare's built-in models

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/claude-code/#page","headline":"Run Claude Code on a Sandbox · Cloudflare Sandbox SDK docs","description":"Use Claude Code to implement a task in your GitHub repository.","url":"https://developers.cloudflare.com/sandbox/tutorials/claude-code/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-05","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Run Claude Managed Agents on self-managed Cloudflare environments.
title: Set up Claude Managed Agents
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Set up Claude Managed Agents

Last updated May 19, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/claude-managed-agents/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Cloudflare provides a self-managed environment for [Claude Managed Agents ↗](https://platform.claude.com/docs/en/managed-agents/overview). The agent loop runs on the Anthropic platform, while Cloudflare provides the runtime — sandboxes, egress control, browser access, email, and custom tools — that the agent's actions execute in.

This integration ships as an open-source deployment template. Fork the repo, deploy it to your Cloudflare account, and customize it as needed.

[Get Started](https://github.com/cloudflare/claude-managed-agents) 

## What you get

Deploy a Workers-based control plane that gives you:

* **Two sandbox backends** — Each agent can run on a full MicroVM ([Containers](https://developers.cloudflare.com/containers/)) or a lightweight isolate ([Dynamic Workers](https://developers.cloudflare.com/dynamic-workers/)). MicroVMs give the agent a full Linux environment with bash and arbitrary processes. Isolates cold-start in milliseconds and costs a fraction of a container session.
* **Private service connectivity** — Connect agents to private internal services over [Workers VPC](https://developers.cloudflare.com/workers-vpc/) and [Mesh](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-mesh/) without exposing them to the public internet.
* **Egress control** — Run all agent traffic through customizable proxies. Inject credentials into outbound requests without the agent ever seeing them, restrict access to specific domains, or write arbitrary proxy middleware.
* **Agent Email** — Give each agent session its own email address for sending and receiving messages with [Cloudflare Email Service](https://developers.cloudflare.com/email-service).
* **Browser Run tools** — Give agents headless browsers powered by [Browser Run](https://developers.cloudflare.com/browser-run/) for web fetches, screenshots, and CDP control. Session recordings provide an audit trail of every browser action.
* **Image generation** — Generate images with [Workers AI](https://developers.cloudflare.com/workers-ai/).
* **Custom tools** — Extend agents with your own tools by adding a function definition to a single file. Tools run in the Workers runtime with access to all your bindings. No additional infrastructure required.
* **Dashboard** — A built-in UI for managing agents, viewing sessions, inspecting logs, and SSH-ing into running MicroVM sandboxes.

## How it works

When a Claude agent starts a session, Anthropic sends a webhook to the Workers-based control plane running in your Cloudflare account. The control plane gives each session its own sandbox, routes outbound traffic through a per-session egress policy, and persists state across session sleeps.

Anthropic describes this as decoupling the brain from the hands — the agent loop runs on Anthropic (the brain), but the infrastructure for running and executing code (the hands) runs on Cloudflare.

## When to use this

Use a self-managed Cloudflare environment when you need:

* Control over the sandbox infrastructure your agents run in
* Secure connections to private internal services
* Custom egress policies for credential injection and domain restrictions
* Custom tools that use Cloudflare bindings (R2, D1, KV, Vectorize, and others)
* The ability to choose between MicroVM and isolate backends per agent

## Get started

Follow the [onboarding guide ↗](https://github.com/cloudflare/claude-managed-agents#onboarding-guide) in the repository to deploy the control plane to your account. The guide walks through creating an Anthropic environment, setting secrets, provisioning storage, deploying the Worker, and configuring webhooks.

Note

You need a Workers Paid plan or Enterprise account. [Containers](https://developers.cloudflare.com/containers/) (used by MicroVM sandboxes) and Worker Loader bindings (used by isolate code execution and egress proxies) require the paid plan.

## Key documentation

The repository includes detailed documentation on each capability:

| Topic                                                                                                                                      | What it covers                                                                                                                                                   |
| ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Connecting to private services ↗](https://github.com/cloudflare/claude-managed-agents/blob/main/docs/connecting-to-private-services.md)   | Reach services in other clouds, on-prem, or on your laptop with Workers VPC bindings                                                                             |
| [Applying egress policies ↗](https://github.com/cloudflare/claude-managed-agents/blob/main/docs/applying-egress-policies.md)               | Inject credentials and lock down agent sessions. Set up allow/deny lists, header injection, custom Worker proxies, and VPC routing                               |
| [Isolate vs VM-based sandboxes ↗](https://github.com/cloudflare/claude-managed-agents/blob/main/docs/isolate-vs-vm-sandboxes.md)           | Pick the best agent execution environment                                                                                                                        |
| [Agent email ↗](https://github.com/cloudflare/claude-managed-agents/blob/main/docs/agent-email.md)                                         | Give agents their own email addresses and sending abilities                                                                                                      |
| [Browser rendering tools ↗](https://github.com/cloudflare/claude-managed-agents/blob/main/docs/browser-rendering-tools.md)                 | Observable agent browser interactions with Browser Run                                                                                                           |
| [Adding custom tools ↗](https://github.com/cloudflare/claude-managed-agents/blob/main/docs/adding-custom-tools.md)                         | New tools are declared in a single file — [src/tools/custom-tools.ts ↗](https://github.com/cloudflare/claude-managed-agents/blob/main/src/tools/custom-tools.ts) |
| [Customizing sandboxes ↗](https://github.com/cloudflare/claude-managed-agents/blob/main/docs/customizing-sandboxes.md)                     | Change Dockerfile and instance\_type knobs for the MicroVM backend                                                                                               |
| [Snapshots and state persistence ↗](https://github.com/cloudflare/claude-managed-agents/blob/main/docs/snapshots-and-state-persistence.md) | State persistence across both sandbox types                                                                                                                      |
| [Architecture ↗](https://github.com/cloudflare/claude-managed-agents/blob/main/docs/architecture.md)                                       | Request lifecycle from webhook ingress through dispatch to either sandbox backend, and every Worker binding the control plane uses                               |
| [Securing access ↗](https://github.com/cloudflare/claude-managed-agents/blob/main/docs/securing-access.md)                                 | Secure access to the CMA control plane                                                                                                                           |

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/claude-managed-agents/#page","headline":"Set up Claude Managed Agents · Cloudflare Sandbox SDK docs","description":"Run Claude Managed Agents on self-managed Cloudflare environments.","url":"https://developers.cloudflare.com/sandbox/tutorials/claude-managed-agents/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-19","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["AI"]}
```

---

---
description: Clone repositories, analyze code with Claude, and post review comments to GitHub PRs.
title: Build a code review bot
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Build a code review bot

Last updated May 5, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/code-review-bot/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Build a GitHub bot that responds to pull requests, clones the repository in a sandbox, uses Claude to analyze code changes, and posts review comments.

**Time to complete**: 30 minutes

## Prerequisites

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

Node.js version manager

Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), discussed later in this guide, requires a Node version of `16.17.0` or later.

You'll also need:

* A [GitHub account ↗](https://github.com/) and [fine-grained personal access token ↗](https://github.com/settings/personal-access-tokens/new) with the following permissions:  
  * **Repository access**: Select the specific repository you want to test with
  * **Permissions** \> **Repository permissions**:  
    * **Metadata**: Read-only (required)
    * **Contents**: Read-only (required to clone the repository)
    * **Pull requests**: Read and write (required to post review comments)
* An [Anthropic API key ↗](https://console.anthropic.com/) for Claude
* A GitHub repository for testing

## 1\. Create your project

npmyarnpnpm

```
npm create cloudflare@latest -- code-review-bot --template=cloudflare/sandbox-sdk/examples/minimal
```

```
yarn create cloudflare code-review-bot --template=cloudflare/sandbox-sdk/examples/minimal
```

```
pnpm create cloudflare@latest code-review-bot --template=cloudflare/sandbox-sdk/examples/minimal
```

```sh
cd code-review-bot
```

## 2\. Install dependencies

npmyarnpnpmbun

```
npm i @anthropic-ai/sdk @octokit/rest
```

```
yarn add @anthropic-ai/sdk @octokit/rest
```

```
pnpm add @anthropic-ai/sdk @octokit/rest
```

```
bun add @anthropic-ai/sdk @octokit/rest
```

## 3\. Build the webhook handler

Replace `src/index.ts`:

```typescript
import { getSandbox, proxyToSandbox, type Sandbox } from "@cloudflare/sandbox";
import { Octokit } from "@octokit/rest";
import Anthropic from "@anthropic-ai/sdk";

export { Sandbox } from "@cloudflare/sandbox";

interface Env {
	Sandbox: DurableObjectNamespace<Sandbox>;
	GITHUB_TOKEN: string;
	ANTHROPIC_API_KEY: string;
	WEBHOOK_SECRET: string;
}

export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const proxyResponse = await proxyToSandbox(request, env);
		if (proxyResponse) return proxyResponse;

		const url = new URL(request.url);

		if (url.pathname === "/webhook" && request.method === "POST") {
			const signature = request.headers.get("x-hub-signature-256");
			const contentType = request.headers.get("content-type") || "";
			const body = await request.text();

			// Verify webhook signature
			if (
				!signature ||
				!(await verifySignature(body, signature, env.WEBHOOK_SECRET))
			) {
				return Response.json({ error: "Invalid signature" }, { status: 401 });
			}

			const event = request.headers.get("x-github-event");

			// Parse payload (GitHub can send as JSON or form-encoded)
			let payload;
			if (contentType.includes("application/json")) {
				payload = JSON.parse(body);
			} else {
				// Handle form-encoded payload
				const params = new URLSearchParams(body);
				payload = JSON.parse(params.get("payload") || "{}");
			}

			// Handle opened and reopened PRs
			if (
				event === "pull_request" &&
				(payload.action === "opened" || payload.action === "reopened")
			) {
				console.log(`Starting review for PR #${payload.pull_request.number}`);
				// Use waitUntil to ensure the review completes even after response is sent
				ctx.waitUntil(
					reviewPullRequest(payload, env).catch(console.error),
				);
				return Response.json({ message: "Review started" });
			}

			return Response.json({ message: "Event ignored" });
		}

		return new Response(
			"Code Review Bot\n\nConfigure GitHub webhook to POST /webhook",
		);
	},
};

async function verifySignature(
	payload: string,
	signature: string,
	secret: string,
): Promise<boolean> {
	const encoder = new TextEncoder();
	const key = await crypto.subtle.importKey(
		"raw",
		encoder.encode(secret),
		{ name: "HMAC", hash: "SHA-256" },
		false,
		["sign"],
	);

	const signatureBytes = await crypto.subtle.sign(
		"HMAC",
		key,
		encoder.encode(payload),
	);
	const expected =
		"sha256=" +
		Array.from(new Uint8Array(signatureBytes))
			.map((b) => b.toString(16).padStart(2, "0"))
			.join("");

	return signature === expected;
}

async function reviewPullRequest(payload: any, env: Env): Promise<void> {
	const pr = payload.pull_request;
	const repo = payload.repository;
	const octokit = new Octokit({ auth: env.GITHUB_TOKEN });
	const sandbox = getSandbox(env.Sandbox, `review-${pr.number}`);

	try {
		// Post initial comment
		console.log("Posting initial comment...");
		await octokit.issues.createComment({
			owner: repo.owner.login,
			repo: repo.name,
			issue_number: pr.number,
			body: "Code review in progress...",
		});
		// Clone repository
		console.log("Cloning repository...");
		const cloneUrl = `https://${env.GITHUB_TOKEN}@github.com/${repo.owner.login}/${repo.name}.git`;
		await sandbox.exec(
			`git clone --depth=1 --branch=${pr.head.ref} ${cloneUrl} /workspace/repo`,
		);

		// Get changed files
		console.log("Fetching changed files...");
		const comparison = await octokit.repos.compareCommits({
			owner: repo.owner.login,
			repo: repo.name,
			base: pr.base.sha,
			head: pr.head.sha,
		});

		const files = [];
		for (const file of (comparison.data.files || []).slice(0, 5)) {
			if (file.status !== "removed") {
				const content = await sandbox.readFile(
					`/workspace/repo/${file.filename}`,
				);
				files.push({
					path: file.filename,
					patch: file.patch || "",
					content: content.content,
				});
			}
		}

		// Generate review with Claude
		console.log(`Analyzing ${files.length} files with Claude...`);
		const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
		const response = await anthropic.messages.create({
			model: "claude-sonnet-4-5",
			max_tokens: 2048,
			messages: [
				{
					role: "user",
					content: `Review this PR:

Title: ${pr.title}

Changed files:
${files.map((f) => `File: ${f.path}\nDiff:\n${f.patch}\n\nContent:\n${f.content.substring(0, 1000)}`).join("\n\n")}

Provide a brief code review focusing on bugs, security, and best practices.`,
				},
			],
		});

		const review =
			response.content[0]?.type === "text"
				? response.content[0].text
				: "No review generated";

		// Post review comment
		console.log("Posting review...");
		await octokit.issues.createComment({
			owner: repo.owner.login,
			repo: repo.name,
			issue_number: pr.number,
			body: `## Code Review\n\n${review}\n\n---\n*Generated by Claude*`,
		});
		console.log("Review complete!");
	} catch (error: any) {
		console.error("Review failed:", error);
		await octokit.issues.createComment({
			owner: repo.owner.login,
			repo: repo.name,
			issue_number: pr.number,
			body: `Review failed: ${error.message}`,
		});
	} finally {
		await sandbox.destroy();
	}
}
```

## 4\. Set up local environment variables

Create a `.dev.vars` file in your project root for local development:

```sh
cat > .dev.vars << EOF
GITHUB_TOKEN=your_github_token_here
ANTHROPIC_API_KEY=your_anthropic_key_here
WEBHOOK_SECRET=your_webhook_secret_here
EOF
```

Replace the placeholder values with:

* `GITHUB_TOKEN`: Your GitHub personal access token with repo permissions
* `ANTHROPIC_API_KEY`: Your API key from the [Anthropic Console ↗](https://console.anthropic.com/)
* `WEBHOOK_SECRET`: A random string (for example: `openssl rand -hex 32`)

Note

The `.dev.vars` file is automatically gitignored and only used during local development with `npm run dev`.

## 5\. Expose local server with Cloudflare Tunnel

To test with real GitHub webhooks locally, use [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/) to expose your local development server.

Start the development server:

```sh
npm run dev
```

In a separate terminal, create a tunnel to your local server:

```sh
cloudflared tunnel --url http://localhost:8787
```

This will output a public URL (for example, `https://example.trycloudflare.com`). Copy this URL for the next step.

Note

If you do not have `cloudflared` installed, refer to [Downloads](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/).

## 6\. Configure GitHub webhook for local testing

Important

Configure this webhook on a **specific GitHub repository** where you will create test pull requests. The bot will only review PRs in repositories where the webhook is configured.

1. Navigate to your test repository on GitHub
2. Go to **Settings** \> **Webhooks** \> **Add webhook**
3. Set **Payload URL**: Your Cloudflare Tunnel URL from Step 5 with `/webhook` appended (for example, `https://example.trycloudflare.com/webhook`)
4. Set **Content type**: `application/json`
5. Set **Secret**: Same value you used for `WEBHOOK_SECRET` in your `.dev.vars` file
6. Select **Let me select individual events** → Check **Pull requests**
7. Click **Add webhook**

## 7\. Test locally with a pull request

Create a test PR:

```sh
git checkout -b test-review
echo "console.log('test');" > test.js
git add test.js
git commit -m "Add test file"
git push origin test-review
```

Open the PR on GitHub. The bot should post a review comment within a few seconds.

## 8\. Deploy to production

Deploy your Worker:

```sh
npx wrangler deploy
```

Then set your production secrets:

```sh
# GitHub token (needs repo permissions)
npx wrangler secret put GITHUB_TOKEN

# Anthropic API key
npx wrangler secret put ANTHROPIC_API_KEY

# Webhook secret (use the same value from .dev.vars)
npx wrangler secret put WEBHOOK_SECRET
```

## 9\. Update webhook for production

1. Go to your repository **Settings** \> **Webhooks**
2. Click on your existing webhook
3. Update **Payload URL** to your deployed Worker URL: `https://code-review-bot.YOUR_SUBDOMAIN.workers.dev/webhook`
4. Click **Update webhook**

Your bot is now running in production and will review all new pull requests automatically.

## What you built

A GitHub code review bot that:

* Receives webhook events from GitHub
* Clones repositories in isolated sandboxes
* Uses Claude to analyze code changes
* Posts review comments automatically

## Next steps

* [Git operations](https://developers.cloudflare.com/sandbox/api/files/#gitcheckout) \- Advanced repository handling
* [Sessions API](https://developers.cloudflare.com/sandbox/api/sessions/) \- Manage long-running sandbox operations
* [GitHub Apps ↗](https://docs.github.com/en/apps) \- Build a proper GitHub App

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/code-review-bot/#page","headline":"Build a code review bot · Cloudflare Sandbox SDK docs","description":"Clone repositories, analyze code with Claude, and post review comments to GitHub PRs.","url":"https://developers.cloudflare.com/sandbox/tutorials/code-review-bot/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-05","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Deploy Cursor self-hosted machines that run each assigned session in an isolated Cloudflare container.
title: Run Cursor Cloud Agents on Cloudflare via self-hosted machines
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Run Cursor Cloud Agents on Cloudflare via self-hosted machines

Last updated Sep 4, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/cursor-cloud-agents/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Run Cursor Cloud Agents on Cloudflare via [self-hosted machines ↗](https://cursor.com/docs/cloud-agent/self-hosted). Each Cursor session assigned to the deployment runs in an isolated container backed by Cloudflare Containers.

Cursor hosts the agent loop, inference, and planning. Cloudflare runs commands, file edits, repository operations, and other tools inside infrastructure that you control.

## Prerequisites

You need:

* A Cursor Enterprise plan with self-hosted machines enabled
* A Cursor team service-account API key with agent scope
* A Cloudflare Workers Paid account with access to Containers and R2
* [Node.js 20 ↗](https://nodejs.org/) or later
* A running [Docker ↗](https://www.docker.com/) daemon for deployment and local development

### Configure a Cursor team pool

To create a team pool, set `CURSOR_API_KEY` in your shell. Then, start a local worker with the Cursor Agent CLI:

```sh
CURSOR_API_KEY="$CURSOR_API_KEY" agent worker --pool cloudflare-test start
```

The command registers the pool and temporarily connects your local machine as a worker. After the pool appears in Cursor, stop the worker with `Ctrl+C` and run `unset CURSOR_API_KEY`. Record the pool name for `CURSOR_POOL`. Keep the local worker stopped while testing the Cloudflare deployment so it does not claim the agent request.

For more information, refer to [Cursor team pools ↗](https://cursor.com/docs/cloud-agent/self-hosted-guides/pool).

## Deploy the template

The template deploys a Worker, a Durable Object namespace, a container application, an R2 bucket binding, and a cron trigger.

1. Clone the template and install its dependencies:  
```sh  
git clone https://github.com/anysphere/cloudflare-workers.git  
cd cloudflare-workers  
npm install  
```
2. Log in to your Cloudflare account:  
```sh  
npx wrangler login  
```
3. Create the R2 bucket for optional repository snapshots:  
```sh  
npx wrangler r2 bucket create cursor-pool-worker-snapshots  
```  
To use another bucket name, update `bucket_name` in `wrangler.jsonc`.
4. Store the required Cursor service-account key as a Worker secret:  
```sh  
npx wrangler secret put CURSOR_API_KEY  
```  
Enter a team service-account key with agent scope. Personal API keys do not work with pool workers.
5. To access private repositories, store your Git credentials as Worker secrets:  
```sh  
npx wrangler secret put GIT_USERNAME  
npx wrangler secret put GIT_TOKEN  
```  
For GitHub, set `GIT_USERNAME` to `x-access-token`. Set `GIT_TOKEN` to a token with access to the repositories that the agents use.
6. In `wrangler.jsonc`, set `vars.CURSOR_POOL` to the Cursor team pool name:  
```jsonc  
{  
  "vars": {  
    "CURSOR_POOL": "default"  
  }  
}  
```
7. Set `containers[].max_instances` to the maximum number of concurrent requests that the deployment must support.
8. Deploy the Worker and container:  
```sh  
npx wrangler deploy  
```  
Wrangler builds the image and deploys the Worker, Durable Object, container application, and cron trigger.  
A new container image rollout stops running containers. Wait for active Cursor sessions to finish before you deploy an update.

## Run a repository-bound agent

Repository-bound agents route work by Git remote. The team pool name provides an additional routing constraint.

1. Go to [Cursor Cloud Agents ↗](https://cursor.com/agents).
2. Start an agent and select a repository.
3. Select **Self-hosted**, then select the name configured in `CURSOR_POOL`.
4. Wait for Cursor to assign the session to the deployment. The Worker then starts a container for the session. The initial scheduled controller run can take up to five minutes to begin.

The request provides the repository URL. The container restores or clones that repository into `$HOME/workspaces/repo-0`. It then starts the Cursor worker:

```sh
agent worker --worker-dir "$HOME/workspaces/repo-0" --pool "$CURSOR_POOL" start --verbose
```

The Cursor worker derives its repository label from the Git remote. Do not configure `repo=` labels manually.

## Run an any-repository agent

Any-repository agents route work by team pool name. They start with an empty working directory and no Git remote.

1. Go to [Cursor Cloud Agents ↗](https://cursor.com/agents).
2. Start an agent and select the **Any repo** group.
3. Select the team pool name configured in `CURSOR_POOL`.

The container creates `$HOME/workspaces/repo-0` without a Git remote. The agent or a project hook can clone a repository during the session.

## Configure repository snapshots

Repository snapshots are an optional cache for repository-bound agents. A snapshot stores the post-clone working tree in R2\. An any-repository agent does not use this cache.

1. Store a secret that protects the snapshot routes:  
```sh  
npx wrangler secret put SNAPSHOT_AUTH_TOKEN  
```
2. In `wrangler.jsonc`, set `vars.WORKER_PUBLIC_URL` to the deployed Worker URL:  
```jsonc  
{  
  "vars": {  
    "CURSOR_POOL": "default",  
    "WORKER_PUBLIC_URL": "https://cursor-pool-workers.<ACCOUNT_SUBDOMAIN>.workers.dev"  
  }  
}  
```
3. Deploy the updated configuration:  
```sh  
npx wrangler deploy  
```

A cache miss performs a normal Git clone. It does not prevent the agent from starting.

## How the template works

The template manages one container for each assigned Cursor session:

* **List:** A cron trigger runs every five minutes. The Worker lists sessions waiting for `CURSOR_POOL`.
* **Stream:** The Worker holds Cursor's server-sent events stream open until the next scheduled run.
* **Assign:** The Worker accepts a waiting session with a unique worker ID. Cursor assigns that session exclusively to the Worker, which prevents duplicate processing.
* **Start:** A Durable Object starts one container with the session environment and repository information.
* **Stop:** The container exits after the configured idle timeout. The Durable Object also enforces a maximum run lifetime.

The container opens an outbound connection to Cursor. The container does not require an inbound port or public IP address.

The Worker only exposes its health and optional snapshot routes. Cursor remains responsible for the agent loop and session orchestration.

## Monitor the deployment

To stream controller and container logs, run:

```sh
npx wrangler tail
```

To list container instances, run:

```sh
npx wrangler containers list
```

To test one scheduled controller run during local development, start `wrangler` with scheduled-event testing:

```sh
npx wrangler dev --test-scheduled
```

In another terminal, invoke the scheduled route:

```sh
curl "http://localhost:8787/cdn-cgi/local/scheduled?cron=*/5+*+*+*+*"
```

If you change the cron interval, update both `triggers.crons` in `wrangler.jsonc` and `CONTROLLER_RUN_BUDGET_MS` in `src/config.ts`.

## Troubleshooting

| Symptom                                                                                                                                     | Cause                                                                                                                                                                                                     | Resolution                                                                                                                                                                                                                                                     |
| ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No sessions are assigned                                                                                                                    | The cron does not run, the key is missing, or the team pool name does not match                                                                                                                           | Run npx wrangler tail. Check controller runs, 401 responses, and the configured team pool name.                                                                                                                                                                |
| The controller returns 401                                                                                                                  | The key is personal or lacks agent scope                                                                                                                                                                  | Replace CURSOR\_API\_KEY with a team service-account key that has agent scope.                                                                                                                                                                                 |
| The team pool is absent for a repo                                                                                                          | The worker started without repository labels                                                                                                                                                              | Select **Any repo**, or start a repository-bound agent with a configured Git remote.                                                                                                                                                                           |
| The session is assigned but does not start                                                                                                  | The container cannot start, clone the repository, or authenticate                                                                                                                                         | Run npx wrangler containers list and inspect npx wrangler tail. Check capacity and Git secrets.                                                                                                                                                                |
| The container exits with Error: Container exited with unexpected exit code: 1 and an earlier log reports cursor-agent CLI not found on PATH | Cloudflare WARP or another TLS-inspecting proxy may have prevented Docker from downloading the Cursor CLI. An unguarded shell pipeline can hide the installation failure and produce an incomplete image. | Run npx wrangler tail and inspect the preceding container logs. If the Cursor CLI is missing, disconnect WARP, clear the Docker build cache, and run npx wrangler deploy again. Alternatively, configure Docker to trust your organization's root certificate. |
| The first repository start is slow                                                                                                          | The snapshot cache is empty or not configured                                                                                                                                                             | Configure both WORKER\_PUBLIC\_URL and SNAPSHOT\_AUTH\_TOKEN, or allow a cold Git clone.                                                                                                                                                                       |

## Related resources

* [Cursor Cloudflare Workers template ↗](https://github.com/anysphere/cloudflare-workers)
* [Cursor self-hosted machines overview ↗](https://cursor.com/docs/cloud-agent/self-hosted)
* [Cursor team pools ↗](https://cursor.com/docs/cloud-agent/self-hosted-guides/pool)
* [Cloudflare Containers](https://developers.cloudflare.com/containers/)
* [R2](https://developers.cloudflare.com/r2/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/cursor-cloud-agents/#page","headline":"Run Cursor Cloud Agents on Cloudflare via self-hosted machines · Cloudflare Sandbox SDK docs","description":"Deploy Cursor self-hosted machines that run each assigned session in an isolated Cloudflare container.","url":"https://developers.cloudflare.com/sandbox/tutorials/cursor-cloud-agents/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-09-04","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Deploy a Devin Outpost that runs each Devin session in an isolated Cloudflare container.
title: Run Devin Outposts on Cloudflare
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Run Devin Outposts on Cloudflare

Last updated Jul 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/devin-outposts/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Run [Devin agents ↗](https://docs.devin.ai/onboard-devin/outposts) on Cloudflare. Each Devin session runs in its own isolated sandbox backed by Cloudflare Containers.

## Prerequisites

You need:

* A Devin Enterprise organization with permission to manage outposts and service users
* A Cloudflare account with access to Workers, Containers, and R2
* For manual deployment, [Node.js 24 ↗](https://nodejs.org/) and a running [Docker ↗](https://www.docker.com/) daemon

### Get your Devin credentials

1. Open your Devin organization outpost settings. Replace both instances of `my-org` in this URL with your organization slug:  
```txt  
https://my-org.devinenterprise.com/org/my-org/settings/enterprise-environment?tab=outposts  
```
2. Create or select an outpost. Copy its outpost ID for `DEVIN_OUTPOST_ID` and its token for `DEVIN_API_TOKEN`.

For more information about outposts, refer to the [Devin Outposts overview ↗](https://docs.devin.ai/onboard-devin/outposts).

## Deploy with one click

The fastest setup uses the **Deploy to Cloudflare** button. The deployment flow prompts for your Devin Outpost ID and API token. It also creates and configures the required containers.

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/sandbox-sdk/tree/main/devin)

Enter these credentials when prompted:

| Variable           | Value                                                                  |
| ------------------ | ---------------------------------------------------------------------- |
| DEVIN\_OUTPOST\_ID | Your Devin Outpost ID                                                  |
| DEVIN\_API\_TOKEN  | A Devin service-user token with the **Run outpost workers** permission |

After the deployment finishes, your outpost is ready to run Devin sessions on Cloudflare by selecting it from the Virtual environment menu.

## Customize and deploy manually

Use the following procedure when you need to add dependencies, tools, or environment variables to the template.

1. Create a project from the Devin Outpost template:  
npmyarnpnpm  
```  
npm create cloudflare@latest -- cloudflare-devin-outpost --template=cloudflare/sandbox-sdk/devin  
```  
```  
yarn create cloudflare cloudflare-devin-outpost --template=cloudflare/sandbox-sdk/devin  
```  
```  
pnpm create cloudflare@latest cloudflare-devin-outpost --template=cloudflare/sandbox-sdk/devin  
```
2. Go to the project directory and log in to your Cloudflare account:  
```sh  
cd cloudflare-devin-outpost  
npx wrangler login  
```
3. Create the R2 bucket for session checkpoints:  
```sh  
npx wrangler r2 bucket create devin-outpost-state  
```  
To use another bucket name, update `bucket_name` in `wrangler.jsonc`.
4. In `wrangler.jsonc`, replace the empty `DEVIN_OUTPOST_ID` value with your outpost ID.  
The default `DEVIN_API_URL` is `https://api.devin.ai/opbeta`. Change this value only if your Devin environment uses another API URL.
5. Store your Devin API token as a Worker secret:  
```sh  
npx wrangler secret put DEVIN_API_TOKEN  
```  
Enter your service-user token when Wrangler prompts you. Do not add the token to `wrangler.jsonc`.
6. Deploy the worker and container:  
```sh  
npm run deploy  
```  
Wrangler builds the container and deploys the worker, container application, and cron trigger.
7. Verify the deployment with the worker URL from the Wrangler output:  
```sh  
curl https://<YOUR_WORKER>.workers.dev/  
```  
The worker returns:  
```json  
{  
  "service": "devin-outpost",  
  "status": "ok"  
}  
```

## Run a Devin session

In Devin, create a session and select your outpost from the **Virtual environment** menu. The worker polls Devin once per minute, so you may need to wait up to one minute for the session container to start.

## How the template works

The template manages the lifecycle of each Devin session:

* **Poll:** A cron trigger runs once per minute. The worker checks the current session statuses for your Devin Outpost.
* **Start:** Each pending or running session receives its own container. The container runs the official Devin worker command.
* **Suspend:** When a session suspends, the container archives `/root`, `/workspace`, and `/opt/devin-persistent` to R2\. The container restores the checkpoint when the session resumes.
* **Terminate:** When a session terminates, the worker removes its container and R2 checkpoint.

The worker coordinates the containers. Devin continues to manage session assignment and the session runtime.

Note

Checkpoints provide suspend-and-resume persistence, not continuous backups. An abrupt container failure can lose recent changes. Each compressed checkpoint must fit within the R2 5 GiB single-upload limit.

## Related resources

* [Devin Outposts overview ↗](https://docs.devin.ai/onboard-devin/outposts)
* [Devin Outpost deployment template ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/devin)
* [Cloudflare Containers](https://developers.cloudflare.com/containers/)
* [R2](https://developers.cloudflare.com/r2/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/devin-outposts/#page","headline":"Run Devin Outposts on Cloudflare · Cloudflare Sandbox SDK docs","description":"Deploy a Devin Outpost that runs each Devin session in an isolated Cloudflare container.","url":"https://developers.cloudflare.com/sandbox/tutorials/devin-outposts/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-21","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Use the OpenAI Agents SDK with Cloudflare Sandbox to build a Python agent that writes, tests, and delivers code in an isolated environment.
title: Build an AI coding agent with OpenAI Agents SDK
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Build an AI coding agent with OpenAI Agents SDK

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/openai-agents/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Sandbox SDK 1.0 preview

This tutorial uses the supported stable bridge template and HTTP API. The 1.0 preview covers the Worker SDK on `@next`; bridge deployments stay on the [stable bridge](https://developers.cloudflare.com/sandbox/bridge/).

The [OpenAI Agents SDK ↗](https://openai.github.io/openai-agents-python/) is a lightweight Python framework for building multi-agent workflows. A Cloudflare Sandbox integration is provided out of the box and ensures that the SDK includes a first-class Cloudflare backend that gives your agents isolated containers for running code, installing packages, and managing files.

In this tutorial, you will deploy a sandbox bridge Worker and build a Python agent that accepts a coding task, executes it inside a Cloudflare Sandbox, and copies the output files to your local machine.

**Time to complete**: 20 minutes

## Prerequisites

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages) with the Containers / Sandbox beta enabled.
2. Install [Python 3.12+ ↗](https://www.python.org/) and [uv ↗](https://docs.astral.sh/uv/).
3. Obtain an [OpenAI API key ↗](https://platform.openai.com/api-keys).

## 1\. Deploy the sandbox bridge

The [sandbox bridge](https://developers.cloudflare.com/sandbox/bridge/) is a Cloudflare Worker that exposes the Sandbox API over HTTP so non-Worker clients — such as a Python script using the OpenAI Agents SDK — can create and control sandboxes.

The Sandbox environment comes pre-configured for Node.js and Python development, so your agents can start writing and running code immediately.

Deploy the bridge to your Cloudflare account:

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/sandbox-sdk/tree/main/bridge/worker)

The button deploys the Worker and generates a `SANDBOX_API_KEY` secret for authentication. When deployment finishes, note your Worker URL and API key — you will need them in the next step.

Manual deployment

If you prefer to deploy step by step:

1. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) and [Docker ↗](https://www.docker.com/).
2. Scaffold the bridge project:  
```sh  
npm create cloudflare sandbox-bridge --template=cloudflare/sandbox-sdk/bridge/worker  
cd sandbox-bridge  
```
3. Authenticate with Cloudflare:  
```sh  
npx wrangler login  
```
4. Set the API key secret:  
```sh  
openssl rand -hex 32 | tee /dev/stderr | npx wrangler secret put SANDBOX_API_KEY  
```  
The key is printed to your terminal and piped to Wrangler. Save it — you will need it to authenticate API requests.
5. Deploy the Worker:  
```sh  
npx wrangler deploy  
```
6. Verify the deployment:  
```sh  
curl https://cloudflare-sandbox-bridge.<your-subdomain>.workers.dev/health  
```  
You should see `{"ok":true}`.

## 2\. Set up your Python project

Create a new directory for the agent:

```sh
mkdir openai-sandbox-agent && cd openai-sandbox-agent
```

Create a `.env` file with your credentials:

```sh
OPENAI_API_KEY=sk-your-openai-key
CLOUDFLARE_SANDBOX_API_KEY=your-bridge-token
CLOUDFLARE_SANDBOX_WORKER_URL=https://cloudflare-sandbox-bridge.your-subdomain.workers.dev
```

## 3\. Build the agent

Create `main.py` with the following content. The inline script metadata tells `uv` which dependencies to install, so everything is contained in a single file:

```python
# /// script
# requires-python = ">=3.12"
# dependencies = ["openai-agents[cloudflare]"]
# ///
"""One-shot coding agent backed by a Cloudflare Sandbox."""

from __future__ import annotations

import asyncio
import os
import sys
from pathlib import Path

from agents import Runner
from agents.extensions.sandbox.cloudflare import (
    CloudflareSandboxClient,
    CloudflareSandboxClientOptions,
)
from agents.run import RunConfig
from agents.sandbox import SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Shell

MODEL = "gpt-5.4"

INSTRUCTIONS = """\
You are an expert developer working inside a sandbox.
The sandbox has bun, node, npm, and python available on the PATH.
Implement the user's task in /workspace, test it, then copy deliverable files to /workspace/output/.
""".strip()


async def copy_output(session, dest: Path) -> list[Path]:
    """Download files from /workspace/output/ in the sandbox to a local directory."""
    dest.mkdir(parents=True, exist_ok=True)
    ls = await session.exec("find", "/workspace/output", "-maxdepth", "1", "-type", "f", shell=False)
    if not ls.ok():
        return []
    copied: list[Path] = []
    for name in (l.strip() for l in ls.stdout.decode().splitlines() if l.strip()):
        handle = await session.read(Path(name))
        local = dest / Path(name).name
        payload = handle.read(); handle.close()
        local.write_bytes(payload if isinstance(payload, bytes) else payload.encode())
        copied.append(local)
    return copied


async def run(prompt: str, output_dir: Path) -> None:
    worker_url = os.environ.get("CLOUDFLARE_SANDBOX_WORKER_URL", "")
    if not worker_url:
        sys.exit("Error: CLOUDFLARE_SANDBOX_WORKER_URL is not set.")

    agent = SandboxAgent(
        name="Developer",
        model=MODEL,
        instructions=INSTRUCTIONS,
        capabilities=[Shell()],
    )

    client = CloudflareSandboxClient()
    options = CloudflareSandboxClientOptions(worker_url=worker_url)
    session = await client.create(manifest=agent.default_manifest, options=options)

    try:
        async with session:
            run_config = RunConfig(
                sandbox=SandboxRunConfig(session=session),
                tracing_disabled=True,
            )

            # Stream tool calls so the user can follow progress.
            result = Runner.run_streamed(agent, prompt, run_config=run_config)
            async for ev in result.stream_events():
                if ev.type == "run_item_stream_event" and ev.name == "tool_called":
                    print(f"  [tool] {getattr(ev.item.raw_item, 'name', '')}")
                elif ev.type == "run_item_stream_event" and ev.name == "tool_output":
                    print(f"  [output] {str(getattr(ev.item, 'output', ''))[:200]}")

            # Copy output files from the sandbox to the local machine.
            copied = await copy_output(session, output_dir)
            if copied:
                print(f"\nCopied {len(copied)} file(s) to {output_dir}:")
                for p in copied:
                    print(f"   {p}")
            else:
                print("\nAgent did not produce any output files.")
    finally:
        await client.delete(session)


if __name__ == "__main__":
    prompt = sys.argv[1] if len(sys.argv) > 1 else "Create a hello world HTTP server using Bun.serve"
    asyncio.run(run(prompt, Path("output")))
```

Here is what the key pieces do:

| Component                      | Purpose                                                                                                                                      |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| SandboxAgent                   | An Agent subclass that accepts sandbox-specific configuration, including capabilities.                                                       |
| Shell()                        | A capability that exposes a shell tool to the LLM, allowing it to run commands inside the sandbox.                                           |
| CloudflareSandboxClient        | Creates and manages sandbox sessions through the bridge Worker. Reads CLOUDFLARE\_SANDBOX\_API\_KEY from the environment for authentication. |
| CloudflareSandboxClientOptions | Points the client at your bridge Worker URL.                                                                                                 |
| Runner.run\_streamed()         | Executes the agent and yields streaming events for tool calls and text output.                                                               |
| SandboxRunConfig               | Attaches a live sandbox session to the run so the agent's tools execute inside the container.                                                |

## 4\. Run the agent

```sh
uv run --env-file .env main.py "Create a hello world HTTP server using Bun.serve"
```

You should see tool calls and output streaming to the console:

```txt
Sending task to sandbox agent (gpt-5.4)...
  [tool] exec_command
  [output] exit_code=0 stdout: mkdir: created directory '/workspace/output'
  [tool] exec_command
  [output] exit_code=0 stdout: Listening on http://localhost:3000

Copied 1 file(s) to output:
   output/server.ts
```

The agent wrote the code, tested it inside the sandbox, and copied the deliverable to your local machine.

## What you built

You built a Python coding agent that:

* Accepts a natural-language coding task
* Executes code in an isolated Cloudflare Sandbox container
* Installs packages, runs tests, and iterates until the task is complete
* Copies deliverable files back to your local machine

The bridge Worker's `Dockerfile` can be fully customized to suit your needs — install additional languages, system packages, or tools to match your use case.

The Cloudflare Sandbox provides more capabilities you can integrate into your agents:

* **PTY sessions** — Open interactive terminal sessions to sandboxes via WebSocket for real-time I/O.
* **Bucket mounts** — Mount R2 or S3-compatible buckets as local directories inside the sandbox for persistent data.
* **Workspace backup and restore** — Persist workspace state with `persist_workspace()` and `hydrate_workspace()` to resume work across sandbox lifecycles.
* **File operations** — Read, write, and manage files programmatically within the sandbox.

## Next steps

* [Workspace chat example ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/bridge/examples/workspace-chat) — A full-stack chat application with a file browser sidebar, built with the OpenAI Agents SDK and Cloudflare Sandbox.
* [OpenAI Agents SDK documentation ↗](https://openai.github.io/openai-agents-python/) — Learn about multi-agent handoffs, guardrails, tracing, and more.
* [Sandbox bridge](https://developers.cloudflare.com/sandbox/bridge/) — Overview of the bridge Worker, usage examples, and configuration.
* [HTTP API reference](https://developers.cloudflare.com/sandbox/bridge/http-api/) — Complete route reference for the bridge API.
* [Sandbox tutorials](https://developers.cloudflare.com/sandbox/tutorials/) — More tutorials covering code execution, data analysis, and CI/CD pipelines.

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/openai-agents/#page","headline":"Build an AI coding agent with OpenAI Agents SDK · Cloudflare Sandbox SDK docs","description":"Use the OpenAI Agents SDK with Cloudflare Sandbox to build a Python agent that writes, tests, and delivers code in an isolated environment.","url":"https://developers.cloudflare.com/sandbox/tutorials/openai-agents/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Python","OpenAI"]}
```

---

---
description: Deploy a Cloudflare execution environment that can be used by Codex via the OpenAI Agents API.
title: Run Codex with Cloudflare Containers using the OpenAI Agents API
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Run Codex with Cloudflare Containers using the OpenAI Agents API

Last updated Sep 10, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/openai-agents-api/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

[OpenAI Agents API ↗](https://developers.openai.com/api/docs/guides/agents-api/overview) gives your application access to the Codex harness through an OpenAI-managed API. OpenAI manages sessions, orchestration, context compaction, and recovery while your application provides tools and Cloudflare Containers can provide the execution environment.

Run self-hosted OpenAI Agents API sessions in Cloudflare Containers. Each session has a Durable Object backed by a container running `codex exec-server`. Signed OpenAI webhooks manage session orchestration.

![Architecture showing an application creating an OpenAI task, webhooks starting a Cloudflare container, and the application fetching the result](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=4160,height=4000,format=webp/_astro/openai-agents-api-arch.CCqSDnZe.jpg) 

The [Cloudflare executor template ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/openai/agents-api) includes the worker and container image used in this guide.

## How it works

* **Cloudflare Worker:** Receives signed OpenAI webhooks and manages one container for each agent session.
* **Cloudflare Container:** Runs `codex exec-server` and agent-generated code against files in `/workspace`.
* **Codex executor:** Connects outbound to OpenAI with a restricted API key while the workspace remains in your Cloudflare account.

## Prerequisites

You need:

* A Cloudflare account with Containers access
* OpenAI Agents API access and an OpenAI API key
* curl
* For manual deployment, Node.js 24 or newer, npm, [Docker ↗](https://www.docker.com/), and Wrangler

Create a restricted OpenAI API key, referred to in this guide as the "executor key", for use by `codex exec-server`. It requires `api.model.read` and `api.agents.environments.connect`. The application key used by the Worker requires `api.agents.read`. Both keys must belong to the same organization, project, and user or service-account owner.

## Quick start

The quickest setup uses the **Deploy to Cloudflare** button. These steps create an OpenAI agent, deploy its execution environment, register the webhook, and run a test task in `/workspace`.

1. **Create an OpenAI agent.** Set your OpenAI API key, then create an agent:

```bash
export OPENAI_API_KEY="<OPENAI_API_KEY>"
```

```bash
curl "https://api.openai.com/v1/agents" \
	--request POST \
	--header "OpenAI-Beta: agents=v1" \
	--header "Authorization: Bearer $OPENAI_API_KEY" \
	--json '{
		"name": "sandbox-demo",
		"model": "gpt-5.6-sol"
	}'
```

Copy the `id` field from the response and save it as the agent ID:

```bash
export OPENAI_AGENT_ID="agent_..."
```

1. **Deploy the worker and container.** Generate and save a shared secret for the container cleanup endpoint:  
```bash  
openssl rand -hex 32  
```  
Select **Deploy to Cloudflare**:  
[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/sandbox-sdk/tree/main/openai/agents-api)  
Enter these values when prompted:

| Variable                   | Value                                                 |
| -------------------------- | ----------------------------------------------------- |
| OPENAI\_API\_KEY           | The OpenAI key used to retrieve session state         |
| OPENAI\_EXECUTOR\_API\_KEY | The restricted executor key                           |
| OPENAI\_AGENT\_ID          | The agent ID created above                            |
| OPENAI\_WEBHOOK\_SECRET    | pending-webhook-registration for the first deployment |
| EXECUTOR\_CLIENT\_SECRET   | The shared secret generated above                     |  
Save the deployed Worker URL:  
```bash  
export WORKER_URL="https://<YOUR_WORKER>.workers.dev"  
```  
The container stays available for 30 seconds (configurable via `EXECUTOR_KEEP_ALIVE_SECONDS`). Prewarming and idle snapshots are enabled by default.
2. **Register the webhook.** In [OpenAI project webhook settings ↗](https://platform.openai.com/settings/project/webhooks), register the publicly reachable endpoint `https://<YOUR_WORKER>.workers.dev/webhook`.  
Subscribe to these events:

  * `agent.session.created`
  * `agent.session.action_required`
  * `agent.session.in_progress`
  * `agent.session.idle`
  * `agent.session.failed`  
Copy the signing secret returned by OpenAI. Replace `OPENAI_WEBHOOK_SECRET` in the Worker's **Settings** \> **Variables and Secrets**, then select **Deploy**. If you used manual deployment, set it with Wrangler from the Cloudflare template directory:  
```bash  
npx wrangler secret put OPENAI_WEBHOOK_SECRET  
```  
Verify the setup:  
```bash  
curl --fail-with-body "$WORKER_URL/health"  
```  
The Worker is ready for this guide when the response contains both `"configured": true` and `"webhook_configured": true`.
3. **Run a test task.** Create a self-hosted session:

```bash
curl "https://api.openai.com/v1/agents/sessions" \
	--request POST \
	--header "OpenAI-Beta: agents=v1" \
	--header "Authorization: Bearer $OPENAI_API_KEY" \
	--json '{
		"agent_id": "$OPENAI_AGENT_ID",
		"environment": {
				"type": "self_hosted",
				"workspace_directory": "/workspace"
		}
	}'
```

Copy the `id` field from the response and save it as the session ID:

```bash
export SESSION_ID="sess_..."
```

Open the session event stream in one terminal:

```bash
curl --no-buffer \
  "https://api.openai.com/v1/agents/sessions/$SESSION_ID/events" \
  --header "OpenAI-Beta: agents=v1" \
  --header "Authorization: Bearer $OPENAI_API_KEY" \
  --header "Accept: text/event-stream"
```

While the stream is open, submit a task from another terminal:

```bash
curl "https://api.openai.com/v1/agents/sessions/$SESSION_ID/events" \
	--request POST \
	--header "OpenAI-Beta: agents=v1" \
	--header "Authorization: Bearer $OPENAI_API_KEY" \
	--json '{
		"events": [
				{
						"type": "session.input.message",
						"input": [
								{
										"role": "user",
										"content": [
												{
														"type": "input_text",
														"text": "Use the shell to write hello to /workspace/hello.txt, then read it."
												}
										]
								}
						]
				}
		]
	}'
```

The event stream shows the agent's progress and response.

Deploy manually

Instead of using the deploy button in step 2 above:

1. Clone the Cloudflare template repository, install dependencies, and log in to Cloudflare:  
```bash  
git clone https://github.com/cloudflare/sandbox-sdk.git  
cd sandbox-sdk  
npm install  
cd openai/agents-api  
npx wrangler login  
```
2. Generate and save a shared secret for the container cleanup endpoint:  
```bash  
openssl rand -hex 32  
```
3. Store the Worker secrets. Enter your OpenAI key, restricted executor key, agent ID, and shared secret when prompted:  
```bash  
npx wrangler secret put OPENAI_API_KEY  
npx wrangler secret put OPENAI_EXECUTOR_API_KEY  
npx wrangler secret put OPENAI_AGENT_ID  
npx wrangler secret put EXECUTOR_CLIENT_SECRET  
```
4. Deploy the worker and container:  
```bash  
npm run deploy  
```

`EXECUTOR_KEEP_ALIVE_SECONDS`, `EXECUTOR_PREWARM_ENABLED`, and `EXECUTOR_SNAPSHOTS_ENABLED` are non-secret settings in `wrangler.jsonc`.

Save the deployed Worker URL, then complete step 3 above. Return to the selected OpenAI example repository root before running step 4.

Reconnect an existing session

Open the session event stream again, then submit follow-up input:

```bash
curl "https://api.openai.com/v1/agents/sessions/$SESSION_ID/events" \
	--request POST \
	--header "OpenAI-Beta: agents=v1" \
	--header "Authorization: Bearer $OPENAI_API_KEY" \
	--json '{
		"events": [
				{
						"type": "session.input.message",
						"input": [
								{
										"role": "user",
										"content": [
												{
														"type": "input_text",
														"text": "Read /workspace/hello.txt again."
												}
										]
								}
						]
				}
		]
	}'
```

## Agents API on Cloudflare Workers

For a complete TypeScript application with an HTTP interface, refer to the [basic Agents API example ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/openai/agents-api/basic) in the Cloudflare Sandbox SDK repository.

The example uses the OpenAI Agents API TypeScript SDK to create self-hosted sessions backed by the deployed executor Worker. It includes endpoints for initial input, follow-up input, and cleanup. Its `POST /demo` endpoint runs the complete workflow: create a session, write and read a file in the container, send a follow-up message, then delete the OpenAI session and Cloudflare executor.

## Clean up

Delete the OpenAI session:

```bash
curl "https://api.openai.com/v1/agents/sessions/$SESSION_ID" \
	--request DELETE \
	--header "OpenAI-Beta: agents=v1" \
	--header "Authorization: Bearer $OPENAI_API_KEY"
```

To stop its Cloudflare Container immediately, use the shared secret saved during deployment:

```bash
export WORKER_URL="https://<YOUR_WORKER>.workers.dev"
export EXECUTOR_CLIENT_SECRET="<EXECUTOR_CLIENT_SECRET>"

curl --fail-with-body \
  --request DELETE \
  --header "Authorization: Bearer $EXECUTOR_CLIENT_SECRET" \
  "$WORKER_URL/executors/$SESSION_ID"
```

Deleting an OpenAI session does not send a container cleanup webhook. Without explicit cleanup, an idle session keeps its snapshot for the next environment connection. A failed-session webhook or a session lookup that returns `404 Not Found` releases the container and clears its saved snapshot.

## Execution lifecycle

1. **Request:** The application creates or retrieves an OpenAI session and submits input through the Agents API.
2. **Prewarm:** By default, a signed `agent.session.created` webhook causes the Worker to retrieve current session state and start the self-hosted container with its environment ID and remote URL.
3. **Reconcile:** An `agent.session.action_required` webhook causes the Worker to retrieve current session state, confirm that the configured agent owns the session, and read the required environment ID and remote URL.
4. **Start:** The session-named Durable Object starts a Cloudflare Container with the connection details and restricted executor key. `codex exec-server` connects outbound to OpenAI.
5. **Keep alive:** container starts, environment-connection actions, and `agent.session.in_progress` events arm the lifecycle deadline. When it expires, the Worker retrieves current session state and gives active sessions another deadline.
6. **Idle:** An `agent.session.idle` webhook snapshots the whole container when snapshots are enabled and arms the lifecycle deadline. When the deadline expires, the Worker stops the container and keeps its snapshot.
7. **Reconnect:** New input sends another `agent.session.action_required` webhook. The Worker reuses a running container for the same environment ID or restores the saved snapshot when it starts the next environment.

![Lifecycle showing an application creating an Agents API session, OpenAI sending webhooks to Cloudflare, and the container connecting its Codex executor to OpenAI](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=5928,height=3104,format=webp/_astro/openai-agents-api-lifecycle.BOaDc5z5.jpg) 

Failure and cleanup

An `agent.session.failed` webhook or a session lookup that returns `404 Not Found` stops the container and clears saved snapshots. Explicit cleanup releases it immediately.

### Workspace restoration

Container snapshots are currently in private beta. If you would like to enable the feature on your Cloudflare account please contact your Cloudflare representative.

When `EXECUTOR_SNAPSHOTS_ENABLED` is `true`, a confirmed idle session creates a whole-container snapshot before its container stops. The next environment connection restores that snapshot, including `/workspace`. If snapshot creation fails, the Worker leaves the current container running and schedules another lifecycle check.

Snapshots are best-effort session recovery, not durable backup. Failed or deleted sessions and explicit cleanup clear the saved snapshot. When snapshots are disabled or unavailable, the next executor receives a fresh `/workspace`. For durable files, adapt the container image to use an [R2 FUSE mount](https://developers.cloudflare.com/containers/examples/r2-fuse-mount/).

## Add tools to the container

The executor image is defined in `openai/agents-api/Dockerfile` in the Cloudflare executor template. Add Debian packages to its existing `apt-get install` command. For example, add `jq` and Python:

```text
RUN apt-get update \
    && apt-get install --yes --no-install-recommends \
      ca-certificates \
      curl \
      git \
      jq \
      python3 \
      ripgrep \
    && rm -rf /var/lib/apt/lists/*
```

You can also install language-specific tools in the image, such as global npm packages. Do not store API keys or other secrets in the Dockerfile. Pass runtime secrets through Worker bindings or container environment variables.

Run `npm run deploy` from `openai/agents-api` to build and deploy the updated image.

## Security considerations

The runnable example is intentionally minimal. Review these defaults before adapting it for production:

* **Secrets:** The controller key, webhook secret, and `EXECUTOR_CLIENT_SECRET` remain Worker secrets. The restricted executor key is passed into the container as `CODEX_API_KEY`, where processes inside the container can read it. Refer to [Container environment variables and secrets](https://developers.cloudflare.com/containers/examples/env-vars-and-secrets/) for other ways to configure container instances.
* **Network access:** The example enables outbound Internet access so `codex exec-server` can reach OpenAI. Use [Container outbound traffic controls](https://developers.cloudflare.com/containers/platform-details/outbound-traffic/) to restrict destinations or inject credentials for other services.
* **Files:** `/workspace` uses ephemeral container storage. Use a [read-only R2 FUSE mount](https://developers.cloudflare.com/containers/examples/r2-fuse-mount/#mounting-buckets-as-read-only) when an agent needs durable source files that it should not modify.
* **Worker access:** OpenAI must be able to reach `/webhook` without an interactive Access login. The Worker verifies OpenAI's webhook signature, and the manual cleanup endpoint requires `EXECUTOR_CLIENT_SECRET`. If you protect other routes with Cloudflare Access, use [path-specific policies](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/app-paths/) that leave `/webhook` reachable.

For more information, refer to [Containers architecture](https://developers.cloudflare.com/containers/platform-details/architecture/).

## Related resources

* [Cloudflare reference worker ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/openai/agents-api)
* [OpenAI Agents API documentation ↗](https://developers.openai.com/api/docs/guides/agents-api/overview)
* [OpenAI Python Cloudflare webhook example ↗](https://github.com/OpenAI/agents-api-python-preview/tree/main/examples/self%5Fhosted%5Fsandbox/webhook%5Fmanaged/cloudflare)
* [OpenAI TypeScript Cloudflare webhook example ↗](https://github.com/OpenAI/agents-api-typescript-preview/tree/main/examples/self%5Fhosted%5Fsandbox/webhook%5Fmanaged/cloudflare)
* [Cloudflare Containers](https://developers.cloudflare.com/containers/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/openai-agents-api/#page","headline":"Run Codex with Cloudflare Containers using the OpenAI Agents API · Cloudflare Sandbox SDK docs","description":"Deploy a Cloudflare execution environment that can be used by Codex via the OpenAI Agents API.","url":"https://developers.cloudflare.com/sandbox/tutorials/openai-agents-api/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-09-10","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Mount R2 buckets as local filesystem paths to persist data across sandbox lifecycles.
title: Data persistence with R2
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Data persistence with R2

Last updated May 11, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/persistent-storage/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Mount object storage buckets as local filesystem paths to persist data across sandbox lifecycles. This tutorial uses Cloudflare R2, but the same approach works with any S3-compatible provider.

This tutorial shows how to persist an external data directory mounted at `/data`. If you want the working project in `/workspace` to persist, refer to [Backup and restore](https://developers.cloudflare.com/sandbox/guides/backup-restore/).

**Time to complete:** 20 minutes

## What you'll build

A Worker that processes data, stores results in an R2 bucket mounted as a local directory, and demonstrates that data persists even after the sandbox is destroyed and recreated.

**Key concepts you'll learn**:

* Mounting R2 buckets as filesystem paths
* Automatic data persistence across sandbox lifecycles
* Working with mounted storage using standard file operations

## Prerequisites

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

Node.js version manager

Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), discussed later in this guide, requires a Node version of `16.17.0` or later.

You'll also need:

* [Docker ↗](https://www.docker.com/) running locally
* An R2 bucket (create one in the [Cloudflare dashboard ↗](https://dash.cloudflare.com/?to=/:account/r2))

## 1\. Create your project

npmyarnpnpm

```
npm create cloudflare@latest -- data-pipeline --template=cloudflare/sandbox-sdk/examples/minimal
```

```
yarn create cloudflare data-pipeline --template=cloudflare/sandbox-sdk/examples/minimal
```

```
pnpm create cloudflare@latest data-pipeline --template=cloudflare/sandbox-sdk/examples/minimal
```

```sh
cd data-pipeline
```

## 2\. Configure R2 binding

Add an R2 bucket binding to your `wrangler.json`:

```json
{
  "name": "data-pipeline",
  "compatibility_date": "2025-11-09",
  "durable_objects": {
    "bindings": [
      { "name": "Sandbox", "class_name": "Sandbox" }
    ]
  },
  "r2_buckets": [
    {
      "binding": "DATA_BUCKET",
      "bucket_name": "my-data-bucket"
    }
  ]
}
```

Replace `my-data-bucket` with your R2 bucket name. Create the bucket first in the [Cloudflare dashboard ↗](https://dash.cloudflare.com/?to=/:account/r2).

## 3\. Build the data processor

Replace `src/index.ts` with code that mounts R2 and processes data:

```js
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		const url = new URL(request.url);
		const sandbox = getSandbox(env.Sandbox, "data-processor");

		// Mount R2 bucket to /data directory
		await sandbox.mountBucket("my-data-bucket", "/data", {
			endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
		});

		if (url.pathname === "/process") {
			// Process data and save to mounted R2
			const result = await sandbox.exec("python", {
				args: [
					"-c",
					`
import json
import os
from datetime import datetime

# Read input (or create sample data)
data = [
    {'id': 1, 'value': 42},
    {'id': 2, 'value': 87},
    {'id': 3, 'value': 15}
]

# Process: calculate sum and average
total = sum(item['value'] for item in data)
avg = total / len(data)

# Save results to mounted R2 (/data is the mounted bucket)
result = {
    'timestamp': datetime.now().isoformat(),
    'total': total,
    'average': avg,
    'processed_count': len(data)
}

os.makedirs('/data/results', exist_ok=True)
with open('/data/results/latest.json', 'w') as f:
    json.dump(result, f, indent=2)

print(json.dumps(result))
				`,
				],
			});

			return Response.json({
				message: "Data processed and saved to R2",
				result: JSON.parse(result.stdout),
			});
		}

		if (url.pathname === "/results") {
			// Read results from mounted R2
			const result = await sandbox.exec("cat", {
				args: ["/data/results/latest.json"],
			});

			if (!result.success) {
				return Response.json(
					{ error: "No results found yet" },
					{ status: 404 },
				);
			}

			return Response.json({
				message: "Results retrieved from R2",
				data: JSON.parse(result.stdout),
			});
		}

		if (url.pathname === "/destroy") {
			// Destroy sandbox to demonstrate persistence
			await sandbox.destroy();
			return Response.json({
				message: "Sandbox destroyed. Data persists in R2!",
			});
		}

		return new Response(
			`
Data Pipeline with Persistent Storage

Endpoints:
- POST /process  - Process data and save to R2
- GET /results   - Retrieve results from R2
- POST /destroy  - Destroy sandbox (data survives!)

Try this flow:
1. POST /process  (processes and saves to R2)
2. POST /destroy  (destroys sandbox)
3. GET /results   (data still accessible from R2)
		`,
			{ headers: { "Content-Type": "text/plain" } },
		);
	},
};
```

```typescript
import { getSandbox, type Sandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

interface Env {
	Sandbox: DurableObjectNamespace<Sandbox>;
	DATA_BUCKET: R2Bucket;
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);
		const sandbox = getSandbox(env.Sandbox, 'data-processor');

		// Mount R2 bucket to /data directory
		await sandbox.mountBucket('my-data-bucket', '/data', {
			endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com'
		});

		if (url.pathname === '/process') {
			// Process data and save to mounted R2
			const result = await sandbox.exec('python', {
				args: ['-c', `
import json
import os
from datetime import datetime

# Read input (or create sample data)
data = [
    {'id': 1, 'value': 42},
    {'id': 2, 'value': 87},
    {'id': 3, 'value': 15}
]

# Process: calculate sum and average
total = sum(item['value'] for item in data)
avg = total / len(data)

# Save results to mounted R2 (/data is the mounted bucket)
result = {
    'timestamp': datetime.now().isoformat(),
    'total': total,
    'average': avg,
    'processed_count': len(data)
}

os.makedirs('/data/results', exist_ok=True)
with open('/data/results/latest.json', 'w') as f:
    json.dump(result, f, indent=2)

print(json.dumps(result))
				`]
			});

			return Response.json({
				message: 'Data processed and saved to R2',
				result: JSON.parse(result.stdout)
			});
		}

		if (url.pathname === '/results') {
			// Read results from mounted R2
			const result = await sandbox.exec('cat', {
				args: ['/data/results/latest.json']
			});

			if (!result.success) {
				return Response.json({ error: 'No results found yet' }, { status: 404 });
			}

			return Response.json({
				message: 'Results retrieved from R2',
				data: JSON.parse(result.stdout)
			});
		}

		if (url.pathname === '/destroy') {
			// Destroy sandbox to demonstrate persistence
			await sandbox.destroy();
			return Response.json({ message: 'Sandbox destroyed. Data persists in R2!' });
		}

		return new Response(`
Data Pipeline with Persistent Storage

Endpoints:
- POST /process  - Process data and save to R2
- GET /results   - Retrieve results from R2
- POST /destroy  - Destroy sandbox (data survives!)

Try this flow:
1. POST /process  (processes and saves to R2)
2. POST /destroy  (destroys sandbox)
3. GET /results   (data still accessible from R2)
		`, { headers: { 'Content-Type': 'text/plain' } });
	}
};
```

Replace YOUR\_ACCOUNT\_ID

Replace `YOUR_ACCOUNT_ID` in the endpoint URL with your Cloudflare account ID. Find it in the [dashboard ↗](https://dash.cloudflare.com/) under **R2** \> **Overview**.

## 4\. Deploy to production

**Generate R2 API tokens:**

1. Go to **R2** \> **Overview** in the [Cloudflare dashboard ↗](https://dash.cloudflare.com/)
2. Select **Manage R2 API Tokens**
3. Create a token with **Object Read & Write** permissions
4. Copy the **Access Key ID** and **Secret Access Key**

**Set up credentials as Worker secrets:**

```sh
npx wrangler secret put AWS_ACCESS_KEY_ID
# Paste your R2 Access Key ID

npx wrangler secret put AWS_SECRET_ACCESS_KEY
# Paste your R2 Secret Access Key
```

Worker secrets are encrypted and only accessible to your deployed Worker. The SDK automatically detects these credentials when `mountBucket()` is called.

**Deploy your Worker:**

```sh
npx wrangler deploy
```

After deployment, wrangler outputs your Worker URL (e.g., `https://data-pipeline.yourname.workers.dev`).

## 5\. Test the persistence flow

Now test against your deployed Worker. Replace `YOUR_WORKER_URL` with your actual Worker URL:

```sh
# 1. Process data (saves to R2)
curl -X POST https://YOUR_WORKER_URL/process
# Returns: { "message": "Data processed...", "result": { "total": 144, "average": 48, ... } }

# 2. Verify data is accessible
curl https://YOUR_WORKER_URL/results
# Returns the same results from R2

# 3. Destroy the sandbox
curl -X POST https://YOUR_WORKER_URL/destroy
# Returns: { "message": "Sandbox destroyed. Data persists in R2!" }

# 4. Access results again (from new sandbox)
curl https://YOUR_WORKER_URL/results
# Still works! Data persisted across sandbox lifecycle
```

The key insight: After destroying the sandbox, the next request creates a new sandbox instance, mounts the same R2 bucket, and finds the data still there.

## What you learned

In this tutorial, you built a data pipeline that demonstrates filesystem persistence through R2 bucket mounting:

* **Mounting buckets**: Use `mountBucket()` to make R2 accessible as a local directory
* **Standard file operations**: Access mounted buckets using familiar filesystem commands (`cat`, Python `open()`, etc.)
* **Automatic persistence**: Data written to mounted directories survives sandbox destruction
* **Choose the right persistence model**: Use bucket mounts for external storage directories such as `/data`, and consider backup and restore when you need a persistent workspace under `/workspace`
* **Credential management**: Configure R2 access using environment variables or explicit credentials

## Next steps

* [Mount buckets guide](https://developers.cloudflare.com/sandbox/guides/mount-buckets/) \- Comprehensive mounting reference
* [Storage API](https://developers.cloudflare.com/sandbox/api/storage/) \- Complete API documentation
* [Environment variables](https://developers.cloudflare.com/sandbox/configuration/environment-variables/) \- Credential configuration options

## Related resources

* [R2 documentation](https://developers.cloudflare.com/r2/) \- Learn about Cloudflare R2
* [Background processes guide](https://developers.cloudflare.com/sandbox/guides/background-processes/) \- Long-running data processing
* [Sandboxes concept](https://developers.cloudflare.com/sandbox/concepts/sandboxes/) \- Understanding sandbox lifecycle

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/persistent-storage/#page","headline":"Data persistence with R2 · Cloudflare Sandbox SDK docs","description":"Mount R2 buckets as local filesystem paths to persist data across sandbox lifecycles.","url":"https://developers.cloudflare.com/sandbox/tutorials/persistent-storage/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-11","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Build a code interpreter using Workers AI GPT-OSS model with the official workers-ai-provider package.
title: Code interpreter with Workers AI
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Code interpreter with Workers AI

Last updated May 5, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/tutorials/workers-ai-code-interpreter/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Build a powerful code interpreter that gives the [gpt-oss model](https://developers.cloudflare.com/workers-ai/models/gpt-oss-120b/) on Workers AI the ability to execute Python code using the Cloudflare Sandbox SDK.

**Time to complete:** 15 minutes

## What you'll build

A Cloudflare Worker that accepts natural language prompts, uses GPT-OSS to decide when Python code execution is needed, runs the code in isolated sandboxes, and returns results with AI-powered explanations.

## Prerequisites

1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages).
2. Install [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).

Node.js version manager

Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), discussed later in this guide, requires a Node version of `16.17.0` or later.

You'll also need:

* [Docker ↗](https://www.docker.com/) running locally

## 1\. Create your project

Create a new Sandbox SDK project:

npmyarnpnpm

```
npm create cloudflare@latest -- workers-ai-interpreter --template=cloudflare/sandbox-sdk/examples/code-interpreter
```

```
yarn create cloudflare workers-ai-interpreter --template=cloudflare/sandbox-sdk/examples/code-interpreter
```

```
pnpm create cloudflare@latest workers-ai-interpreter --template=cloudflare/sandbox-sdk/examples/code-interpreter
```

```sh
cd workers-ai-interpreter
```

## 2\. Review the implementation

The template includes a complete implementation using the latest best practices. Let's examine the key components:

```typescript
// src/index.ts
import { getSandbox } from "@cloudflare/sandbox";
import { generateText, stepCountIs, tool } from "ai";
import { createWorkersAI } from "workers-ai-provider";
import { z } from "zod";

const MODEL = "@cf/openai/gpt-oss-120b" as const;

async function handleAIRequest(input: string, env: Env): Promise<string> {
	const workersai = createWorkersAI({ binding: env.AI });

	const result = await generateText({
		model: workersai(MODEL),
		messages: [{ role: "user", content: input }],
		tools: {
			execute_python: tool({
				description: "Execute Python code and return the output",
				inputSchema: z.object({
					code: z.string().describe("The Python code to execute"),
				}),
				execute: async ({ code }) => {
					return executePythonCode(env, code);
				},
			}),
		},
		stopWhen: stepCountIs(5),
	});

	return result.text || "No response generated";
}
```

**Key improvements over direct REST API calls:**

* **Official packages**: Uses `workers-ai-provider` instead of manual API calls
* **Vercel AI SDK**: Leverages `generateText()` and `tool()` for clean function calling
* **No API keys**: Uses native AI binding instead of environment variables
* **Type safety**: Full TypeScript support with proper typing

## 3\. Check your configuration

The template includes the proper Wrangler configuration:

```jsonc
{
  "name": "sandbox-code-interpreter-example",
  "main": "src/index.ts",
  // Set this to today's date
  "compatibility_date": "2026-09-12",
  "ai": {
    "binding": "AI"
  },
  "containers": [
    {
      "class_name": "Sandbox",
      "image": "./Dockerfile",
      "name": "sandbox",
      "max_instances": 1,
      "instance_type": "basic"
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "class_name": "Sandbox",
        "name": "Sandbox"
      }
    ]
  }
}
```

```toml
name = "sandbox-code-interpreter-example"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-12"

[ai]
binding = "AI"

[[containers]]
class_name = "Sandbox"
image = "./Dockerfile"
name = "sandbox"
max_instances = 1
instance_type = "basic"

[[durable_objects.bindings]]
class_name = "Sandbox"
name = "Sandbox"
```

**Configuration highlights:**

* **AI binding**: Enables direct access to Workers AI models
* **Container setup**: Configures sandbox container with Dockerfile
* **Durable Objects**: Provides persistent sandboxes with state management

## 4\. Test locally

Start the development server:

```sh
npm run dev
```

Note

First run builds the Docker container (2-3 minutes). Subsequent runs are much faster.

Test with curl:

```sh
# Simple calculation
curl -X POST http://localhost:8787/run \
  -H "Content-Type: application/json" \
  -d '{"input": "Calculate 5 factorial using Python"}'

# Complex operations
curl -X POST http://localhost:8787/run \
  -H "Content-Type: application/json" \
  -d '{"input": "Use Python to find all prime numbers under 20"}'

# Data analysis
curl -X POST http://localhost:8787/run \
  -H "Content-Type: application/json" \
  -d '{"input": "Create a list of the first 10 squares and calculate their sum"}'
```

## 5\. Deploy

Deploy your Worker:

```sh
npx wrangler deploy
```

Caution

After first deployment, wait 2-3 minutes for container provisioning before making requests.

## 6\. Test your deployment

Try more complex queries:

```sh
# Data visualization preparation
curl -X POST https://workers-ai-interpreter.YOUR_SUBDOMAIN.workers.dev/run \
  -H "Content-Type: application/json" \
  -d '{"input": "Generate sample sales data for 12 months and calculate quarterly totals"}'

# Algorithm implementation
curl -X POST https://workers-ai-interpreter.YOUR_SUBDOMAIN.workers.dev/run \
  -H "Content-Type: application/json" \
  -d '{"input": "Implement a binary search function and test it with a sorted array"}'

# Mathematical computation
curl -X POST https://workers-ai-interpreter.YOUR_SUBDOMAIN.workers.dev/run \
  -H "Content-Type: application/json" \
  -d '{"input": "Calculate the standard deviation of [2, 4, 4, 4, 5, 5, 7, 9]"}'
```

## How it works

1. **User input**: Send natural language prompts to the `/run` endpoint
2. **AI decision**: GPT-OSS receives the prompt with an `execute_python` tool available
3. **Smart execution**: Model decides whether Python code execution is needed
4. **Sandbox isolation**: Code runs in isolated Cloudflare Sandbox containers
5. **AI explanation**: Results are integrated back into the AI's response for final output

## What you built

You deployed a sophisticated code interpreter that:

* **Native Workers AI integration**: Uses the official `workers-ai-provider` package for seamless integration
* **Function calling**: Leverages Vercel AI SDK for clean tool definitions and execution
* **Secure execution**: Runs Python code in isolated sandbox containers
* **Intelligent responses**: Combines AI reasoning with code execution results

## Next steps

* [Analyze data with AI](https://developers.cloudflare.com/sandbox/tutorials/analyze-data-with-ai/) \- Add pandas and matplotlib for advanced data analysis
* [Code Interpreter API](https://developers.cloudflare.com/sandbox/api/interpreter/) \- Use the built-in code interpreter with structured outputs
* [Streaming output](https://developers.cloudflare.com/sandbox/guides/streaming-output/) \- Show real-time execution progress
* [API reference](https://developers.cloudflare.com/sandbox/api/) \- Explore all available sandbox methods

## Related resources

* [Workers AI](https://developers.cloudflare.com/workers-ai/) \- Learn about Cloudflare's AI platform
* [workers-ai-provider package ↗](https://github.com/cloudflare/ai/tree/main/packages/workers-ai-provider) \- Official Workers AI integration
* [Vercel AI SDK ↗](https://sdk.vercel.ai/) \- Universal toolkit for AI applications
* [GPT-OSS model documentation](https://developers.cloudflare.com/workers-ai/models/gpt-oss-120b/) \- Model details and capabilities

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/tutorials/workers-ai-code-interpreter/#page","headline":"Code interpreter with Workers AI · Cloudflare Sandbox SDK docs","description":"Build a code interpreter using Workers AI GPT-OSS model with the official workers-ai-provider package.","url":"https://developers.cloudflare.com/sandbox/tutorials/workers-ai-code-interpreter/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-05","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Practical guides for solving specific tasks with the Sandbox SDK.
title: How-to guides
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# How-to guides

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

These guides show you how to solve specific problems and implement features with the Sandbox SDK. Each guide focuses on a particular task and provides practical, production-ready solutions.

[**2026 deprecation migration guide**Migrate away from deprecated Sandbox SDK features on the current stable package.](https://developers.cloudflare.com/sandbox/guides/2026-deprecation/)

[**Run background processes**Start and manage long-running services and applications.](https://developers.cloudflare.com/sandbox/guides/background-processes/)

[**Backup and restore**Snapshot a sandbox directory to R2 and restore it later.](https://developers.cloudflare.com/sandbox/guides/backup-restore/)

[**Browser terminals**Connect browser-based terminals to sandbox shells using xterm.js or raw WebSockets.](https://developers.cloudflare.com/sandbox/guides/browser-terminals/)

[**Use code interpreter**Execute Python and JavaScript code with rich outputs.](https://developers.cloudflare.com/sandbox/guides/code-execution/)

[**Deploy a Sandbox application**Deploy a Sandbox Worker and keep the npm package and container image on the same release line.](https://developers.cloudflare.com/sandbox/guides/deploy/)

[**Run Docker-in-Docker**Run Docker commands inside a sandbox container.](https://developers.cloudflare.com/sandbox/guides/docker-in-docker/)

[**Execute commands**Run commands with streaming output, error handling, and shell access.](https://developers.cloudflare.com/sandbox/guides/execute-commands/)

[**Expose services**Create preview URLs and expose ports for web services.](https://developers.cloudflare.com/sandbox/guides/expose-services/)

[**Watch filesystem changes**Monitor files and directories in real-time to build responsive development tools and automation workflows.](https://developers.cloudflare.com/sandbox/guides/file-watching/)

[**Work with Git**Clone repositories, manage branches, and automate Git operations.](https://developers.cloudflare.com/sandbox/guides/git-workflows/)

[**Manage files**Read, write, organize, and synchronize files in the sandbox.](https://developers.cloudflare.com/sandbox/guides/manage-files/)

[**Mount buckets**Mount S3-compatible object storage as local filesystems for persistent data storage.](https://developers.cloudflare.com/sandbox/guides/mount-buckets/)

[**Handle outbound traffic**Intercept and handle outbound HTTP from sandboxes using Workers.](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/)

[**Configure preview URLs on a custom domain**Set up wildcard DNS, routes, and TLS so exposePort preview URLs work on your domain.](https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/)

[**Stream output**Handle real-time output from commands and processes.](https://developers.cloudflare.com/sandbox/guides/streaming-output/)

[**WebSocket connections**Connect to WebSocket servers running in sandboxes.](https://developers.cloudflare.com/sandbox/guides/websocket-connections/)

[**Connect to Workers bindings**Access KV, R2, Durable Objects, and other bindings from a sandbox.](https://developers.cloudflare.com/sandbox/guides/workers-connections/)

## Related resources

* [Tutorials](https://developers.cloudflare.com/sandbox/tutorials/) \- Step-by-step learning paths
* [API reference](https://developers.cloudflare.com/sandbox/api/) \- Complete method documentation

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/sandbox/guides/#page","headline":"How-to guides · Cloudflare Sandbox SDK docs","description":"Practical guides for solving specific tasks with the Sandbox SDK.","url":"https://developers.cloudflare.com/sandbox/guides/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Migrate away from deprecated Sandbox SDK features on the current stable package.
title: 2026 deprecation migration guide
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# 2026 deprecation migration guide

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/2026-deprecation/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Two different migration paths

This guide is for apps that **stay on the current stable** `@cloudflare/sandbox` package and need to leave deprecated features (transports, default sessions, stream helpers, and related APIs).

To move onto **Sandbox SDK 1.0** (`@cloudflare/sandbox@next`), use [Migrate to the 1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/) instead. That path covers argv `exec`, removed sessions, terminals, interpreter attach, and container cutover. Completing this stable-line guide first is still useful if you use the older transports or stream helpers.

This guide walks through migrating away from Sandbox SDK features deprecated in the [deprecation announcement](https://developers.cloudflare.com/changelog/sandbox/2026-06-09-deprecating-sandbox-sdk-features/). Do not build new work on these APIs. Finish this cleanup on the stable package, then move to the [1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/) when you can.

For the announcement and rationale, refer to the [deprecation changelog entry](https://developers.cloudflare.com/changelog/sandbox/2026-06-09-deprecating-sandbox-sdk-features/).

## Before you migrate

Update to the latest Sandbox SDK release before changing transport or session configuration. If your project uses a version earlier than `0.9.1`, deploy a newer `@cloudflare/sandbox` package and container image before switching to RPC transport. Session isolation with `enableDefaultSession: false` requires Sandbox SDK `0.10.3` or newer — on `0.9.1`–`0.10.2`, upgrade first, then set the flag.

Search your codebase for deprecated configuration and APIs:

```sh
rg 'SANDBOX_TRANSPORT|transport:|exposePort\(|enableDefaultSession|execStream\(|readFileStream|writeFileStream'
```

Also review any code that uses stream-specific file helpers or depends on shell state carrying across separate `exec()` calls.

## HTTP and WebSocket transports

HTTP and WebSocket transports are deprecated. Switch to the RPC transport.

To configure RPC transport for every sandbox in your Worker, set `SANDBOX_TRANSPORT` in your Worker's configuration:

```jsonc
{
	"vars": {
		"SANDBOX_TRANSPORT": "rpc"
	}
}
```

```toml
[vars]
SANDBOX_TRANSPORT = "rpc"
```

To configure RPC transport for a specific sandbox, pass `transport: "rpc"` to `getSandbox()`:

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "user-123", {
	transport: "rpc",
});
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "user-123", {
	transport: "rpc",
});
```

For more information, refer to [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/).

## Desktop

The desktop feature was removed in `0.10.2`. The feature ran a full Linux desktop inside the sandbox for computer-use style automation. If you still need that shape, rebuild it with [extensions](https://developers.cloudflare.com/sandbox/1-0-preview/extensions/) rather than a built-in desktop API. Keep Sandbox SDK for isolated command execution, file operations, and runtime workflows that do not require an in-sandbox desktop.

## Expose ports

Replace `exposePort()` with the tunnels API for public URLs. The tunnels API requires RPC transport.

Use quick tunnels for development, demos, and short-lived URLs. Use named tunnels for production traffic, webhook receivers, OAuth callbacks, and stable hostnames on a zone you control.

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox", {
	transport: "rpc",
});

const server = await sandbox.startProcess("python -m http.server 8080");
await server.waitForPort(8080);

const tunnel = await sandbox.tunnels.get(8080);
return Response.json({ url: tunnel.url });
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox", {
	transport: "rpc",
});

const server = await sandbox.startProcess("python -m http.server 8080");
await server.waitForPort(8080);

const tunnel = await sandbox.tunnels.get(8080);
return Response.json({ url: tunnel.url });
```

If your `exposePort()` flow used `proxyToSandbox()` to inject authentication or rewrite responses, account for that behavior before moving the public URL to a tunnel.

For more information, refer to [Tunnels](https://developers.cloudflare.com/sandbox/api/tunnels/) and [Expose services](https://developers.cloudflare.com/sandbox/guides/expose-services/).

## Default sessions

Set `enableDefaultSession: false` on `getSandbox()`. Operations without an explicit session will then run in isolation and will not inherit shell state from earlier calls.

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "user-123", {
	enableDefaultSession: false,
	transport: "rpc",
});
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "user-123", {
	enableDefaultSession: false,
	transport: "rpc",
});
```

If your code expects commands like `cd /workspace/app` to affect later `exec()` calls, create an explicit session and run related commands through that session:

```js
const buildSession = await sandbox.createSession({
	id: "build",
	cwd: "/workspace/app",
});

await buildSession.exec("npm install");
await buildSession.exec("npm test");
```

```ts
const buildSession = await sandbox.createSession({
	id: "build",
	cwd: "/workspace/app",
});

await buildSession.exec("npm install");
await buildSession.exec("npm test");
```

For one-off commands, pass `cwd` or `env` directly to `exec()` instead of relying on persisted shell state:

```js
await sandbox.exec("npm test", {
	cwd: "/workspace/app",
	env: {
		NODE_ENV: "test",
	},
});
```

```ts
await sandbox.exec("npm test", {
	cwd: "/workspace/app",
	env: {
		NODE_ENV: "test",
	},
});
```

For more information, refer to [Sandbox options](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/#enabledefaultsession) and [Sessions](https://developers.cloudflare.com/sandbox/api/sessions/).

## Streaming APIs

The Sandbox SDK is consolidating separate streaming APIs into the base `exec()`, `readFile()`, and `writeFile()` methods. Audit code that depends on stream-specific helpers and move to the base APIs where they support streaming behavior.

For command output, use `exec()` with streaming callbacks:

```js
await sandbox.exec("npm install", {
	stream: true,
	onOutput: (stream, data) => {
		console.log(`[${stream}] ${data}`);
	},
});
```

```ts
await sandbox.exec("npm install", {
	stream: true,
	onOutput: (stream, data) => {
		console.log(`[${stream}] ${data}`);
	},
});
```

For large or binary files, use the base file APIs with RPC transport. Pass a `ReadableStream` to `writeFile()`, or read a file as a stream with `encoding: "none"`:

```js
const request = await fetch("https://example.com/archive.tar.gz");

if (!request.body) {
	throw new Error("Expected archive response body");
}

await sandbox.writeFile("/workspace/archive.tar.gz", request.body);

const file = await sandbox.readFile("/workspace/archive.tar.gz", {
	encoding: "none",
});

return new Response(file.content, {
	headers: { "Content-Type": file.mimeType },
});
```

```ts
const request = await fetch("https://example.com/archive.tar.gz");

if (!request.body) {
	throw new Error("Expected archive response body");
}

await sandbox.writeFile("/workspace/archive.tar.gz", request.body);

const file = await sandbox.readFile("/workspace/archive.tar.gz", {
	encoding: "none",
});

return new Response(file.content, {
	headers: { "Content-Type": file.mimeType },
});
```

For more information, refer to [Commands](https://developers.cloudflare.com/sandbox/api/commands/) and [Files](https://developers.cloudflare.com/sandbox/api/files/).

## Verify the migration

Use this checklist before you depend on a Sandbox SDK release that has removed the deprecated APIs:

* RPC transport is configured with `SANDBOX_TRANSPORT=rpc` or `transport: "rpc"`.
* No `websocket` or `http` transport configuration remains.
* No `exposePort()` usage remains in the migrated path.
* `enableDefaultSession` is set to `false`.
* Stateful command workflows use `sandbox.createSession()`.
* One-off commands pass `cwd` and `env` directly.
* Streaming file and command code uses the base APIs.
* Your Worker has been deployed and smoke-tested.

## Coding agents

Coding agents with [Cloudflare Skills ↗](https://github.com/cloudflare/skills) installed ([Agent setup](https://developers.cloudflare.com/agent-setup/)) should use **`sandbox-stable`** for work on the current stable package and follow **this guide** for deprecated-API cleanup while staying on stable. For a full move to Sandbox SDK 1.0 (`@next`), use **`sandbox-migrate-to-next`** (and the [1.0 migrate guide](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)) instead.

## 1.0 preview

After you finish the stable-line changes in this guide, move on to the **Sandbox SDK 1.0** preview on `@cloudflare/sandbox@next` when you can. That preview is the path to the next stable major release.

Refer to [Sandbox SDK 1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/) and [Migrate to the 1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/).

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/2026-deprecation/#page","headline":"2026 deprecation migration guide · Cloudflare Sandbox SDK docs","description":"Migrate away from deprecated Sandbox SDK features on the current stable package.","url":"https://developers.cloudflare.com/sandbox/guides/2026-deprecation/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Start and manage long-running services and applications.
title: Run background processes
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Run background processes

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/background-processes/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

This guide shows you how to start, monitor, and manage long-running background processes in the sandbox.

Coming soon: Sandbox SDK 1.0

This page documents `startProcess` and related helpers on today's stable `@cloudflare/sandbox` package.

In the **1.0 preview** (`@next`), long-running work uses the same `exec(argv)` process handle as short commands. Refer to [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/) or [migrate to the preview](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/).

## When to use background processes

Use `startProcess()` instead of `exec()` when:

* **Running web servers** \- HTTP servers, APIs, WebSocket servers
* **Long-running services** \- Database servers, caches, message queues
* **Development servers** \- Hot-reloading dev servers, watch modes
* **Continuous monitoring** \- Log watchers, health checkers
* **Parallel execution** \- Multiple services running simultaneously

Note

For **one-time commands, builds, or scripts that complete and exit**, use `exec()` instead. See the [Execute commands guide](https://developers.cloudflare.com/sandbox/guides/execute-commands/).

## Start a background process

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Start a web server
const server = await sandbox.startProcess("python -m http.server 8000");

console.log("Server started");
console.log("Process ID:", server.id);
console.log("PID:", server.pid);
console.log("Status:", server.status); // 'running'

// Process runs in background - your code continues
```

```plaintext
import { getSandbox } from '@cloudflare/sandbox';

const sandbox = getSandbox(env.Sandbox, 'my-sandbox');

// Start a web server
const server = await sandbox.startProcess('python -m http.server 8000');

console.log('Server started');
console.log('Process ID:', server.id);
console.log('PID:', server.pid);
console.log('Status:', server.status); // 'running'

// Process runs in background - your code continues
```

## Configure process environment

Set working directory and environment variables:

```js
const process = await sandbox.startProcess("node server.js", {
	cwd: "/workspace/api",
	env: {
		NODE_ENV: "production",
		PORT: "8080",
		API_KEY: env.API_KEY,
		DATABASE_URL: env.DATABASE_URL,
	},
});

console.log("API server started");
```

```plaintext
const process = await sandbox.startProcess('node server.js', {
  cwd: '/workspace/api',
  env: {
    NODE_ENV: 'production',
    PORT: '8080',
    API_KEY: env.API_KEY,
    DATABASE_URL: env.DATABASE_URL
  }
});

console.log('API server started');
```

## Monitor process status

List and check running processes:

```js
const processes = await sandbox.listProcesses();

console.log(`Running ${processes.length} processes:`);

for (const proc of processes) {
	console.log(`${proc.id}: ${proc.command} (${proc.status})`);
}

// Check if specific process is running
const isRunning = processes.some(
	(p) => p.id === processId && p.status === "running",
);
```

```plaintext
const processes = await sandbox.listProcesses();

console.log(`Running ${processes.length} processes:`);

for (const proc of processes) {
  console.log(`${proc.id}: ${proc.command} (${proc.status})`);
}

// Check if specific process is running
const isRunning = processes.some(p => p.id === processId && p.status === 'running');
```

## Wait for process readiness

Wait for a process to be ready before proceeding:

```js
const server = await sandbox.startProcess("node server.js");

// Wait for server to respond on port 3000
await server.waitForPort(3000);

console.log("Server is ready");
```

```plaintext
const server = await sandbox.startProcess('node server.js');

// Wait for server to respond on port 3000
await server.waitForPort(3000);

console.log('Server is ready');
```

Or wait for specific log patterns:

```js
const server = await sandbox.startProcess("node server.js");

// Wait for log message
const result = await server.waitForLog("Server listening");
console.log("Server is ready:", result.line);
```

```plaintext
const server = await sandbox.startProcess('node server.js');

// Wait for log message
const result = await server.waitForLog('Server listening');
console.log('Server is ready:', result.line);
```

## Monitor process logs

Stream logs in real-time:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const server = await sandbox.startProcess("node server.js");

// Stream logs
const logStream = await sandbox.streamProcessLogs(server.id);

for await (const log of parseSSEStream(logStream)) {
	console.log(log.data);
}
```

```plaintext
import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox';

const server = await sandbox.startProcess('node server.js');

// Stream logs
const logStream = await sandbox.streamProcessLogs(server.id);

for await (const log of parseSSEStream<LogEvent>(logStream)) {
  console.log(log.data);
}
```

Or get accumulated logs:

```js
const logs = await sandbox.getProcessLogs(server.id);
console.log("Logs:", logs);
```

```plaintext
const logs = await sandbox.getProcessLogs(server.id);
console.log('Logs:', logs);
```

## Stop processes

Stop background processes and their children:

```js
// Stop specific process (terminates entire process tree)
await sandbox.killProcess(server.id);

// Force kill if needed
await sandbox.killProcess(server.id, "SIGKILL");

// Stop all processes
await sandbox.killAllProcesses();
```

```plaintext
// Stop specific process (terminates entire process tree)
await sandbox.killProcess(server.id);

// Force kill if needed
await sandbox.killProcess(server.id, 'SIGKILL');

// Stop all processes
await sandbox.killAllProcesses();
```

`killProcess()` terminates the specified process and all child processes it spawned. This ensures that processes running in the background do not leave orphaned child processes when terminated.

For example, if your process spawns multiple worker processes or background tasks, `killProcess()` will clean up the entire process tree:

```js
// This script spawns multiple child processes
const batch = await sandbox.startProcess(
	'bash -c "process1 & process2 & process3 & wait"',
);

// killProcess() terminates the bash process AND all three child processes
await sandbox.killProcess(batch.id);
```

```plaintext
// This script spawns multiple child processes
const batch = await sandbox.startProcess(
  'bash -c "process1 & process2 & process3 & wait"'
);

// killProcess() terminates the bash process AND all three child processes
await sandbox.killProcess(batch.id);
```

## Run multiple processes

Start services in sequence, waiting for dependencies:

```js
// Start database first
const db = await sandbox.startProcess("redis-server");

// Wait for database to be ready
await db.waitForPort(6379, { mode: "tcp" });

// Now start API server (depends on database)
const api = await sandbox.startProcess("node api-server.js", {
	env: { DATABASE_URL: "redis://localhost:6379" },
});

// Wait for API to be ready
await api.waitForPort(8080, { path: "/health" });

console.log("All services running");
```

```plaintext
// Start database first
const db = await sandbox.startProcess('redis-server');

// Wait for database to be ready
await db.waitForPort(6379, { mode: 'tcp' });

// Now start API server (depends on database)
const api = await sandbox.startProcess('node api-server.js', {
  env: { DATABASE_URL: 'redis://localhost:6379' }
});

// Wait for API to be ready
await api.waitForPort(8080, { path: '/health' });

console.log('All services running');
```

## Keep containers alive for long-running processes

By default, containers automatically shut down after 10 minutes of inactivity. For long-running processes that may have idle periods (like CI/CD pipelines, batch jobs, or monitoring tasks), use the [keepAlive option](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/#keepalive):

```js
import { getSandbox, parseSSEStream } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		// Enable keepAlive for long-running processes
		const sandbox = getSandbox(env.Sandbox, "build-job-123", {
			keepAlive: true,
		});

		try {
			// Start a long-running build process
			const build = await sandbox.startProcess("npm run build:production");

			// Monitor progress
			const logs = await sandbox.streamProcessLogs(build.id);

			// Process can run indefinitely without container shutdown
			for await (const log of parseSSEStream(logs)) {
				console.log(log.data);
				if (log.data.includes("Build complete")) {
					break;
				}
			}

			return new Response("Build completed");
		} finally {
			// Important: Must explicitly destroy when done
			await sandbox.destroy();
		}
	},
};
```

```ts
import { getSandbox, parseSSEStream, type LogEvent } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Enable keepAlive for long-running processes
    const sandbox = getSandbox(env.Sandbox, 'build-job-123', {
      keepAlive: true
    });

    try {
      // Start a long-running build process
      const build = await sandbox.startProcess('npm run build:production');

      // Monitor progress
      const logs = await sandbox.streamProcessLogs(build.id);

      // Process can run indefinitely without container shutdown
      for await (const log of parseSSEStream<LogEvent>(logs)) {
        console.log(log.data);
        if (log.data.includes('Build complete')) {
          break;
        }
      }

      return new Response('Build completed');
    } finally {
      // Important: Must explicitly destroy when done
      await sandbox.destroy();
    }
  }
};
```

Always destroy with keepAlive

When using `keepAlive: true`, containers will not automatically timeout. You **must** call `sandbox.destroy()` when finished to prevent containers running indefinitely and counting toward your account limits.

## Best practices

* **Wait for readiness** \- Use `waitForPort()` or `waitForLog()` to detect when services are ready
* **Clean up** \- Always stop processes when done
* **Handle failures** \- Monitor logs for errors and restart if needed
* **Use try/finally** \- Ensure cleanup happens even on errors
* **Use `keepAlive` for long-running tasks** \- Prevent container shutdown during processes with idle periods

## Troubleshooting

### Process exits immediately

Check logs to see why:

```js
const process = await sandbox.startProcess("node server.js");
await new Promise((resolve) => setTimeout(resolve, 1000));

const processes = await sandbox.listProcesses();
if (!processes.find((p) => p.id === process.id)) {
	const logs = await sandbox.getProcessLogs(process.id);
	console.error("Process exited:", logs);
}
```

```plaintext
const process = await sandbox.startProcess('node server.js');
await new Promise(resolve => setTimeout(resolve, 1000));

const processes = await sandbox.listProcesses();
if (!processes.find(p => p.id === process.id)) {
  const logs = await sandbox.getProcessLogs(process.id);
  console.error('Process exited:', logs);
}
```

### Port already in use

Kill existing processes before starting:

```js
await sandbox.killAllProcesses();
const server = await sandbox.startProcess("node server.js");
```

```plaintext
await sandbox.killAllProcesses();
const server = await sandbox.startProcess('node server.js');
```

## Related resources

* [Commands API reference](https://developers.cloudflare.com/sandbox/api/commands/) \- Complete process management API
* [Sandbox options configuration](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/) \- Configure `keepAlive` and other options
* [Lifecycle API](https://developers.cloudflare.com/sandbox/api/lifecycle/) \- Create and manage sandboxes
* [Sessions API reference](https://developers.cloudflare.com/sandbox/api/sessions/) \- Create isolated execution contexts
* [Execute commands guide](https://developers.cloudflare.com/sandbox/guides/execute-commands/) \- One-time command execution
* [Expose services guide](https://developers.cloudflare.com/sandbox/guides/expose-services/) \- Make processes accessible
* [Streaming output guide](https://developers.cloudflare.com/sandbox/guides/streaming-output/) \- Monitor process output

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/background-processes/#page","headline":"Run background processes · Cloudflare Sandbox SDK docs","description":"Start and manage long-running services and applications.","url":"https://developers.cloudflare.com/sandbox/guides/background-processes/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Snapshot a sandbox directory to R2 and restore it later.
title: Backup and restore
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Backup and restore

Last updated Sep 1, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/backup-restore/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

This guide shows you how to snapshot a sandbox directory to R2 and restore it later.

Use backup and restore when a project directory such as `/workspace` should come back after the sandbox sleeps. For a separate persisted storage path, mount a bucket instead. If you mount a bucket over `/workspace`, the mount overlays files seeded by your image in production.

For why production restore uses an overlay, refer to [Directory backups](https://developers.cloudflare.com/sandbox/concepts/backup-restore/).

## Prerequisites

1. Create an R2 bucket:  
```sh  
npx wrangler r2 bucket create my-backup-bucket  
```
2. Add the `BACKUP_BUCKET` R2 binding and presigned URL settings to your Wrangler configuration:  
```jsonc  
{  
	"name": "my-sandbox-worker",  
	"main": "src/index.ts",  
	// Set this to today's date  
	"compatibility_date": "2026-09-12",  
	"compatibility_flags": ["nodejs_compat"],  
	"containers": [  
		{  
			"class_name": "Sandbox",  
			"image": "./Dockerfile",  
		},  
	],  
	"durable_objects": {  
		"bindings": [  
			{  
				"class_name": "Sandbox",  
				"name": "Sandbox",  
			},  
		],  
	},  
	"migrations": [  
		{  
			"new_sqlite_classes": ["Sandbox"],  
			"tag": "v1",  
		},  
	],  
	"vars": {  
		"BACKUP_BUCKET_NAME": "my-backup-bucket",  
		"CLOUDFLARE_ACCOUNT_ID": "<YOUR_ACCOUNT_ID>",  
	},  
	"r2_buckets": [  
		{  
			"binding": "BACKUP_BUCKET",  
			"bucket_name": "my-backup-bucket",  
		},  
	],  
}  
```  
```toml  
name = "my-sandbox-worker"  
main = "src/index.ts"  
# Set this to today's date  
compatibility_date = "2026-09-12"  
compatibility_flags = [ "nodejs_compat" ]  
[[containers]]  
class_name = "Sandbox"  
image = "./Dockerfile"  
[[durable_objects.bindings]]  
class_name = "Sandbox"  
name = "Sandbox"  
[[migrations]]  
new_sqlite_classes = [ "Sandbox" ]  
tag = "v1"  
[vars]  
BACKUP_BUCKET_NAME = "my-backup-bucket"  
CLOUDFLARE_ACCOUNT_ID = "<YOUR_ACCOUNT_ID>"  
[[r2_buckets]]  
binding = "BACKUP_BUCKET"  
bucket_name = "my-backup-bucket"  
```  
If the bucket uses a jurisdiction-specific endpoint, add `BACKUP_BUCKET_ENDPOINT` to `vars`. For an EU bucket, use `https://<ACCOUNT_ID>.eu.r2.cloudflarestorage.com`.
3. Store R2 API credentials as secrets:  
```sh  
npx wrangler secret put R2_ACCESS_KEY_ID  
npx wrangler secret put R2_SECRET_ACCESS_KEY  
```  
Create the token in the [Cloudflare dashboard ↗](https://dash.cloudflare.com/) under **R2** \> **Overview** \> **Manage R2 API Tokens**. Grant **Object Read & Write** on the backup bucket.

Note

The `vars` and API secrets in steps 2 and 3 are required for production. For `wrangler dev`, only the `BACKUP_BUCKET` binding is required. Refer to [Use backup and restore in local development](#use-backup-and-restore-in-local-development).

## Create a backup

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const backup = await sandbox.createBackup({ dir: "/workspace" });
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const backup = await sandbox.createBackup({ dir: "/workspace" });
```

The directory must be an absolute path under `/workspace`, `/home`, `/tmp`, `/var/tmp`, or `/app`.

## Restore a backup

Stop processes that write to the target directory, then restore:

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const backup = await sandbox.createBackup({ dir: "/workspace" });
const result = await sandbox.restoreBackup(backup);
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const backup = await sandbox.createBackup({ dir: "/workspace" });
const result = await sandbox.restoreBackup(backup);
```

In production, restore mounts a copy-on-write overlay. The mount is lost when the sandbox sleeps or the container restarts. Restore again from the stored handle.

The restore target is `backup.dir`. You can point that field at a different allowed directory than the one you originally backed up.

## Exclude generated caches

After a production restore, renaming a directory inside the restored tree can fail with `EXDEV` (`cross-device link not permitted`). Omit disposable generated directories from the backup, or delete them after restore. Vite's cache is one such directory:

```js
const backup = await sandbox.createBackup({
	dir: "/workspace/app",
	excludes: ["node_modules/.vite"],
});
```

```ts
const backup = await sandbox.createBackup({
	dir: "/workspace/app",
	excludes: ["node_modules/.vite"],
});
```

```js
await sandbox.restoreBackup(backup);
await sandbox.exec("rm -rf /workspace/app/node_modules/.vite");
```

```ts
await sandbox.restoreBackup(backup);
await sandbox.exec("rm -rf /workspace/app/node_modules/.vite");
```

This failure does not occur in `wrangler dev`, which extracts the archive. For overlay restore, refer to [Directory backups](https://developers.cloudflare.com/sandbox/concepts/backup-restore/).

## Exclude gitignored files

To skip `.gitignore` matches such as `node_modules/` or `dist/` in a git repository:

```js
const backup = await sandbox.createBackup({
	dir: "/workspace",
	gitignore: true,
});
```

```ts
const backup = await sandbox.createBackup({
	dir: "/workspace",
	gitignore: true,
});
```

If the directory is not inside a git repository, `gitignore` has no effect. If `git` is not installed in the container, the SDK logs a warning and continues without git-based exclusions. Nested `.gitignore` files apply.

## Checkpoint and roll back

```js
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const checkpoint = await sandbox.createBackup({ dir: "/workspace" });

try {
	await sandbox.exec("npm install some-experimental-package");
	await sandbox.exec("npm run build");
} catch (error) {
	await sandbox.restoreBackup(checkpoint);
}
```

```ts
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const checkpoint = await sandbox.createBackup({ dir: "/workspace" });

try {
	await sandbox.exec("npm install some-experimental-package");
	await sandbox.exec("npm run build");
} catch (error) {
	await sandbox.restoreBackup(checkpoint);
}
```

## Store backup handles

`DirectoryBackup` is serializable. Persist it to KV, D1, or Durable Object storage:

```js
const backup = await sandbox.createBackup({
	dir: "/workspace",
	name: "deploy-v2",
	ttl: 604800, // 7 days
});

await env.KV.put(`backup:${userId}`, JSON.stringify(backup));

const stored = await env.KV.get(`backup:${userId}`);
if (stored) {
	await sandbox.restoreBackup(JSON.parse(stored));
}
```

```ts
const backup = await sandbox.createBackup({
	dir: "/workspace",
	name: "deploy-v2",
	ttl: 604800, // 7 days
});

await env.KV.put(`backup:${userId}`, JSON.stringify(backup));

const stored = await env.KV.get(`backup:${userId}`);
if (stored) {
	await sandbox.restoreBackup(JSON.parse(stored));
}
```

## Set a name and TTL

Names can be up to 256 characters. The default TTL is 3 days (`259200` seconds). The SDK rejects an expired backup at restore time. It does not delete the R2 objects.

```js
const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const shortBackup = await sandbox.createBackup({
	dir: "/workspace",
	ttl: 600, // 10 minutes
});

const longBackup = await sandbox.createBackup({
	dir: "/workspace",
	name: "daily-snapshot",
	ttl: 604800, // 7 days
});
```

```ts
const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const shortBackup = await sandbox.createBackup({
	dir: "/workspace",
	ttl: 600, // 10 minutes
});

const longBackup = await sandbox.createBackup({
	dir: "/workspace",
	name: "daily-snapshot",
	ttl: 604800, // 7 days
});
```

To delete expired objects automatically, add an [R2 object lifecycle rule](https://developers.cloudflare.com/r2/buckets/object-lifecycles/) on the `backups/` prefix. If your longest TTL is 7 days, expire objects older than 7 days.

## Clean up backup objects

Archives live at `backups/{backupId}/data.sqsh` and `backups/{backupId}/meta.json`.

### Replace the latest backup

```js
if (previousBackup) {
	await env.BACKUP_BUCKET.delete([
		`backups/${previousBackup.id}/data.sqsh`,
		`backups/${previousBackup.id}/meta.json`,
	]);
}

const backup = await sandbox.createBackup({
	dir: "/workspace",
	name: "latest",
});
await env.KV.put("latest-backup", JSON.stringify(backup));
```

```ts
if (previousBackup) {
	await env.BACKUP_BUCKET.delete([
		`backups/${previousBackup.id}/data.sqsh`,
		`backups/${previousBackup.id}/meta.json`,
	]);
}

const backup = await sandbox.createBackup({
	dir: "/workspace",
	name: "latest",
});
await env.KV.put("latest-backup", JSON.stringify(backup));
```

### Delete a backup by ID

```js
await env.BACKUP_BUCKET.delete([
	`backups/${backup.id}/data.sqsh`,
	`backups/${backup.id}/meta.json`,
]);
```

```ts
await env.BACKUP_BUCKET.delete([
	`backups/${backup.id}/data.sqsh`,
	`backups/${backup.id}/meta.json`,
]);
```

### Delete backups by age

List objects under `backups/` and delete by upload time:

```js
const listed = await env.BACKUP_BUCKET.list({ prefix: "backups/" });
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;

for (const object of listed.objects) {
	const ageMs = Date.now() - object.uploaded.getTime();
	if (ageMs > sevenDaysMs) {
		await env.BACKUP_BUCKET.delete(object.key);
	}
}
```

```ts
const listed = await env.BACKUP_BUCKET.list({ prefix: "backups/" });
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;

for (const object of listed.objects) {
	const ageMs = Date.now() - object.uploaded.getTime();
	if (ageMs > sevenDaysMs) {
		await env.BACKUP_BUCKET.delete(object.key);
	}
}
```

## Use backup and restore in local development

Pass `localBucket: true` so `wrangler dev` uses the `BACKUP_BUCKET` binding. Presigned URL credentials are not required.

```js
const backup = await sandbox.createBackup({
	dir: "/workspace",
	localBucket: Boolean(env.LOCAL_DEV),
});

const result = await sandbox.restoreBackup(backup);
```

```ts
const backup = await sandbox.createBackup({
	dir: "/workspace",
	localBucket: Boolean(env.LOCAL_DEV),
});

const result = await sandbox.restoreBackup(backup);
```

Local restore extracts the archive with `unsquashfs` and replaces the directory. The stored handle's `localBucket` field selects the restore path.

## Fix path permissions

`createBackup()` must read every file under the target directory. Files with mode `0600` or directories owned by another user cause `BackupCreateError`.

Set permissions in the image when you can. `a+rX` adds read permission on files and execute permission on directories:

```dockerfile
RUN mkdir -p /home/sandbox && chmod -R a+rX /home/sandbox
```

If a process creates restrictive files at runtime, fix them before the backup:

```ts
await sandbox.exec("chmod -R a+rX /home/sandbox/.claude");
const backup = await sandbox.createBackup({ dir: "/home/sandbox" });
```

## Handle errors

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

try {
	const backup = await sandbox.createBackup({ dir: "/workspace" });
} catch (error) {
	if (error.code === "INVALID_BACKUP_CONFIG") {
		console.error("Configuration error:", error.message);
	} else if (error.code === "BACKUP_CREATE_FAILED") {
		console.error("Backup failed:", error.message);
	}
}

try {
	await sandbox.restoreBackup(backup);
} catch (error) {
	if (error.code === "BACKUP_NOT_FOUND") {
		console.error("Backup not found in R2:", error.message);
	} else if (error.code === "BACKUP_EXPIRED") {
		console.error("Backup TTL has elapsed:", error.message);
	} else if (error.code === "BACKUP_RESTORE_FAILED") {
		console.error("Restore failed:", error.message);
	}
}
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

try {
	const backup = await sandbox.createBackup({ dir: "/workspace" });
} catch (error) {
	if (error.code === "INVALID_BACKUP_CONFIG") {
		console.error("Configuration error:", error.message);
	} else if (error.code === "BACKUP_CREATE_FAILED") {
		console.error("Backup failed:", error.message);
	}
}

try {
	await sandbox.restoreBackup(backup);
} catch (error) {
	if (error.code === "BACKUP_NOT_FOUND") {
		console.error("Backup not found in R2:", error.message);
	} else if (error.code === "BACKUP_EXPIRED") {
		console.error("Backup TTL has elapsed:", error.message);
	} else if (error.code === "BACKUP_RESTORE_FAILED") {
		console.error("Restore failed:", error.message);
	}
}
```

## Related resources

* [Directory backups](https://developers.cloudflare.com/sandbox/concepts/backup-restore/) \- Overlay restore, local extract, and `EXDEV`
* [Backups API](https://developers.cloudflare.com/sandbox/api/backups/) \- Methods, options, and types
* [Storage API](https://developers.cloudflare.com/sandbox/api/storage/) \- Mount S3-compatible buckets
* [R2 documentation](https://developers.cloudflare.com/r2/) \- R2 buckets and credentials
* [R2 lifecycle rules](https://developers.cloudflare.com/r2/buckets/object-lifecycles/) \- Automatic object cleanup

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/backup-restore/#page","headline":"Backup and restore · Cloudflare Sandbox SDK docs","description":"Snapshot a sandbox directory to R2 and restore it later.","url":"https://developers.cloudflare.com/sandbox/guides/backup-restore/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-09-01","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Connect browser-based terminals to sandbox shells using xterm.js or raw WebSockets.
title: Browser terminals
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Browser terminals

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/browser-terminals/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

This guide shows you how to connect a browser-based terminal to a sandbox shell. You can use the `SandboxAddon` with xterm.js, or connect directly over WebSockets.

Sandbox SDK 1.0 preview

This guide documents browser terminals on today's stable `@cloudflare/sandbox` package.

On **`@cloudflare/sandbox@next`**, follow [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/) and [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/).

## Prerequisites

You need an existing Cloudflare Worker with a sandbox binding. Refer to [Getting started](https://developers.cloudflare.com/sandbox/get-started/) if you do not have one.

Install the terminal dependencies in your frontend project:

npmyarnpnpmbun

```
npm install @xterm/xterm @xterm/addon-fit @cloudflare/sandbox
```

```
yarn install @xterm/xterm @xterm/addon-fit @cloudflare/sandbox
```

```
pnpm install @xterm/xterm @xterm/addon-fit @cloudflare/sandbox
```

```
bun install @xterm/xterm @xterm/addon-fit @cloudflare/sandbox
```

If you are not using xterm.js, you only need `@cloudflare/sandbox` for types.

## Handle WebSocket upgrades in the Worker

Add a route that proxies WebSocket connections to the sandbox terminal. The example below supports both the default session and named sessions via a query parameter:

```js
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		if (
			url.pathname === "/ws/terminal" &&
			request.headers.get("Upgrade") === "websocket"
		) {
			const sandbox = getSandbox(env.Sandbox, "my-sandbox");
			const sessionId = url.searchParams.get("session");

			if (sessionId) {
				const session = await sandbox.getSession(sessionId);
				return await session.terminal(request);
			}

			return await sandbox.terminal(request, { cols: 80, rows: 24 });
		}

		return new Response("Not found", { status: 404 });
	},
};
```

```ts
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === '/ws/terminal' && request.headers.get('Upgrade') === 'websocket') {
      const sandbox = getSandbox(env.Sandbox, 'my-sandbox');
      const sessionId = url.searchParams.get('session');

      if (sessionId) {
        const session = await sandbox.getSession(sessionId);
        return await session.terminal(request);
      }

      return await sandbox.terminal(request, { cols: 80, rows: 24 });
    }

    return new Response('Not found', { status: 404 });
  }
};
```

## Connect with xterm.js and SandboxAddon

Create the terminal in your browser code and attach the `SandboxAddon`. The addon manages the WebSocket connection, automatic reconnection, and resize forwarding.

```js
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { SandboxAddon } from "@cloudflare/sandbox/xterm";
import "@xterm/xterm/css/xterm.css";

const terminal = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);

const addon = new SandboxAddon({
	getWebSocketUrl: ({ sandboxId, sessionId, origin }) => {
		const params = new URLSearchParams({ id: sandboxId });
		if (sessionId) params.set("session", sessionId);
		return `${origin}/ws/terminal?${params}`;
	},
	onStateChange: (state, error) => {
		console.log(`Terminal ${state}`, error ?? "");
	},
});

terminal.loadAddon(addon);
terminal.open(document.getElementById("terminal"));
fitAddon.fit();

// Connect to the default session
addon.connect({ sandboxId: "my-sandbox" });

// Or connect to a specific session
// addon.connect({ sandboxId: 'my-sandbox', sessionId: 'development' });

window.addEventListener("resize", () => fitAddon.fit());
```

```ts
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { SandboxAddon } from '@cloudflare/sandbox/xterm';
import '@xterm/xterm/css/xterm.css';

const terminal = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);

const addon = new SandboxAddon({
  getWebSocketUrl: ({ sandboxId, sessionId, origin }) => {
    const params = new URLSearchParams({ id: sandboxId });
    if (sessionId) params.set('session', sessionId);
    return `${origin}/ws/terminal?${params}`;
  },
  onStateChange: (state, error) => {
    console.log(`Terminal ${state}`, error ?? '');
  }
});

terminal.loadAddon(addon);
terminal.open(document.getElementById('terminal'));
fitAddon.fit();

// Connect to the default session
addon.connect({ sandboxId: 'my-sandbox' });

// Or connect to a specific session
// addon.connect({ sandboxId: 'my-sandbox', sessionId: 'development' });

window.addEventListener('resize', () => fitAddon.fit());
```

For the full addon API, refer to the [Terminal API reference](https://developers.cloudflare.com/sandbox/api/terminal/).

## Connect without xterm.js

If you are building a custom terminal UI or running in an environment without xterm.js, connect directly over WebSockets. The protocol uses binary frames for terminal data and JSON text frames for control messages.

```js
const ws = new WebSocket("wss://example.com/ws/terminal?id=my-sandbox");
ws.binaryType = "arraybuffer";

const decoder = new TextDecoder();
const encoder = new TextEncoder();

ws.addEventListener("message", (event) => {
	if (event.data instanceof ArrayBuffer) {
		// Terminal output (binary) — includes ANSI escape sequences
		const text = decoder.decode(event.data);
		appendToDisplay(text);
		return;
	}

	// Control message (JSON text)
	const msg = JSON.parse(event.data);

	switch (msg.type) {
		case "ready":
			// Terminal is accepting input — send initial resize
			ws.send(JSON.stringify({ type: "resize", cols: 80, rows: 24 }));
			break;

		case "exit":
			console.log(`Shell exited: code ${msg.code}`);
			break;

		case "error":
			console.error("Terminal error:", msg.message);
			break;
	}
});

// Send keystrokes as binary
function sendInput(text) {
	if (ws.readyState === WebSocket.OPEN) {
		ws.send(encoder.encode(text));
	}
}
```

```ts
const ws = new WebSocket('wss://example.com/ws/terminal?id=my-sandbox');
ws.binaryType = 'arraybuffer';

const decoder = new TextDecoder();
const encoder = new TextEncoder();

ws.addEventListener('message', (event) => {
  if (event.data instanceof ArrayBuffer) {
    // Terminal output (binary) — includes ANSI escape sequences
    const text = decoder.decode(event.data);
    appendToDisplay(text);
    return;
  }

  // Control message (JSON text)
  const msg = JSON.parse(event.data);

  switch (msg.type) {
    case 'ready':
      // Terminal is accepting input — send initial resize
      ws.send(JSON.stringify({ type: 'resize', cols: 80, rows: 24 }));
      break;

    case 'exit':
      console.log(`Shell exited: code ${msg.code}`);
      break;

    case 'error':
      console.error('Terminal error:', msg.message);
      break;
  }
});

// Send keystrokes as binary
function sendInput(text: string): void {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(encoder.encode(text));
  }
}
```

Key protocol details:

* Set `binaryType` to `arraybuffer` before connecting.
* Buffered output from a previous connection arrives as binary frames before the `ready` message.
* Send keystrokes as binary (UTF-8). Send control messages (`resize`) as JSON text.
* The PTY stays alive when a client disconnects. Reconnecting replays buffered output.

For the full protocol specification, refer to the [WebSocket protocol section](https://developers.cloudflare.com/sandbox/api/terminal/#websocket-protocol) in the API reference.

## Best practices

* **Always use FitAddon** — Without it, terminal dimensions do not match the container and text wraps incorrectly.
* **Handle resize events** — Call `fitAddon.fit()` on window resize so the terminal and PTY stay in sync.
* **Clean up on unmount** — Call `addon.disconnect()` when removing the terminal from the page.
* **Scope terminals to a user sandbox** — Use sessions for multiple terminal contexts in the same workspace. Use separate sandboxes for separate users.

## Related resources

* [Terminal API reference](https://developers.cloudflare.com/sandbox/api/terminal/) — Method signatures, addon API, and WebSocket protocol
* [Terminal connections](https://developers.cloudflare.com/sandbox/concepts/terminal/) — How terminal connections work
* [Session management](https://developers.cloudflare.com/sandbox/concepts/sessions/) — How sessions work

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/browser-terminals/#page","headline":"Browser terminals · Cloudflare Sandbox SDK docs","description":"Connect browser-based terminals to sandbox shells using xterm.js or raw WebSockets.","url":"https://developers.cloudflare.com/sandbox/guides/browser-terminals/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Execute Python and JavaScript code with rich outputs.
title: Use code interpreter
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Use code interpreter

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/code-execution/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

This guide shows you how to execute Python and JavaScript code with rich outputs using the Code Interpreter API.

Coming soon: Sandbox SDK 1.0

This page documents the interpreter on today's stable `@cloudflare/sandbox` package.

In the **1.0 preview** (`@next`), the interpreter is an opt-in extension. Refer to [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/) and the [Interpreter API](https://developers.cloudflare.com/sandbox/1-0-preview/api/interpreter/).

## When to use code interpreter

Use the Code Interpreter API for **simple, direct code execution** with minimal setup:

* **Quick code execution** \- Run Python/JS code without environment setup
* **Rich outputs** \- Get charts, tables, images, HTML automatically
* **AI-generated code** \- Execute LLM-generated code with structured results
* **Persistent state** \- Variables preserved between executions in the same context

Use `exec()` for **advanced or custom workflows**:

* **System operations** \- Install packages, manage files, run builds
* **Custom environments** \- Configure specific versions, dependencies
* **Shell commands** \- Git operations, system utilities, complex pipelines
* **Long-running processes** \- Background services, servers

## Create an execution context

Code contexts maintain state between executions:

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Create a Python context
const pythonContext = await sandbox.createCodeContext({
	language: "python",
});

console.log("Context ID:", pythonContext.id);
console.log("Language:", pythonContext.language);

// Create a JavaScript context
const jsContext = await sandbox.createCodeContext({
	language: "javascript",
});
```

```plaintext
import { getSandbox } from '@cloudflare/sandbox';

const sandbox = getSandbox(env.Sandbox, 'my-sandbox');

// Create a Python context
const pythonContext = await sandbox.createCodeContext({
  language: 'python'
});

console.log('Context ID:', pythonContext.id);
console.log('Language:', pythonContext.language);

// Create a JavaScript context
const jsContext = await sandbox.createCodeContext({
  language: 'javascript'
});
```

## Execute code

### Simple execution

```js
// Create context
const context = await sandbox.createCodeContext({
	language: "python",
});

// Execute code
const result = await sandbox.runCode(
	`
print("Hello from Code Interpreter!")
result = 2 + 2
print(f"2 + 2 = {result}")
`,
	{ context: context.id },
);

console.log("Output:", result.output);
console.log("Success:", result.success);
```

```plaintext
// Create context
const context = await sandbox.createCodeContext({
  language: 'python'
});

// Execute code
const result = await sandbox.runCode(`
print("Hello from Code Interpreter!")
result = 2 + 2
print(f"2 + 2 = {result}")
`, { context: context.id });

console.log('Output:', result.output);
console.log('Success:', result.success);
```

### State within a context

Variables and imports remain available between executions in the same context, as long as the container stays active:

```js
const context = await sandbox.createCodeContext({
	language: "python",
});

// First execution - import and define variables
await sandbox.runCode(
	`
import pandas as pd
import numpy as np

data = [1, 2, 3, 4, 5]
print("Data initialized")
`,
	{ context: context.id },
);

// Second execution - use previously defined variables
const result = await sandbox.runCode(
	`
mean = np.mean(data)
print(f"Mean: {mean}")
`,
	{ context: context.id },
);

console.log(result.output); // "Mean: 3.0"
```

```plaintext
const context = await sandbox.createCodeContext({
  language: 'python'
});

// First execution - import and define variables
await sandbox.runCode(`
import pandas as pd
import numpy as np

data = [1, 2, 3, 4, 5]
print("Data initialized")
`, { context: context.id });

// Second execution - use previously defined variables
const result = await sandbox.runCode(`
mean = np.mean(data)
print(f"Mean: {mean}")
`, { context: context.id });

console.log(result.output); // "Mean: 3.0"
```

Note

Context state is lost if the container restarts due to inactivity. For critical data, store results outside the sandbox or design your code to reinitialize as needed.

## Handle rich outputs

The code interpreter returns multiple output formats:

```js
const result = await sandbox.runCode(
	`
import matplotlib.pyplot as plt

plt.plot([1, 2, 3], [1, 4, 9])
plt.title('Simple Chart')
plt.show()
`,
	{ context: context.id },
);

// Check available formats
console.log("Formats:", result.formats); // ['text', 'png']

// Access outputs
if (result.outputs.png) {
	// Return as image
	return new Response(atob(result.outputs.png), {
		headers: { "Content-Type": "image/png" },
	});
}

if (result.outputs.html) {
	// Return as HTML (pandas DataFrames)
	return new Response(result.outputs.html, {
		headers: { "Content-Type": "text/html" },
	});
}

if (result.outputs.json) {
	// Return as JSON
	return Response.json(result.outputs.json);
}
```

```plaintext
const result = await sandbox.runCode(`
import matplotlib.pyplot as plt

plt.plot([1, 2, 3], [1, 4, 9])
plt.title('Simple Chart')
plt.show()
`, { context: context.id });

// Check available formats
console.log('Formats:', result.formats);  // ['text', 'png']

// Access outputs
if (result.outputs.png) {
  // Return as image
  return new Response(atob(result.outputs.png), {
    headers: { 'Content-Type': 'image/png' }
  });
}

if (result.outputs.html) {
  // Return as HTML (pandas DataFrames)
  return new Response(result.outputs.html, {
    headers: { 'Content-Type': 'text/html' }
  });
}

if (result.outputs.json) {
  // Return as JSON
  return Response.json(result.outputs.json);
}
```

## Stream execution output

For long-running code, stream output in real-time:

```js
const context = await sandbox.createCodeContext({
	language: "python",
});

const result = await sandbox.runCode(
	`
import time

for i in range(10):
    print(f"Processing item {i+1}/10...")
    time.sleep(0.5)

print("Done!")
`,
	{
		context: context.id,
		stream: true,
		onOutput: (data) => {
			console.log("Output:", data);
		},
		onResult: (result) => {
			console.log("Result:", result);
		},
		onError: (error) => {
			console.error("Error:", error);
		},
	},
);
```

```plaintext
const context = await sandbox.createCodeContext({
  language: 'python'
});

const result = await sandbox.runCode(
  `
import time

for i in range(10):
    print(f"Processing item {i+1}/10...")
    time.sleep(0.5)

print("Done!")
`,
  {
    context: context.id,
    stream: true,
    onOutput: (data) => {
      console.log('Output:', data);
    },
    onResult: (result) => {
      console.log('Result:', result);
    },
    onError: (error) => {
      console.error('Error:', error);
    }
  }
);
```

## Execute AI-generated code

Run LLM-generated code safely in a sandbox:

```js
// 1. Generate code with Claude
const response = await fetch("https://api.anthropic.com/v1/messages", {
	method: "POST",
	headers: {
		"Content-Type": "application/json",
		"x-api-key": env.ANTHROPIC_API_KEY,
		"anthropic-version": "2023-06-01",
	},
	body: JSON.stringify({
		model: "claude-3-5-sonnet-20241022",
		max_tokens: 1024,
		messages: [
			{
				role: "user",
				content: "Write Python code to calculate fibonacci sequence up to 100",
			},
		],
	}),
});

const { content } = await response.json();
const code = content[0].text;

// 2. Execute in sandbox
const context = await sandbox.createCodeContext({ language: "python" });
const result = await sandbox.runCode(code, { context: context.id });

console.log("Generated code:", code);
console.log("Output:", result.output);
console.log("Success:", result.success);
```

```plaintext
// 1. Generate code with Claude
const response = await fetch('https://api.anthropic.com/v1/messages', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': env.ANTHROPIC_API_KEY,
    'anthropic-version': '2023-06-01'
  },
  body: JSON.stringify({
    model: 'claude-3-5-sonnet-20241022',
    max_tokens: 1024,
    messages: [{
      role: 'user',
      content: 'Write Python code to calculate fibonacci sequence up to 100'
    }]
  })
});

const { content } = await response.json();
const code = content[0].text;

// 2. Execute in sandbox
const context = await sandbox.createCodeContext({ language: 'python' });
const result = await sandbox.runCode(code, { context: context.id });

console.log('Generated code:', code);
console.log('Output:', result.output);
console.log('Success:', result.success);
```

## Manage contexts

### List all contexts

```js
const contexts = await sandbox.listCodeContexts();

console.log(`${contexts.length} active contexts:`);

for (const ctx of contexts) {
	console.log(`  ${ctx.id} (${ctx.language})`);
}
```

```plaintext
const contexts = await sandbox.listCodeContexts();

console.log(`${contexts.length} active contexts:`);

for (const ctx of contexts) {
  console.log(`  ${ctx.id} (${ctx.language})`);
}
```

### Delete contexts

```js
// Delete specific context
await sandbox.deleteCodeContext(context.id);
console.log("Context deleted");

// Clean up all contexts
const contexts = await sandbox.listCodeContexts();
for (const ctx of contexts) {
	await sandbox.deleteCodeContext(ctx.id);
}
console.log("All contexts deleted");
```

```plaintext
// Delete specific context
await sandbox.deleteCodeContext(context.id);
console.log('Context deleted');

// Clean up all contexts
const contexts = await sandbox.listCodeContexts();
for (const ctx of contexts) {
  await sandbox.deleteCodeContext(ctx.id);
}
console.log('All contexts deleted');
```

## Best practices

* **Clean up contexts** \- Delete contexts when done to free resources
* **Handle errors** \- Always check `result.success` and `result.error`
* **Stream long operations** \- Use streaming for code that takes >2 seconds
* **Validate AI code** \- Review generated code before execution

## Related resources

* [Code Interpreter API reference](https://developers.cloudflare.com/sandbox/api/interpreter/) \- Complete API documentation
* [AI code executor tutorial](https://developers.cloudflare.com/sandbox/tutorials/ai-code-executor/) \- Build complete AI executor
* [Execute commands guide](https://developers.cloudflare.com/sandbox/guides/execute-commands/) \- Lower-level command execution

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/code-execution/#page","headline":"Use code interpreter · Cloudflare Sandbox SDK docs","description":"Execute Python and JavaScript code with rich outputs.","url":"https://developers.cloudflare.com/sandbox/guides/code-execution/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Deploy a Sandbox Worker and keep the npm package and container image on the same release line.
title: Deploy a Sandbox application
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Deploy a Sandbox application

Last updated Aug 28, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/deploy/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Sandbox runs on [Containers](https://developers.cloudflare.com/containers/). For deploy commands, Workers Builds, and rollout flags, refer to [Deploy Containers](https://developers.cloudflare.com/containers/guides/deploy/) and [Rollouts](https://developers.cloudflare.com/containers/configuration/rollouts/).

To put `exposePort()` on a custom domain, refer to [Configure preview URLs on a custom domain](https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/).

Sandbox SDK 1.0 preview

This guide targets the stable `@cloudflare/sandbox` package.

On **`@cloudflare/sandbox@next`**, keep the Worker package and container image on the same preview line. For a breaking cutover, refer to [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/#deploy-the-cutover).

## Keep the package and image aligned

The Worker depends on `@cloudflare/sandbox` (or `@cloudflare/sandbox@next`). The container image must come from the same release line (Dockerfile and base image tags from the template or docs for that version).

When you bump the npm package:

1. Update the Dockerfile or image reference for the same line.
2. Run `wrangler deploy` so the new image is published.
3. If the Worker and image must cut over together, deploy with an immediate rollout:  
npmyarnpnpm  
```  
npx wrangler deploy --containers-rollout=immediate  
```  
```  
yarn wrangler deploy --containers-rollout=immediate  
```  
```  
pnpm wrangler deploy --containers-rollout=immediate  
```  
Use this for stable to `@next` cutovers and other breaking package/image pairs. Refer to [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/#deploy-the-cutover) and [Rollouts](https://developers.cloudflare.com/containers/configuration/rollouts/).

Do not mix a stable package with a `@next` image, or the reverse.

## Deploy from your machine

1. Start Docker if `image` is a Dockerfile path. Registry image references do not need Docker at deploy time.
2. From the project root:  
npmyarnpnpm  
```  
npx wrangler deploy  
```  
```  
yarn wrangler deploy  
```  
```  
pnpm wrangler deploy  
```
3. Confirm the Worker URL responds, then exercise a sandbox route.

The first deploy can take several minutes while the image provisions.

## Workers Builds

For production, use `wrangler deploy` so the package and image can update together.

Non-production Workers Builds defaults to `wrangler versions upload`, which does not publish a new image. [Preview URLs](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/) are not generated for these Workers (they implement Durable Objects). Test with `wrangler dev`, or with a staging Worker or [environment](https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/#wrangler-environments) that runs `wrangler deploy`.

More detail: [Before production](https://developers.cloudflare.com/containers/guides/deploy/#before-production).

## Related

* [Deploy Containers](https://developers.cloudflare.com/containers/guides/deploy/)
* [Rollouts](https://developers.cloudflare.com/containers/configuration/rollouts/)
* [Configure preview URLs on a custom domain](https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/)
* [Migrate to Sandbox SDK 1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/deploy/#page","headline":"Deploy a Sandbox application · Cloudflare Sandbox SDK docs","description":"Deploy a Sandbox Worker and keep the npm package and container image on the same release line.","url":"https://developers.cloudflare.com/sandbox/guides/deploy/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-28","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Run Docker commands inside a sandbox container.
title: Run Docker-in-Docker
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Run Docker-in-Docker

Last updated Apr 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/docker-in-docker/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

This guide shows you how to run Docker inside a Sandbox, enabling you to build and run container images from within a secure sandbox.

## When to use Docker-in-Docker

Use Docker-in-Docker when you need to:

* **Develop containerized applications** \- Run `docker build` to create images from Dockerfiles
* **Run Docker as part of CI/CD** \- Respond to code changes and build and push images using Cloudflare Containers
* **Run arbitrary container images** \- Start containers from an end-user provided image

## Create a Docker-enabled image

Cloudflare Containers run without root privileges, so you must use the rootless Docker image. Create a custom Dockerfile that combines the sandbox binary with Docker:

```dockerfile
FROM docker:dind-rootless
USER root

# Use the musl build so it runs on Alpine-based docker:dind-rootless
COPY --from=docker.io/cloudflare/sandbox:0.7.4-musl /container-server/sandbox /sandbox
COPY --from=docker.io/cloudflare/sandbox:0.7.4-musl /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6
COPY --from=docker.io/cloudflare/sandbox:0.7.4-musl /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1
COPY --from=docker.io/cloudflare/sandbox:0.7.4-musl /bin/bash /bin/bash
COPY --from=docker.io/cloudflare/sandbox:0.7.4-musl /usr/lib/libreadline.so.8 /usr/lib/libreadline.so.8
COPY --from=docker.io/cloudflare/sandbox:0.7.4-musl /usr/lib/libreadline.so.8.2 /usr/lib/libreadline.so.8.2

# Create startup script that starts dockerd with
# iptables disabled, waits for readiness, then keeps running
RUN printf '#!/bin/sh\n\
  set -eu\n\
  dockerd-entrypoint.sh dockerd --iptables=false --ip6tables=false &\n\
  until docker version >/dev/null 2>&1; do sleep 0.2; done\n\
  echo "Docker is ready"\n\
  wait\n' > /home/rootless/boot-docker-for-dind.sh && chmod +x /home/rootless/boot-docker-for-dind.sh

ENTRYPOINT ["/sandbox"]
CMD ["/home/rootless/boot-docker-for-dind.sh"]
```

Working with disabled iptables

Cloudflare Containers do not support iptables manipulation. The `--iptables=false` and `--ip6tables=false` flags prevent Docker from attempting to configure network rules, which would otherwise fail.

To send or receive traffic from a container running within Docker-in-Docker, use the `--network=host` flag when running Docker commands.

This allows you to connect to the container, but it means each inner container has access to your outer container's network stack. Ensure you understand the security implications of this setup before proceeding.

## Use Docker in your sandbox

Once deployed, you can run Docker commands through the sandbox:

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "docker-sandbox");

// Build an image
await sandbox.writeFile(
	"/workspace/Dockerfile",
	`
FROM alpine:latest
RUN apk add --no-cache curl
CMD ["echo", "Hello from Docker!"]
`,
);

const build = await sandbox.exec(
	"docker build --network=host -t my-image /workspace",
);
if (!build.success) {
	console.error("Build failed:", build.stderr);
}

// Run a container
const run = await sandbox.exec("docker run --network=host --rm my-image");
console.log(run.stdout); // "Hello from Docker!"
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "docker-sandbox");

// Build an image
await sandbox.writeFile(
	"/workspace/Dockerfile",
	`
FROM alpine:latest
RUN apk add --no-cache curl
CMD ["echo", "Hello from Docker!"]
`,
);

const build = await sandbox.exec(
	"docker build --network=host -t my-image /workspace",
);
if (!build.success) {
	console.error("Build failed:", build.stderr);
}

// Run a container
const run = await sandbox.exec("docker run --network=host --rm my-image");
console.log(run.stdout); // "Hello from Docker!"
```

## Limitations

Docker-in-Docker in Cloudflare Containers has the following limitations:

* **No iptables** \- Network isolation features that rely on iptables are not available
* **Rootless mode only** \- You cannot use privileged containers or features requiring root
* **Ephemeral storage** \- Built images and containers are lost when the sandbox sleeps. You must persist them manually.

## Related resources

* [Dockerfile reference](https://developers.cloudflare.com/sandbox/configuration/dockerfile/) \- Customize your sandbox image
* [Execute commands](https://developers.cloudflare.com/sandbox/guides/execute-commands/) \- Run commands in the sandbox
* [Background processes](https://developers.cloudflare.com/sandbox/guides/background-processes/) \- Manage long-running processes

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/docker-in-docker/#page","headline":"Run Docker-in-Docker · Cloudflare Sandbox SDK docs","description":"Run Docker commands inside a sandbox container.","url":"https://developers.cloudflare.com/sandbox/guides/docker-in-docker/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Run commands with streaming output, error handling, and shell access.
title: Execute commands
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Execute commands

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/execute-commands/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

This guide shows you how to execute commands in the sandbox, handle output, and manage errors effectively.

Coming soon: Sandbox SDK 1.0

This page documents command execution on today's stable `@cloudflare/sandbox` package.

**Sandbox SDK 1.0** (preview on `@cloudflare/sandbox@next`) uses argv `exec()` and process handles instead of string `exec` / `startProcess` / `execStream`. Refer to [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/), the [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/), or [migrate to the preview](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/).

## Choose the right method

The SDK provides multiple approaches for running commands:

* **`exec()`** \- Run a command and wait for complete result. Best for one-time commands like builds, installations, and scripts.
* **`execStream()`** \- Stream output in real-time. Best for long-running commands where you need immediate feedback.
* **`startProcess()`** \- Start a background process. Best for web servers, databases, and services that need to keep running.

Note

For **web servers, databases, or services that need to keep running**, use `startProcess()` instead. See the [Background processes guide](https://developers.cloudflare.com/sandbox/guides/background-processes/).

## Execute basic commands

Use `exec()` for simple commands that complete quickly:

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Execute a single command
const result = await sandbox.exec("python --version");

console.log(result.stdout); // "Python 3.11.0"
console.log(result.exitCode); // 0
console.log(result.success); // true
```

```plaintext
import { getSandbox } from '@cloudflare/sandbox';

const sandbox = getSandbox(env.Sandbox, 'my-sandbox');

// Execute a single command
const result = await sandbox.exec('python --version');

console.log(result.stdout);   // "Python 3.11.0"
console.log(result.exitCode); // 0
console.log(result.success);  // true
```

## Pass arguments safely

When passing user input or dynamic values, avoid string interpolation to prevent injection attacks:

```js
// Unsafe - vulnerable to injection
const filename = userInput;
await sandbox.exec(`cat ${filename}`);

// Safe - use proper escaping or validation
const safeFilename = filename.replace(/[^a-zA-Z0-9_.-]/g, "");
await sandbox.exec(`cat ${safeFilename}`);

// Better - write to file and execute
await sandbox.writeFile("/tmp/input.txt", userInput);
await sandbox.exec("python process.py /tmp/input.txt");
```

```plaintext
// Unsafe - vulnerable to injection
const filename = userInput;
await sandbox.exec(`cat ${filename}`);

// Safe - use proper escaping or validation
const safeFilename = filename.replace(/[^a-zA-Z0-9_.-]/g, '');
await sandbox.exec(`cat ${safeFilename}`);

// Better - write to file and execute
await sandbox.writeFile('/tmp/input.txt', userInput);
await sandbox.exec('python process.py /tmp/input.txt');
```

## Handle errors

Commands can fail in two ways:

1. **Non-zero exit code** \- Command ran but failed (result.success === false)
2. **Execution error** \- Command couldn't start (throws exception)

```js
try {
	const result = await sandbox.exec("python analyze.py");

	if (!result.success) {
		// Command failed (non-zero exit code)
		console.error("Analysis failed:", result.stderr);
		console.log("Exit code:", result.exitCode);

		// Handle specific exit codes
		if (result.exitCode === 1) {
			throw new Error("Invalid input data");
		} else if (result.exitCode === 2) {
			throw new Error("Missing dependencies");
		}
	}

	// Success - process output
	return JSON.parse(result.stdout);
} catch (error) {
	// Execution error (couldn't start command)
	console.error("Execution failed:", error.message);
	throw error;
}
```

```plaintext
try {
  const result = await sandbox.exec('python analyze.py');

  if (!result.success) {
    // Command failed (non-zero exit code)
    console.error('Analysis failed:', result.stderr);
    console.log('Exit code:', result.exitCode);

    // Handle specific exit codes
    if (result.exitCode === 1) {
      throw new Error('Invalid input data');
    } else if (result.exitCode === 2) {
      throw new Error('Missing dependencies');
    }
  }

  // Success - process output
  return JSON.parse(result.stdout);

} catch (error) {
  // Execution error (couldn't start command)
  console.error('Execution failed:', error.message);
  throw error;
}
```

## Execute shell commands

The sandbox supports shell features like pipes, redirects, and chaining:

```js
// Pipes and filters
const result = await sandbox.exec('ls -la | grep ".py" | wc -l');
console.log("Python files:", result.stdout.trim());

// Output redirection
await sandbox.exec("python generate.py > output.txt 2> errors.txt");

// Multiple commands
await sandbox.exec("cd /workspace && npm install && npm test");
```

```plaintext
// Pipes and filters
const result = await sandbox.exec('ls -la | grep ".py" | wc -l');
console.log('Python files:', result.stdout.trim());

// Output redirection
await sandbox.exec('python generate.py > output.txt 2> errors.txt');

// Multiple commands
await sandbox.exec('cd /workspace && npm install && npm test');
```

## Execute Python scripts

```js
// Run inline Python
const result = await sandbox.exec('python -c "print(sum([1, 2, 3, 4, 5]))"');
console.log("Sum:", result.stdout.trim()); // "15"

// Run a script file
await sandbox.writeFile(
	"/workspace/analyze.py",
	`
import sys
print(f"Argument: {sys.argv[1]}")
`,
);

await sandbox.exec("python /workspace/analyze.py data.csv");
```

```plaintext
// Run inline Python
const result = await sandbox.exec('python -c "print(sum([1, 2, 3, 4, 5]))"');
console.log('Sum:', result.stdout.trim()); // "15"

// Run a script file
await sandbox.writeFile('/workspace/analyze.py', `
import sys
print(f"Argument: {sys.argv[1]}")
`);

await sandbox.exec('python /workspace/analyze.py data.csv');
```

## Timeouts

Set a maximum execution time for commands to prevent long-running operations from blocking indefinitely.

### Per-command timeout

Pass `timeout` in the options to set a timeout for a single command:

```js
const result = await sandbox.exec("npm run build", {
	timeout: 30000, // 30 seconds
});
```

```plaintext
const result = await sandbox.exec('npm run build', {
  timeout: 30000 // 30 seconds
});
```

### Session-level timeout

Set a default timeout for all commands in a session with `commandTimeoutMs`:

```js
const session = await sandbox.createSession({
	commandTimeoutMs: 10000, // 10s default for all commands
});

await session.exec("npm install"); // Times out after 10s
await session.exec("npm run build"); // Times out after 10s

// Per-command timeout overrides the session default
await session.exec("npm test", { timeout: 60000 }); // 60s for this command
```

```plaintext
const session = await sandbox.createSession({
  commandTimeoutMs: 10000 // 10s default for all commands
});

await session.exec('npm install');    // Times out after 10s
await session.exec('npm run build');  // Times out after 10s

// Per-command timeout overrides the session default
await session.exec('npm test', { timeout: 60000 }); // 60s for this command
```

### Global timeout

Set the `COMMAND_TIMEOUT_MS` [environment variable](https://developers.cloudflare.com/sandbox/configuration/environment-variables/#command%5Ftimeout%5Fms) to define a global default timeout for every `exec()` call across all sessions.

### Timeout precedence

When multiple timeouts are configured, the most specific value wins:

1. **Per-command** `timeout` on `exec()` (highest priority)
2. **Session-level** `commandTimeoutMs` on `createSession()`
3. **Global** `COMMAND_TIMEOUT_MS` environment variable (lowest priority)

If none are set, commands run without a timeout.

### Timeout does not kill the process

Caution

When a command times out, the SDK raises an error and closes the connection. The underlying process **continues running** inside the container. To stop a timed-out process, delete the session with [deleteSession()](https://developers.cloudflare.com/sandbox/api/sessions/#deletesession) or destroy the sandbox with [destroy()](https://developers.cloudflare.com/sandbox/api/lifecycle/#destroy).

## Best practices

* **Check exit codes** \- Always verify `result.success` and `result.exitCode`
* **Validate inputs** \- Escape or validate user input to prevent injection
* **Use streaming** \- For long operations, use `execStream()` for real-time feedback
* **Use background processes** \- For services that need to keep running (web servers, databases), use the [Background processes guide](https://developers.cloudflare.com/sandbox/guides/background-processes/) instead
* **Handle errors** \- Check stderr for error details

## Troubleshooting

### Command not found

Verify the command exists in the container:

```js
const check = await sandbox.exec("which python3");
if (!check.success) {
	console.error("python3 not found");
}
```

```plaintext
const check = await sandbox.exec('which python3');
if (!check.success) {
  console.error('python3 not found');
}
```

### Working directory issues

Use absolute paths or change directory:

```js
// Use absolute path
await sandbox.exec("python /workspace/my-app/script.py");

// Or change directory
await sandbox.exec("cd /workspace/my-app && python script.py");
```

```plaintext
// Use absolute path
await sandbox.exec('python /workspace/my-app/script.py');

// Or change directory
await sandbox.exec('cd /workspace/my-app && python script.py');
```

## Related resources

* [Commands API reference](https://developers.cloudflare.com/sandbox/api/commands/) \- Complete method documentation
* [Background processes guide](https://developers.cloudflare.com/sandbox/guides/background-processes/) \- Managing long-running processes
* [Streaming output guide](https://developers.cloudflare.com/sandbox/guides/streaming-output/) \- Advanced streaming patterns
* [Code Interpreter guide](https://developers.cloudflare.com/sandbox/guides/code-execution/) \- Higher-level code execution

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/execute-commands/#page","headline":"Execute commands · Cloudflare Sandbox SDK docs","description":"Run commands with streaming output, error handling, and shell access.","url":"https://developers.cloudflare.com/sandbox/guides/execute-commands/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Create preview URLs and expose ports for web services.
title: Expose services
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Expose services

Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/expose-services/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Coming soon: Sandbox SDK 1.0

This guide documents exposing services on today's stable `@cloudflare/sandbox` package.

On **`@next`**, start the service with `exec(argv)` (not `startProcess`), then expose or use tunnels — [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/).

Production requires custom domain

Preview URLs require a custom domain with wildcard DNS routing in production. See [Configure preview URLs on a custom domain](https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/) for setup instructions.

Prefer \`sandbox.tunnels\` for public URLs

[sandbox.tunnels](https://developers.cloudflare.com/sandbox/api/tunnels/) is the recommended option for most public-URL use cases, including production. Quick tunnels give you a zero-config `*.trycloudflare.com` URL; named tunnels bind a stable `<name>.<your-zone>` hostname. Follow this guide when you specifically want the Worker itself to front the request (for example, to inject authentication or rewrite responses).

This guide shows you how to expose services running in your sandbox to the internet via preview URLs.

## When to expose ports

Expose ports when you need to:

* **Test web applications** \- Preview frontend or backend apps
* **Share demos** \- Give others access to running applications
* **Develop APIs** \- Test endpoints from external tools
* **Debug services** \- Access internal services for troubleshooting
* **Build dev environments** \- Create shareable development workspaces

## Basic port exposure

The typical workflow is: start service → wait for ready → expose port → handle requests with `proxyToSandbox`.

```js
import { getSandbox, proxyToSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		// Proxy requests to exposed ports first
		const proxyResponse = await proxyToSandbox(request, env);
		if (proxyResponse) return proxyResponse;

		// Extract hostname from request
		const { hostname } = new URL(request.url);
		const sandbox = getSandbox(env.Sandbox, "my-sandbox");

		// 1. Start a web server
		await sandbox.startProcess("python -m http.server 8000");

		// 2. Wait for service to start
		await new Promise((resolve) => setTimeout(resolve, 2000));

		// 3. Expose the port
		const exposed = await sandbox.exposePort(8000, { hostname });

		// 4. Preview URL is now available (public by default)
		console.log("Server accessible at:", exposed.url);
		// Production: https://8000-abc123.yourdomain.com
		// Local dev: http://localhost:8787/...

		return Response.json({ url: exposed.url });
	},
};
```

```plaintext
import { getSandbox, proxyToSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Proxy requests to exposed ports first
    const proxyResponse = await proxyToSandbox(request, env);
    if (proxyResponse) return proxyResponse;

    // Extract hostname from request
    const { hostname } = new URL(request.url);
    const sandbox = getSandbox(env.Sandbox, 'my-sandbox');

    // 1. Start a web server
    await sandbox.startProcess('python -m http.server 8000');

    // 2. Wait for service to start
    await new Promise(resolve => setTimeout(resolve, 2000));

    // 3. Expose the port
    const exposed = await sandbox.exposePort(8000, { hostname });

    // 4. Preview URL is now available (public by default)
    console.log('Server accessible at:', exposed.url);
    // Production: https://8000-abc123.yourdomain.com
    // Local dev: http://localhost:8787/...

    return Response.json({ url: exposed.url });
  }
};
```

Caution

**Preview URLs are public by default.** Anyone with the URL can access your service. Add authentication if needed.

Local development requirement

When using `wrangler dev`, you must add `EXPOSE` directives to your Dockerfile for each port you plan to expose. Without this, you'll see "Connection refused: container port not found". See [Local development](#local-development) section below for setup details.

Uppercase sandbox IDs don't work with preview URLs

Preview URLs extract the sandbox ID from the hostname, which is always lowercase (e.g., `8000-myproject-123.yourdomain.com`). If you created your sandbox with an uppercase ID like `"MyProject-123"`, the URL routes to `"myproject-123"` (a different Durable Object), making your sandbox unreachable.

To fix this, use `normalizeId: true` when creating sandboxes for port exposure:

```ts
const sandbox = getSandbox(env.Sandbox, 'MyProject-123', { normalizeId: true });
```

This lowercases the ID during creation so it matches preview URL routing. Without this, `exposePort()` throws an error.

**Best practice**: Use lowercase IDs from the start (`'my-project-123'`).

See [Sandbox options](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/#normalizeid) for details.

## Stable URLs with custom tokens

For production deployments or when sharing URLs with users, use custom tokens to maintain consistent preview URLs across container restarts:

```js
// Extract hostname from request
const { hostname } = new URL(request.url);

// Without custom token - URL changes on restart
const exposed = await sandbox.exposePort(8080, { hostname });
// https://8080-sandbox-id-random16chars12.yourdomain.com

// With custom token - URL stays the same across restarts
const stable = await sandbox.exposePort(8080, {
	hostname,
	token: "api-v1",
});
// https://8080-sandbox-id-api-v1.yourdomain.com
// Same URL after container restart ✓

return Response.json({
	"Temporary URL (changes on restart)": exposed.url,
	"Stable URL (consistent)": stable.url,
});
```

```plaintext
// Extract hostname from request
const { hostname } = new URL(request.url);

// Without custom token - URL changes on restart
const exposed = await sandbox.exposePort(8080, { hostname });
// https://8080-sandbox-id-random16chars12.yourdomain.com

// With custom token - URL stays the same across restarts
const stable = await sandbox.exposePort(8080, { 
  hostname, 
  token: 'api-v1' 
});
// https://8080-sandbox-id-api-v1.yourdomain.com
// Same URL after container restart ✓

return Response.json({
  'Temporary URL (changes on restart)': exposed.url,
  'Stable URL (consistent)': stable.url
});
```

**Token requirements:**

* 1-16 characters long
* Lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (\_) only
* Must be unique within each sandbox

**Use cases:**

* Production APIs with stable endpoints
* Sharing demo URLs with external users
* Integration testing with predictable URLs
* Documentation with consistent examples

## Name your exposed ports

When exposing multiple ports, use names to stay organized:

```js
// Extract hostname from request
const { hostname } = new URL(request.url);

// Start and expose API server with stable token
await sandbox.startProcess("node api.js", { env: { PORT: "8080" } });
await new Promise((resolve) => setTimeout(resolve, 2000));
const api = await sandbox.exposePort(8080, {
	hostname,
	name: "api",
	token: "api-prod",
});

// Start and expose frontend with stable token
await sandbox.startProcess("npm run dev", { env: { PORT: "5173" } });
await new Promise((resolve) => setTimeout(resolve, 2000));
const frontend = await sandbox.exposePort(5173, {
	hostname,
	name: "frontend",
	token: "web-app",
});

console.log("Services:");
console.log("- API:", api.url);
console.log("- Frontend:", frontend.url);
```

```plaintext
// Extract hostname from request
const { hostname } = new URL(request.url);

// Start and expose API server with stable token
await sandbox.startProcess('node api.js', { env: { PORT: '8080' } });
await new Promise(resolve => setTimeout(resolve, 2000));
const api = await sandbox.exposePort(8080, { 
  hostname, 
  name: 'api',
  token: 'api-prod'
});

// Start and expose frontend with stable token
await sandbox.startProcess('npm run dev', { env: { PORT: '5173' } });
await new Promise(resolve => setTimeout(resolve, 2000));
const frontend = await sandbox.exposePort(5173, { 
  hostname, 
  name: 'frontend',
  token: 'web-app'
});

console.log('Services:');
console.log('- API:', api.url);
console.log('- Frontend:', frontend.url);
```

## Wait for service readiness

Always verify a service is ready before exposing. Use a simple delay for most cases:

```js
// Extract hostname from request
const { hostname } = new URL(request.url);

// Start service
await sandbox.startProcess("npm run dev", { env: { PORT: "8080" } });

// Wait 2-3 seconds
await new Promise((resolve) => setTimeout(resolve, 2000));

// Now expose
await sandbox.exposePort(8080, { hostname });
```

```plaintext
// Extract hostname from request
const { hostname } = new URL(request.url);

// Start service
await sandbox.startProcess('npm run dev', { env: { PORT: '8080' } });

// Wait 2-3 seconds
await new Promise(resolve => setTimeout(resolve, 2000));

// Now expose
await sandbox.exposePort(8080, { hostname });
```

For critical services, poll the health endpoint:

```js
// Extract hostname from request
const { hostname } = new URL(request.url);

await sandbox.startProcess("node api-server.js", { env: { PORT: "8080" } });

// Wait for health check
for (let i = 0; i < 10; i++) {
	await new Promise((resolve) => setTimeout(resolve, 1000));

	const check = await sandbox.exec(
		'curl -f http://localhost:8080/health || echo "not ready"',
	);
	if (check.stdout.includes("ok")) {
		break;
	}
}

await sandbox.exposePort(8080, { hostname });
```

```plaintext
// Extract hostname from request
const { hostname } = new URL(request.url);

await sandbox.startProcess('node api-server.js', { env: { PORT: '8080' } });

// Wait for health check
for (let i = 0; i < 10; i++) {
  await new Promise(resolve => setTimeout(resolve, 1000));

  const check = await sandbox.exec('curl -f http://localhost:8080/health || echo "not ready"');
  if (check.stdout.includes('ok')) {
    break;
  }
}

await sandbox.exposePort(8080, { hostname });
```

## Multiple services

Expose multiple ports for full-stack applications:

```js
// Extract hostname from request
const { hostname } = new URL(request.url);

// Start backend
await sandbox.startProcess("node api/server.js", {
	env: { PORT: "8080" },
});
await new Promise((resolve) => setTimeout(resolve, 2000));

// Start frontend
await sandbox.startProcess("npm run dev", {
	cwd: "/workspace/frontend",
	env: { PORT: "5173", API_URL: "http://localhost:8080" },
});
await new Promise((resolve) => setTimeout(resolve, 3000));

// Expose both
const api = await sandbox.exposePort(8080, { hostname, name: "api" });
const frontend = await sandbox.exposePort(5173, { hostname, name: "frontend" });

return Response.json({
	api: api.url,
	frontend: frontend.url,
});
```

```plaintext
// Extract hostname from request
const { hostname } = new URL(request.url);

// Start backend
await sandbox.startProcess('node api/server.js', {
  env: { PORT: '8080' }
});
await new Promise(resolve => setTimeout(resolve, 2000));

// Start frontend
await sandbox.startProcess('npm run dev', {
  cwd: '/workspace/frontend',
  env: { PORT: '5173', API_URL: 'http://localhost:8080' }
});
await new Promise(resolve => setTimeout(resolve, 3000));

// Expose both
const api = await sandbox.exposePort(8080, { hostname, name: 'api' });
const frontend = await sandbox.exposePort(5173, { hostname, name: 'frontend' });

return Response.json({
  api: api.url,
  frontend: frontend.url
});
```

## Manage exposed ports

### List currently exposed ports

```js
const { ports, count } = await sandbox.getExposedPorts();

console.log(`${count} ports currently exposed:`);

for (const port of ports) {
	console.log(`  Port ${port.port}: ${port.url}`);
	if (port.name) {
		console.log(`    Name: ${port.name}`);
	}
}
```

```plaintext
const { ports, count } = await sandbox.getExposedPorts();

console.log(`${count} ports currently exposed:`);

for (const port of ports) {
  console.log(`  Port ${port.port}: ${port.url}`);
  if (port.name) {
    console.log(`    Name: ${port.name}`);
  }
}
```

### Unexpose ports

```js
// Unexpose a single port
await sandbox.unexposePort(8000);

// Unexpose multiple ports
for (const port of [3000, 5173, 8080]) {
	await sandbox.unexposePort(port);
}
```

```plaintext
// Unexpose a single port
await sandbox.unexposePort(8000);

// Unexpose multiple ports
for (const port of [3000, 5173, 8080]) {
  await sandbox.unexposePort(port);
}
```

## Best practices

* **Wait for readiness** \- Don't expose ports immediately after starting processes
* **Use named ports** \- Easier to track when exposing multiple ports
* **Clean up** \- Unexpose ports when done to prevent abandoned URLs
* **Add authentication** \- Preview URLs are public; protect sensitive services

## Local development

When developing locally with `wrangler dev`, you must expose ports in your Dockerfile:

```dockerfile
FROM docker.io/cloudflare/sandbox:0.3.3

# Expose ports you plan to use
EXPOSE 8000
EXPOSE 8080
EXPOSE 5173
```

Update `wrangler.jsonc` to use your Dockerfile:

```jsonc
{
  "containers": [
    {
      "class_name": "Sandbox",
      "image": "./Dockerfile"
    }
  ]
}
```

In production, all ports are available and controlled programmatically via `exposePort()` / `unexposePort()`.

## Troubleshooting

### Port 3000 is reserved

Port 3000 is used by the internal Bun server and cannot be exposed:

```js
// Extract hostname from request
const { hostname } = new URL(request.url);

// ❌ This will fail
await sandbox.exposePort(3000, { hostname }); // Error: Port 3000 is reserved

// ✅ Use a different port
await sandbox.startProcess("node server.js", { env: { PORT: "8080" } });
await sandbox.exposePort(8080, { hostname });
```

```plaintext
// Extract hostname from request
const { hostname } = new URL(request.url);

// ❌ This will fail
await sandbox.exposePort(3000, { hostname });  // Error: Port 3000 is reserved

// ✅ Use a different port
await sandbox.startProcess('node server.js', { env: { PORT: '8080' } });
await sandbox.exposePort(8080, { hostname });
```

### Port not ready

Wait for the service to start before exposing:

```js
// Extract hostname from request
const { hostname } = new URL(request.url);

await sandbox.startProcess("npm run dev");
await new Promise((resolve) => setTimeout(resolve, 3000));
await sandbox.exposePort(8080, { hostname });
```

```plaintext
// Extract hostname from request
const { hostname } = new URL(request.url);

await sandbox.startProcess('npm run dev');
await new Promise(resolve => setTimeout(resolve, 3000));
await sandbox.exposePort(8080, { hostname });
```

### Port already exposed

Check before exposing to avoid errors:

```js
// Extract hostname from request
const { hostname } = new URL(request.url);

const { ports } = await sandbox.getExposedPorts();
if (!ports.some((p) => p.port === 8080)) {
	await sandbox.exposePort(8080, { hostname });
}
```

```plaintext
// Extract hostname from request
const { hostname } = new URL(request.url);

const { ports } = await sandbox.getExposedPorts();
if (!ports.some(p => p.port === 8080)) {
  await sandbox.exposePort(8080, { hostname });
}
```

### Uppercase sandbox ID error

**Error**: `Preview URLs require lowercase sandbox IDs`

**Cause**: You created a sandbox with uppercase characters (e.g., `"MyProject-123"`) but preview URLs always use lowercase in routing, causing a mismatch.

**Solution**:

```js
// Create sandbox with normalization
const sandbox = getSandbox(env.Sandbox, "MyProject-123", { normalizeId: true });
await sandbox.exposePort(8080, { hostname });
```

```plaintext
// Create sandbox with normalization
const sandbox = getSandbox(env.Sandbox, 'MyProject-123', { normalizeId: true });
await sandbox.exposePort(8080, { hostname });
```

This creates the Durable Object with ID `"myproject-123"`, matching the preview URL routing.

See [Sandbox options - normalizeId](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/#normalizeid) for details.

## Preview URL Format

**Production**: `https://{port}-{sandbox-id}-{token}.yourdomain.com`

* Auto-generated token: `https://8080-abc123-random16chars12.yourdomain.com`
* Custom token: `https://8080-abc123-my-api-v1.yourdomain.com`

**Local development**: `http://localhost:8787/...`

**Note**: Port 3000 is reserved for the internal Bun server and cannot be exposed.

## Related resources

* [Ports API reference](https://developers.cloudflare.com/sandbox/api/ports/) \- Complete port exposure API
* [Background processes guide](https://developers.cloudflare.com/sandbox/guides/background-processes/) \- Managing services
* [Execute commands guide](https://developers.cloudflare.com/sandbox/guides/execute-commands/) \- Starting services
* [Tunnels API reference](https://developers.cloudflare.com/sandbox/api/tunnels/) \- Recommended alternative for most public-URL use cases (quick or named tunnels)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/expose-services/#page","headline":"Expose services · Cloudflare Sandbox SDK docs","description":"Create preview URLs and expose ports for web services.","url":"https://developers.cloudflare.com/sandbox/guides/expose-services/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Monitor files and directories in real-time to build responsive development tools and automation workflows.
title: Watch filesystem changes
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Watch filesystem changes

Last updated Apr 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/file-watching/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

This guide shows you how to monitor filesystem changes in real-time using the Sandbox SDK's file watching API. File watching is useful for building development tools, automated workflows, and applications that react to file changes as they happen.

The `watch()` method returns an SSE (Server-Sent Events) stream that you consume with `parseSSEStream()`. Each event in the stream describes a filesystem change.

## Basic file watching

Start by watching a directory for any changes:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
		console.log(`Is directory: ${event.isDirectory}`);
	}
}
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
		console.log(`Is directory: ${event.isDirectory}`);
	}
}
```

The stream emits four lifecycle event types:

* **`watching`** — Watch established, includes the `watchId`
* **`event`** — A filesystem change occurred
* **`error`** — The watch encountered an error
* **`stopped`** — The watch was stopped

Filesystem change events (`event.eventType`) include:

* **`create`** — File or directory was created
* **`modify`** — File content changed
* **`delete`** — File or directory was removed
* **`move_from`** / **`move_to`** — File or directory was moved or renamed
* **`attrib`** — File attributes changed (permissions, timestamps)

## Filter by file type

Use `include` patterns to watch only specific file types:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

// Only watch TypeScript and JavaScript files
const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts", "*.tsx", "*.js", "*.jsx"],
});

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
	}
}
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

// Only watch TypeScript and JavaScript files
const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts", "*.tsx", "*.js", "*.jsx"],
});

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
	}
}
```

Common include patterns:

* `*.ts` — TypeScript files
* `*.js` — JavaScript files
* `*.json` — JSON configuration files
* `*.md` — Markdown documentation
* `package*.json` — Package files specifically

## Exclude directories

Use `exclude` patterns to skip certain directories or files:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace", {
	exclude: ["node_modules", "dist", "*.log", ".git", "*.tmp"],
});

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		console.log(`Change detected: ${event.path}`);
	}
}
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace", {
	exclude: ["node_modules", "dist", "*.log", ".git", "*.tmp"],
});

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		console.log(`Change detected: ${event.path}`);
	}
}
```

Default exclusions

The following patterns are excluded by default: `.git`, `node_modules`, `.DS_Store`. You can override this by providing your own `exclude` array.

## Build responsive development tools

### Auto-rebuild on changes

Trigger builds automatically when source files are modified:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts", "*.tsx"],
});

let buildInProgress = false;

for await (const event of parseSSEStream(stream)) {
	if (
		event.type === "event" &&
		event.eventType === "modify" &&
		!buildInProgress
	) {
		buildInProgress = true;
		console.log(`File changed: ${event.path}, rebuilding...`);

		try {
			const result = await sandbox.exec("npm run build");
			if (result.success) {
				console.log("Build completed successfully");
			} else {
				console.error("Build failed:", result.stderr);
			}
		} catch (error) {
			console.error("Build error:", error);
		} finally {
			buildInProgress = false;
		}
	}
}
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts", "*.tsx"],
});

let buildInProgress = false;

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (
		event.type === "event" &&
		event.eventType === "modify" &&
		!buildInProgress
	) {
		buildInProgress = true;
		console.log(`File changed: ${event.path}, rebuilding...`);

		try {
			const result = await sandbox.exec("npm run build");
			if (result.success) {
				console.log("Build completed successfully");
			} else {
				console.error("Build failed:", result.stderr);
			}
		} catch (error) {
			console.error("Build error:", error);
		} finally {
			buildInProgress = false;
		}
	}
}
```

### Auto-run tests on change

Re-run tests when test files are modified:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/tests", {
	include: ["*.test.ts", "*.spec.ts"],
});

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event" && event.eventType === "modify") {
		console.log(`Test file changed: ${event.path}`);
		const result = await sandbox.exec(`npm test -- ${event.path}`);
		console.log(result.success ? "Tests passed" : "Tests failed");
	}
}
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/tests", {
	include: ["*.test.ts", "*.spec.ts"],
});

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event" && event.eventType === "modify") {
		console.log(`Test file changed: ${event.path}`);
		const result = await sandbox.exec(`npm test -- ${event.path}`);
		console.log(result.success ? "Tests passed" : "Tests failed");
	}
}
```

### Incremental indexing

Re-index only changed files instead of rescanning an entire directory tree:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/docs", {
	include: ["*.md", "*.mdx"],
});

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		switch (event.eventType) {
			case "create":
			case "modify":
				console.log(`Indexing ${event.path}...`);
				await indexFile(event.path);
				break;
			case "delete":
				console.log(`Removing ${event.path} from index...`);
				await removeFromIndex(event.path);
				break;
		}
	}
}
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/docs", {
	include: ["*.md", "*.mdx"],
});

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		switch (event.eventType) {
			case "create":
			case "modify":
				console.log(`Indexing ${event.path}...`);
				await indexFile(event.path);
				break;
			case "delete":
				console.log(`Removing ${event.path} from index...`);
				await removeFromIndex(event.path);
				break;
		}
	}
}
```

## Advanced patterns

### Process events with a helper function

Extract event processing into a reusable function that handles stream lifecycle:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

async function watchFiles(sandbox, path, options, handler) {
	const stream = await sandbox.watch(path, options);

	for await (const event of parseSSEStream(stream)) {
		switch (event.type) {
			case "watching":
				console.log(`Watching ${event.path}`);
				break;
			case "event":
				await handler(event.eventType, event.path, event.isDirectory);
				break;
			case "error":
				console.error(`Watch error: ${event.error}`);
				break;
			case "stopped":
				console.log(`Watch stopped: ${event.reason}`);
				return;
		}
	}
}

// Usage
await watchFiles(
	sandbox,
	"/workspace/src",
	{ include: ["*.ts"] },
	async (eventType, filePath) => {
		console.log(`${eventType}: ${filePath}`);
	},
);
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

async function watchFiles(
	sandbox: any,
	path: string,
	options: { include?: string[]; exclude?: string[] },
	handler: (
		eventType: string,
		filePath: string,
		isDirectory: boolean,
	) => Promise<void>,
) {
	const stream = await sandbox.watch(path, options);

	for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
		switch (event.type) {
			case "watching":
				console.log(`Watching ${event.path}`);
				break;
			case "event":
				await handler(event.eventType, event.path, event.isDirectory);
				break;
			case "error":
				console.error(`Watch error: ${event.error}`);
				break;
			case "stopped":
				console.log(`Watch stopped: ${event.reason}`);
				return;
		}
	}
}

// Usage
await watchFiles(
	sandbox,
	"/workspace/src",
	{ include: ["*.ts"] },
	async (eventType, filePath) => {
		console.log(`${eventType}: ${filePath}`);
	},
);
```

### Debounced file operations

Avoid excessive operations by collecting changes before processing:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
const changedFiles = new Set();
let debounceTimeout = null;

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		changedFiles.add(event.path);

		if (debounceTimeout) {
			clearTimeout(debounceTimeout);
		}

		debounceTimeout = setTimeout(async () => {
			console.log(`Processing ${changedFiles.size} changed files...`);
			for (const filePath of changedFiles) {
				await processFile(filePath);
			}
			changedFiles.clear();
			debounceTimeout = null;
		}, 1000);
	}
}
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
const changedFiles = new Set<string>();
let debounceTimeout: ReturnType<typeof setTimeout> | null = null;

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		changedFiles.add(event.path);

		if (debounceTimeout) {
			clearTimeout(debounceTimeout);
		}

		debounceTimeout = setTimeout(async () => {
			console.log(`Processing ${changedFiles.size} changed files...`);
			for (const filePath of changedFiles) {
				await processFile(filePath);
			}
			changedFiles.clear();
			debounceTimeout = null;
		}, 1000);
	}
}
```

### Watch with non-recursive mode

Watch only the top level of a directory, without descending into subdirectories:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

// Only watch root-level config files
const stream = await sandbox.watch("/workspace", {
	include: ["package.json", "tsconfig.json", "vite.config.ts"],
	recursive: false,
});

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		console.log("Configuration changed, rebuilding project...");
		await sandbox.exec("npm run build");
	}
}
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

// Only watch root-level config files
const stream = await sandbox.watch("/workspace", {
	include: ["package.json", "tsconfig.json", "vite.config.ts"],
	recursive: false,
});

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		console.log("Configuration changed, rebuilding project...");
		await sandbox.exec("npm run build");
	}
}
```

## Stop a watch

The stream ends naturally when the container sleeps or shuts down. There are two ways to stop a watch early:

### Use an AbortController

Pass an `AbortSignal` to `parseSSEStream`. Aborting the signal cancels the stream reader, which propagates cleanup to the server. This is the recommended approach when you need to cancel the watch from outside the consuming loop:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
const controller = new AbortController();

// Cancel after 60 seconds
setTimeout(() => controller.abort(), 60_000);

for await (const event of parseSSEStream(stream, controller.signal)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
	}
}

console.log("Watch stopped");
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
const controller = new AbortController();

// Cancel after 60 seconds
setTimeout(() => controller.abort(), 60_000);

for await (const event of parseSSEStream<FileWatchSSEEvent>(
	stream,
	controller.signal,
)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
	}
}

console.log("Watch stopped");
```

### Break out of the loop

Breaking out of the `for await` loop also cancels the stream:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
let eventCount = 0;

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
		eventCount++;

		// Stop after 100 events
		if (eventCount >= 100) {
			break; // Breaking out of the loop cancels the stream
		}
	}
}

console.log("Watch stopped");
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
let eventCount = 0;

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
		eventCount++;

		// Stop after 100 events
		if (eventCount >= 100) {
			break; // Breaking out of the loop cancels the stream
		}
	}
}

console.log("Watch stopped");
```

## Best practices

### Use server-side filtering

Filter with `include` or `exclude` patterns rather than filtering events in JavaScript. Server-side filtering happens at the inotify level, which reduces the number of events sent over the network.

Note

`include` and `exclude` are mutually exclusive. Use one or the other, not both. If you need to watch specific file types while ignoring certain directories, use `include` patterns that match the files you want.

```js
import { parseSSEStream } from "@cloudflare/sandbox";

// Efficient: filtering happens at the inotify level
const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts"],
});

// Less efficient: all events are sent and then filtered in JavaScript
const stream2 = await sandbox.watch("/workspace/src");
for await (const event of parseSSEStream(stream2)) {
	if (event.type === "event") {
		if (!event.path.endsWith(".ts")) continue;
		// Handle event
	}
}
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

// Efficient: filtering happens at the inotify level
const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts"],
});

// Less efficient: all events are sent and then filtered in JavaScript
const stream2 = await sandbox.watch("/workspace/src");
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream2)) {
	if (event.type === "event") {
		if (!event.path.endsWith(".ts")) continue;
		// Handle event
	}
}
```

### Handle errors in event processing

Errors in your event handler do not stop the watch stream. Wrap handler logic in `try...catch` to prevent unhandled exceptions:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		try {
			await handleFileChange(event.eventType, event.path);
		} catch (error) {
			console.error(
				`Failed to handle ${event.eventType} for ${event.path}:`,
				error,
			);
			// Continue processing events
		}
	}

	if (event.type === "error") {
		console.error("Watch error:", event.error);
	}
}
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		try {
			await handleFileChange(event.eventType, event.path);
		} catch (error) {
			console.error(
				`Failed to handle ${event.eventType} for ${event.path}:`,
				error,
			);
			// Continue processing events
		}
	}

	if (event.type === "error") {
		console.error("Watch error:", event.error);
	}
}
```

### Ensure directories exist before watching

Watching a non-existent path returns an error. Verify the path exists before starting a watch:

```js
const watchPath = "/workspace/src";
const result = await sandbox.exists(watchPath);

if (!result.exists) {
	await sandbox.mkdir(watchPath, { recursive: true });
}

const stream = await sandbox.watch(watchPath, {
	include: ["*.ts"],
});
```

```ts
const watchPath = "/workspace/src";
const result = await sandbox.exists(watchPath);

if (!result.exists) {
	await sandbox.mkdir(watchPath, { recursive: true });
}

const stream = await sandbox.watch(watchPath, {
	include: ["*.ts"],
});
```

## Troubleshooting

### High CPU usage

If watching large directories causes performance issues:

1. Use specific `include` patterns instead of watching everything
2. Exclude large directories like `node_modules` and `dist`
3. Watch specific subdirectories instead of the entire project
4. Use `recursive: false` for shallow monitoring

### Path not found errors

All paths must exist and resolve to within `/workspace`. Relative paths are resolved from `/workspace`.

Container lifecycle

File watchers are automatically stopped when the sandbox sleeps or shuts down. If the sandbox wakes up, you must re-establish watches in your application logic.

## Related resources

* [File Watching API reference](https://developers.cloudflare.com/sandbox/api/file-watching/) — Complete API documentation and types
* [Manage files guide](https://developers.cloudflare.com/sandbox/guides/manage-files/) — File operations
* [Background processes guide](https://developers.cloudflare.com/sandbox/guides/background-processes/) — Long-running processes
* [Stream output guide](https://developers.cloudflare.com/sandbox/guides/streaming-output/) — Real-time output handling

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/file-watching/#page","headline":"Watch filesystem changes · Cloudflare Sandbox SDK docs","description":"Monitor files and directories in real-time to build responsive development tools and automation workflows.","url":"https://developers.cloudflare.com/sandbox/guides/file-watching/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Clone repositories, manage branches, and automate Git operations.
title: Work with Git
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Work with Git

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/git-workflows/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

This guide shows you how to clone repositories, manage branches, and automate Git operations in the sandbox.

Coming soon: Sandbox SDK 1.0

This page documents `sandbox.gitCheckout()` on today's stable `@cloudflare/sandbox` package.

On the **1.0 preview** (`@next`), `gitCheckout` is removed. Run `git` with argv `exec` — for example `sandbox.exec(['git', 'clone', url, dir])` then `await process.output()`. See the [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/) and [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/).

## Clone repositories

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Basic clone
await sandbox.gitCheckout("https://github.com/user/repo");

// Clone specific branch
await sandbox.gitCheckout("https://github.com/user/repo", {
	branch: "develop",
});

// Shallow clone (faster for large repos)
await sandbox.gitCheckout("https://github.com/user/large-repo", {
	depth: 1,
});

// Clone to specific directory
await sandbox.gitCheckout("https://github.com/user/my-app", {
	targetDir: "/workspace/project",
});
```

```plaintext
import { getSandbox } from '@cloudflare/sandbox';

const sandbox = getSandbox(env.Sandbox, 'my-sandbox');

// Basic clone
await sandbox.gitCheckout('https://github.com/user/repo');

// Clone specific branch
await sandbox.gitCheckout('https://github.com/user/repo', {
  branch: 'develop'
});

// Shallow clone (faster for large repos)
await sandbox.gitCheckout('https://github.com/user/large-repo', {
  depth: 1
});

// Clone to specific directory
await sandbox.gitCheckout('https://github.com/user/my-app', {
  targetDir: '/workspace/project'
});
```

## Clone private repositories

Use a personal access token in the URL:

```js
const token = env.GITHUB_TOKEN;
const repoUrl = `https://${token}@github.com/user/private-repo.git`;

await sandbox.gitCheckout(repoUrl);
```

```plaintext
const token = env.GITHUB_TOKEN;
const repoUrl = `https://${token}@github.com/user/private-repo.git`;

await sandbox.gitCheckout(repoUrl);
```

More secure alternative

Embedding a token in the URL passes the credential directly into the sandbox. For better access control, use an outbound handler that injects the real token at request time — the sandbox never holds the credential. Refer to [Handle outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/).

## Clone and build

Clone a repository and run build steps:

```js
await sandbox.gitCheckout("https://github.com/user/my-app");

const repoName = "my-app";

// Install and build
await sandbox.exec(`cd ${repoName} && npm install`);
await sandbox.exec(`cd ${repoName} && npm run build`);

console.log("Build complete");
```

```plaintext
await sandbox.gitCheckout('https://github.com/user/my-app');

const repoName = 'my-app';

// Install and build
await sandbox.exec(`cd ${repoName} && npm install`);
await sandbox.exec(`cd ${repoName} && npm run build`);

console.log('Build complete');
```

## Work with branches

```js
await sandbox.gitCheckout("https://github.com/user/repo");

// Switch branches
await sandbox.exec("cd repo && git checkout feature-branch");

// Create new branch
await sandbox.exec("cd repo && git checkout -b new-feature");
```

```plaintext
await sandbox.gitCheckout('https://github.com/user/repo');

// Switch branches
await sandbox.exec('cd repo && git checkout feature-branch');

// Create new branch
await sandbox.exec('cd repo && git checkout -b new-feature');
```

## Make changes and commit

```js
await sandbox.gitCheckout("https://github.com/user/repo");

// Modify a file
const readme = await sandbox.readFile("/workspace/repo/README.md");
await sandbox.writeFile(
	"/workspace/repo/README.md",
	readme.content + "\n\n## New Section",
);

// Commit changes
await sandbox.exec('cd repo && git config user.name "Sandbox Bot"');
await sandbox.exec('cd repo && git config user.email "bot@example.com"');
await sandbox.exec("cd repo && git add README.md");
await sandbox.exec('cd repo && git commit -m "Update README"');
```

```plaintext
await sandbox.gitCheckout('https://github.com/user/repo');

// Modify a file
const readme = await sandbox.readFile('/workspace/repo/README.md');
await sandbox.writeFile('/workspace/repo/README.md', readme.content + '\n\n## New Section');

// Commit changes
await sandbox.exec('cd repo && git config user.name "Sandbox Bot"');
await sandbox.exec('cd repo && git config user.email "bot@example.com"');
await sandbox.exec('cd repo && git add README.md');
await sandbox.exec('cd repo && git commit -m "Update README"');
```

## Best practices

* **Use shallow clones** \- Faster for large repos with `depth: 1`
* **Store credentials securely** \- Use environment variables for tokens
* **Clean up** \- Delete unused repositories to save space

## Troubleshooting

### Authentication fails

Verify your token is set:

```js
if (!env.GITHUB_TOKEN) {
	throw new Error("GITHUB_TOKEN not configured");
}

const repoUrl = `https://${env.GITHUB_TOKEN}@github.com/user/private-repo.git`;
await sandbox.gitCheckout(repoUrl);
```

```plaintext
if (!env.GITHUB_TOKEN) {
  throw new Error('GITHUB_TOKEN not configured');
}

const repoUrl = `https://${env.GITHUB_TOKEN}@github.com/user/private-repo.git`;
await sandbox.gitCheckout(repoUrl);
```

### Large repository timeout

Use shallow clone:

```js
await sandbox.gitCheckout("https://github.com/user/large-repo", {
	depth: 1,
});
```

```plaintext
await sandbox.gitCheckout('https://github.com/user/large-repo', {
  depth: 1
});
```

## Related resources

* [Files API reference](https://developers.cloudflare.com/sandbox/api/files/) \- File operations after cloning
* [Execute commands guide](https://developers.cloudflare.com/sandbox/guides/execute-commands/) \- Run git commands
* [Manage files guide](https://developers.cloudflare.com/sandbox/guides/manage-files/) \- Work with cloned files

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/git-workflows/#page","headline":"Work with Git · Cloudflare Sandbox SDK docs","description":"Clone repositories, manage branches, and automate Git operations.","url":"https://developers.cloudflare.com/sandbox/guides/git-workflows/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Read, write, organize, and synchronize files in the sandbox.
title: Manage files
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Manage files

Last updated May 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/manage-files/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

This guide shows you how to read, write, organize, and synchronize files in the sandbox filesystem.

## Path conventions

File operations support both absolute and relative paths:

* `/workspace` \- Default working directory for application files
* `/tmp` \- Temporary files (may be cleared)
* `/home` \- User home directory

```js
// Absolute paths
await sandbox.writeFile("/workspace/app.js", code);

// Relative paths (session-aware)
const session = await sandbox.createSession();
await session.exec("cd /workspace/my-project");
await session.writeFile("app.js", code); // Writes to /workspace/my-project/app.js
await session.writeFile("src/index.js", code); // Writes to /workspace/my-project/src/index.js
```

```plaintext
// Absolute paths
await sandbox.writeFile('/workspace/app.js', code);

// Relative paths (session-aware)
const session = await sandbox.createSession();
await session.exec('cd /workspace/my-project');
await session.writeFile('app.js', code);  // Writes to /workspace/my-project/app.js
await session.writeFile('src/index.js', code);  // Writes to /workspace/my-project/src/index.js
```

## Write files

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Write text file
await sandbox.writeFile(
	"/workspace/app.js",
	`console.log('Hello from sandbox!');`,
);

// Write JSON
const config = { name: "my-app", version: "1.0.0" };
await sandbox.writeFile(
	"/workspace/config.json",
	JSON.stringify(config, null, 2),
);

// Write binary file (base64)
const buffer = await fetch(imageUrl).then((r) => r.arrayBuffer());
const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
await sandbox.writeFile("/workspace/image.png", base64, { encoding: "base64" });
```

```plaintext
import { getSandbox } from '@cloudflare/sandbox';

const sandbox = getSandbox(env.Sandbox, 'my-sandbox');

// Write text file
await sandbox.writeFile('/workspace/app.js', `console.log('Hello from sandbox!');`);

// Write JSON
const config = { name: 'my-app', version: '1.0.0' };
await sandbox.writeFile('/workspace/config.json', JSON.stringify(config, null, 2));

// Write binary file (base64)
const buffer = await fetch(imageUrl).then(r => r.arrayBuffer());
const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
await sandbox.writeFile('/workspace/image.png', base64, { encoding: 'base64' });
```

## Read files

```js
// Read text file
const file = await sandbox.readFile("/workspace/app.js");
console.log(file.content);

// Read and parse JSON
const configFile = await sandbox.readFile("/workspace/config.json");
const config = JSON.parse(configFile.content);

// Read binary file (v0.10.1 with `rpc` transport)
const imageFile = await sandbox.readFile("/workspace/image.png", {
	encoding: "none",
});
return new Response(imageFile.content, {
	headers: { "Content-Type": imageFile.mimeType },
});
```

```plaintext
// Read text file
const file = await sandbox.readFile('/workspace/app.js');
console.log(file.content);

// Read and parse JSON
const configFile = await sandbox.readFile('/workspace/config.json');
const config = JSON.parse(configFile.content);

// Read binary file (v0.10.1 with `rpc` transport)
const imageFile = await sandbox.readFile('/workspace/image.png', { encoding: 'none' });
return new Response(imageFile.content, {
  headers: { 'Content-Type': imageFile.mimeType }
});
```

Note

For more details on the `rpc` transport please see the [Transport](https://developers.cloudflare.com/sandbox/configuration/transport/) docs.

## Organize files

```js
// Create directories
await sandbox.mkdir("/workspace/src", { recursive: true });
await sandbox.mkdir("/workspace/tests", { recursive: true });

// Rename file
await sandbox.renameFile("/workspace/draft.txt", "/workspace/final.txt");

// Move file
await sandbox.moveFile("/tmp/download.txt", "/workspace/data.txt");

// Delete file
await sandbox.deleteFile("/workspace/temp.txt");
```

```plaintext
// Create directories
await sandbox.mkdir('/workspace/src', { recursive: true });
await sandbox.mkdir('/workspace/tests', { recursive: true });

// Rename file
await sandbox.renameFile('/workspace/draft.txt', '/workspace/final.txt');

// Move file
await sandbox.moveFile('/tmp/download.txt', '/workspace/data.txt');

// Delete file
await sandbox.deleteFile('/workspace/temp.txt');
```

## Batch operations

Write multiple files in parallel:

```js
const files = {
	"/workspace/src/app.js": 'console.log("app");',
	"/workspace/src/utils.js": 'console.log("utils");',
	"/workspace/README.md": "# My Project",
};

await Promise.all(
	Object.entries(files).map(([path, content]) =>
		sandbox.writeFile(path, content),
	),
);
```

```plaintext
const files = {
  '/workspace/src/app.js': 'console.log("app");',
  '/workspace/src/utils.js': 'console.log("utils");',
  '/workspace/README.md': '# My Project'
};

await Promise.all(
  Object.entries(files).map(([path, content]) =>
    sandbox.writeFile(path, content)
  )
);
```

## Check if file exists

```js
const result = await sandbox.exists("/workspace/config.json");
if (!result.exists) {
	// Create default config
	await sandbox.writeFile("/workspace/config.json", "{}");
}

// Check directory
const dirResult = await sandbox.exists("/workspace/data");
if (!dirResult.exists) {
	await sandbox.mkdir("/workspace/data");
}

// Also available on sessions
const sessionResult = await session.exists("/workspace/temp.txt");
```

```plaintext
const result = await sandbox.exists('/workspace/config.json');
if (!result.exists) {
  // Create default config
  await sandbox.writeFile('/workspace/config.json', '{}');
}

// Check directory
const dirResult = await sandbox.exists('/workspace/data');
if (!dirResult.exists) {
  await sandbox.mkdir('/workspace/data');
}

// Also available on sessions
const sessionResult = await session.exists('/workspace/temp.txt');
```

## Best practices

* **Use `/workspace`** \- Default working directory for app files
* **Use absolute paths** \- Always use full paths like `/workspace/file.txt`
* **Batch operations** \- Use `Promise.all()` for multiple independent file writes
* **Create parent directories** \- Use `recursive: true` when creating nested paths
* **Handle errors** \- Check for `FILE_NOT_FOUND` errors gracefully

## Troubleshooting

### Directory doesn't exist

Create parent directories first:

```js
// Create directory, then write file
await sandbox.mkdir("/workspace/data", { recursive: true });
await sandbox.writeFile("/workspace/data/file.txt", content);
```

```plaintext
// Create directory, then write file
await sandbox.mkdir('/workspace/data', { recursive: true });
await sandbox.writeFile('/workspace/data/file.txt', content);
```

### Binary file encoding

Use `encoding: "none"` (with `rpc` transport) for binary files:

```js
// Write binary
await sandbox.writeFile("/workspace/image.png", readableStream);

// Read binary
const file = await sandbox.readFile("/workspace/image.png", {
	encoding: "none",
});
```

```plaintext
// Write binary
await sandbox.writeFile('/workspace/image.png', readableStream);

// Read binary
const file = await sandbox.readFile('/workspace/image.png', {
  encoding: 'none'
});
```

For older SDK versions or `http` transport:

```js
// Write binary
await sandbox.writeFile("/workspace/image.png", base64data, {
	encoding: "base64",
});

// Read binary
const file = await sandbox.readFile("/workspace/image.png", {
	encoding: "base64",
});
```

```plaintext
// Write binary
await sandbox.writeFile('/workspace/image.png', base64data, { encoding: "base64" });

// Read binary
const file = await sandbox.readFile('/workspace/image.png', {
  encoding: 'base64'
});
```

### Base64 validation errors

When writing with `encoding: 'base64'`, content must contain only valid base64 characters:

```js
try {
	// Invalid: contains invalid base64 characters
	await sandbox.writeFile("/workspace/data.bin", "invalid!@#$", {
		encoding: "base64",
	});
} catch (error) {
	if (error.code === "VALIDATION_FAILED") {
		// Content contains invalid base64 characters
		console.error("Invalid base64 content");
	}
}
```

```plaintext
try {
  // Invalid: contains invalid base64 characters
  await sandbox.writeFile('/workspace/data.bin', 'invalid!@#$', {
    encoding: 'base64'
  });
} catch (error) {
  if (error.code === 'VALIDATION_FAILED') {
    // Content contains invalid base64 characters
    console.error('Invalid base64 content');
  }
}
```

## Related resources

* [Files API reference](https://developers.cloudflare.com/sandbox/api/files/) \- Complete method documentation
* [Execute commands guide](https://developers.cloudflare.com/sandbox/guides/execute-commands/) \- Run file operations with commands
* [Git workflows guide](https://developers.cloudflare.com/sandbox/guides/git-workflows/) \- Clone and manage repositories
* [Code Interpreter guide](https://developers.cloudflare.com/sandbox/guides/code-execution/) \- Generate and execute code files

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/manage-files/#page","headline":"Manage files · Cloudflare Sandbox SDK docs","description":"Read, write, organize, and synchronize files in the sandbox.","url":"https://developers.cloudflare.com/sandbox/guides/manage-files/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-13","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Mount S3-compatible object storage as local filesystems for persistent data storage.
title: Mount buckets
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Mount buckets

Last updated Sep 1, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/mount-buckets/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Mount S3-compatible object storage buckets as local filesystem paths. Access object storage using standard file operations. For Cloudflare R2 in production, you can also mount by Worker R2 binding name so credentials stay in the Worker runtime.

Mounting \`/workspace\`

Mounting a bucket at `/workspace` or a subpath under it can be confusing in app or project setups. In production, the mount overlays that path instead of merging with files already in the image or template. If you want `/workspace` itself to persist over time, [Backup and restore](https://developers.cloudflare.com/sandbox/guides/backup-restore/) is often a better fit.

S3-compatible providers

The SDK works with any S3-compatible object storage provider. Examples include Cloudflare R2, Amazon S3, Google Cloud Storage, Backblaze B2, MinIO, and [many others ↗](https://github.com/s3fs-fuse/s3fs-fuse/wiki/Non-Amazon-S3). The SDK automatically detects and optimizes for R2, S3, and GCS.

## Production prerequisites for R2 binding mounts

To mount an R2 bucket in production without passing credentials into the container, add an R2 binding and export `ContainerProxy` from your Worker entrypoint.

```jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "r2_buckets": [
    {
      "binding": "MY_BUCKET",
      "bucket_name": "my-r2-bucket"
    }
  ]
}
```

```toml
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-r2-bucket"
```

```js
import { ContainerProxy } from "@cloudflare/sandbox";

export { ContainerProxy };
```

```typescript
import { ContainerProxy } from "@cloudflare/sandbox";

export { ContainerProxy };
```

When you omit `endpoint`, the first argument to `mountBucket()` must be the Worker R2 binding name, such as `MY_BUCKET`.

## When to mount buckets

Mount S3-compatible buckets when you need:

* **Persistent data** \- Data survives sandbox destruction
* **Large datasets** \- Process data without downloading
* **Shared storage** \- Multiple sandboxes access the same data
* **Cost-effective persistence** \- Cheaper than keeping sandboxes alive

## Mount an R2 bucket

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "data-processor");

// Mount R2 bucket by Worker binding name
await sandbox.mountBucket("MY_BUCKET", "/data");

// Access bucket with standard filesystem operations
await sandbox.exec("ls", { args: ["/data"] });
await sandbox.writeFile("/data/results.json", JSON.stringify(results));

// Use from Python
await sandbox.exec("python", {
	args: [
		"-c",
		`
import pandas as pd
df = pd.read_csv('/data/input.csv')
df.describe().to_csv('/data/summary.csv')
`,
	],
});
```

```typescript
import { getSandbox } from '@cloudflare/sandbox';

const sandbox = getSandbox(env.Sandbox, 'data-processor');

// Mount R2 bucket by Worker binding name
await sandbox.mountBucket('MY_BUCKET', '/data');

// Access bucket with standard filesystem operations
await sandbox.exec('ls', { args: ['/data'] });
await sandbox.writeFile('/data/results.json', JSON.stringify(results));

// Use from Python
await sandbox.exec('python', { args: ['-c', `
import pandas as pd
df = pd.read_csv('/data/input.csv')
df.describe().to_csv('/data/summary.csv')
`] });
```

In this example, `MY_BUCKET` is the binding name from `wrangler.toml`. It does not have to match the bucket's dashboard name, although many projects use matching names.

Mounting affects entire sandbox

Mounted buckets are visible across all sessions since they share the filesystem. Mount once per sandbox.

## Credentials

R2 binding mounts do not require credentials. Remote endpoint mounts remain supported for Cloudflare R2 and other S3-compatible providers, and those flows can still use automatic credential detection or explicit credentials.

### Automatic detection

When you include an `endpoint`, set credentials as Worker secrets and the SDK automatically detects them:

```sh
npx wrangler secret put R2_ACCESS_KEY_ID
npx wrangler secret put R2_SECRET_ACCESS_KEY
```

R2 credentials

We also automatically detect `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` for compatibility with other S3-compatible providers.

```js
// Credentials automatically detected from environment for remote endpoint mounts
await sandbox.mountBucket("my-r2-bucket", "/data", {
	endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
});
```

```typescript
// Credentials automatically detected from environment for remote endpoint mounts
await sandbox.mountBucket('my-r2-bucket', '/data', {
	endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com'
});
```

### Explicit credentials

Pass credentials directly when needed:

```js
await sandbox.mountBucket("my-r2-bucket", "/data", {
	endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
	credentials: {
		accessKeyId: env.R2_ACCESS_KEY_ID,
		secretAccessKey: env.R2_SECRET_ACCESS_KEY,
	},
});
```

```typescript
await sandbox.mountBucket('my-r2-bucket', '/data', {
	endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',
	credentials: {
		accessKeyId: env.R2_ACCESS_KEY_ID,
		secretAccessKey: env.R2_SECRET_ACCESS_KEY
	}
});
```

### Credential proxy

When you mount with explicit credentials, s3fs writes those credentials to a password file on the container's disk. A compromised container process can read and exfiltrate the credentials, or use them to access storage outside the intended bucket scope.

Set `credentialProxy: true` to keep credentials out of the container entirely. Instead of passing real credentials into the container, the Durable Object intercepts all outbound S3 requests at the network layer, re-signs them with the real credentials, and forwards them upstream. The container only ever holds dummy credentials that are useless outside the proxy.

This works with [AWS SigV4 ↗](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) signing for S3-compatible endpoints (including R2) and HMAC signing for Google Cloud Storage. It is recommended to set `credentialProxy: true` for all endpoint mounts. The option defaults to `false` for backwards compatibility and will become the default in a future version of the Sandbox SDK.

```js
await sandbox.mountBucket("my-bucket", "/data", {
	endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
	provider: "r2",
	credentials: {
		accessKeyId: env.R2_ACCESS_KEY_ID,
		secretAccessKey: env.R2_SECRET_ACCESS_KEY,
	},
	credentialProxy: true,
});
```

```typescript
await sandbox.mountBucket('my-bucket', '/data', {
	endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',
	provider: 'r2',
	credentials: {
		accessKeyId: env.R2_ACCESS_KEY_ID,
		secretAccessKey: env.R2_SECRET_ACCESS_KEY
	},
	credentialProxy: true
});
```

ContainerProxy export required

Credential proxy mounts use egress interception and require `ContainerProxy` to be exported from your Worker entrypoint. Without this export, the proxy cannot intercept requests and the mount will fail.

```typescript
import { ContainerProxy } from "@cloudflare/sandbox";

export { ContainerProxy };
```

## Mount bucket subdirectories

Mount a specific subdirectory within a bucket using the `prefix` option. Only contents under the prefix are visible at the mount point:

```js
// Mount only the /uploads/images/ subdirectory
await sandbox.mountBucket("MY_BUCKET", "/images", {
	prefix: "/uploads/images/",
});

// Files appear at mount point without the prefix
// Bound bucket: my-r2-bucket/uploads/images/photo.jpg
// Mounted path: /images/photo.jpg
await sandbox.exec("ls", { args: ["/images"] });

// Write to subdirectory
await sandbox.writeFile("/images/photo.jpg", imageData);
// Creates my-r2-bucket/uploads/images/photo.jpg

// Mount different prefixes to different paths
await sandbox.mountBucket("MY_BUCKET", "/training-data", {
	prefix: "/ml/training/",
});

await sandbox.mountBucket("MY_BUCKET", "/test-data", {
	prefix: "/ml/testing/",
});
```

```typescript
// Mount only the /uploads/images/ subdirectory
await sandbox.mountBucket('MY_BUCKET', '/images', {
	prefix: '/uploads/images/'
});

// Files appear at mount point without the prefix
// Bound bucket: my-r2-bucket/uploads/images/photo.jpg
// Mounted path: /images/photo.jpg
await sandbox.exec('ls', { args: ['/images'] });

// Write to subdirectory
await sandbox.writeFile('/images/photo.jpg', imageData);
// Creates my-r2-bucket/uploads/images/photo.jpg

// Mount different prefixes to different paths
await sandbox.mountBucket('MY_BUCKET', '/training-data', {
	prefix: '/ml/training/'
});

await sandbox.mountBucket('MY_BUCKET', '/test-data', {
	prefix: '/ml/testing/'
});
```

Prefix format

The `prefix` must start with `/` (for example, `/data` or `/logs/2024/`).

## Read-only mounts

Protect data by mounting buckets in read-only mode:

```js
await sandbox.mountBucket("MY_BUCKET", "/data", {
	readOnly: true,
});

// Reads work
await sandbox.exec("cat", { args: ["/data/dataset.csv"] });

// Writes fail
await sandbox.writeFile("/data/new-file.txt", "data"); // Error: Read-only filesystem
```

```typescript
await sandbox.mountBucket('MY_BUCKET', '/data', {
	readOnly: true
});

// Reads work
await sandbox.exec('cat', { args: ['/data/dataset.csv'] });

// Writes fail
await sandbox.writeFile('/data/new-file.txt', 'data');  // Error: Read-only filesystem
```

## Local development

You can also mount R2 buckets during local development with `wrangler dev` by passing the `localBucket` option. Production R2 binding mounts and local `localBucket` mounts both avoid explicit credentials, but they are different execution paths. Production uses credential-less egress interception and overlays the target path. Local development uses periodic synchronization with the R2 binding.

```js
await sandbox.mountBucket("MY_BUCKET", "/data", {
	localBucket: true,
});

// Access files using standard operations
await sandbox.exec("ls", { args: ["/data"] });
await sandbox.writeFile("/data/results.json", JSON.stringify(results));
```

```typescript
await sandbox.mountBucket('MY_BUCKET', '/data', {
	localBucket: true
});

// Access files using standard operations
await sandbox.exec('ls', { args: ['/data'] });
await sandbox.writeFile('/data/results.json', JSON.stringify(results));
```

Note

You can use an environment variable to toggle `localBucket` between local development and production. Set an environment variable such as `LOCAL_DEV` in your Wrangler configuration using `vars` for local development, then reference it in your code:

```typescript
const mountOptions = env.LOCAL_DEV ? { localBucket: true } : {};

await sandbox.mountBucket('MY_BUCKET', '/data', mountOptions);
```

When `localBucket` is `true`, the SDK uses local R2 binding synchronization. When `localBucket` is `false` or omitted and `endpoint` is also omitted, the SDK uses the production R2 binding mount path. For more information on setting environment variables, refer to [Environment variables in Wrangler configuration](https://developers.cloudflare.com/workers/configuration/environment-variables/).

The `readOnly` and `prefix` options work the same way in local mode:

```js
// Read-only local mount
await sandbox.mountBucket("MY_BUCKET", "/data", {
	localBucket: true,
	readOnly: true,
});

// Mount a subdirectory
await sandbox.mountBucket("MY_BUCKET", "/images", {
	localBucket: true,
	prefix: "/uploads/images/",
});
```

```typescript
// Read-only local mount
await sandbox.mountBucket('MY_BUCKET', '/data', {
	localBucket: true,
	readOnly: true
});

// Mount a subdirectory
await sandbox.mountBucket('MY_BUCKET', '/images', {
	localBucket: true,
	prefix: '/uploads/images/'
});
```

### Local development considerations

During local development, files are synchronized between R2 and the container using a periodic sync process rather than a direct filesystem mount. Keep the following in mind:

* **Synchronization window** \- A brief delay exists between when a file is written and when it appears on the other side. For example, if you upload a file to R2 and then immediately read it from the mounted path in the container, the file may not yet be available. Allow a short window for synchronization to complete before reading recently written data.
* **High-frequency writes** \- Rapid successive writes to the same file path may take slightly longer to fully propagate. For best results, avoid writing to the same file from both R2 and the container at the same time.
* **Bidirectional sync** \- Changes made in the container are synced to R2, and changes made in R2 are synced to the container. Both directions follow the same periodic sync model.

Note

These considerations apply to local development with `wrangler dev` only. In production, bucket mounts use a direct filesystem mount with no synchronization delay. Local sync-style behavior may not fully reflect how a production mount overlays the target path.

## Unmount buckets

```js
// Mount for processing
await sandbox.mountBucket("MY_BUCKET", "/data");

// Do work
await sandbox.exec("python process_data.py");

// Clean up
await sandbox.unmountBucket("/data");
```

```typescript
// Mount for processing
await sandbox.mountBucket('MY_BUCKET', '/data');

// Do work
await sandbox.exec('python process_data.py');

// Clean up
await sandbox.unmountBucket('/data');
```

Automatic cleanup

Mounted buckets are automatically unmounted when the sandbox is destroyed. Manual unmounting is optional.

## Other providers

The SDK supports any S3-compatible object storage. Here are examples for common providers:

### Amazon S3

```js
await sandbox.mountBucket("my-s3-bucket", "/data", {
	endpoint: "https://s3.us-west-2.amazonaws.com",
	credentials: {
		accessKeyId: env.AWS_ACCESS_KEY_ID,
		secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
	},
});
```

```typescript
await sandbox.mountBucket('my-s3-bucket', '/data', {
	endpoint: 'https://s3.us-west-2.amazonaws.com',
	credentials: {
		accessKeyId: env.AWS_ACCESS_KEY_ID,
		secretAccessKey: env.AWS_SECRET_ACCESS_KEY
	}
});
```

### Google Cloud Storage

```js
await sandbox.mountBucket("my-gcs-bucket", "/data", {
	endpoint: "https://storage.googleapis.com",
	credentials: {
		accessKeyId: env.GCS_ACCESS_KEY_ID,
		secretAccessKey: env.GCS_SECRET_ACCESS_KEY,
	},
});
```

```typescript
await sandbox.mountBucket('my-gcs-bucket', '/data', {
	endpoint: 'https://storage.googleapis.com',
	credentials: {
		accessKeyId: env.GCS_ACCESS_KEY_ID,
		secretAccessKey: env.GCS_SECRET_ACCESS_KEY
	}
});
```

GCS requires HMAC keys

Generate HMAC keys in GCS console under **Settings** \> **Interoperability**.

### Other S3-compatible providers

For providers like Backblaze B2, MinIO, Wasabi, or others, use the standard mount pattern:

```js
await sandbox.mountBucket("my-bucket", "/data", {
	endpoint: "https://s3.us-west-000.backblazeb2.com",
	credentials: {
		accessKeyId: env.ACCESS_KEY_ID,
		secretAccessKey: env.SECRET_ACCESS_KEY,
	},
});
```

```typescript
await sandbox.mountBucket('my-bucket', '/data', {
	endpoint: 'https://s3.us-west-000.backblazeb2.com',
	credentials: {
		accessKeyId: env.ACCESS_KEY_ID,
		secretAccessKey: env.SECRET_ACCESS_KEY
	}
});
```

For provider-specific configuration, see the [s3fs-fuse wiki ↗](https://github.com/s3fs-fuse/s3fs-fuse/wiki/Non-Amazon-S3) for supported providers and recommended flags.

## Troubleshooting

### R2 binding not found error

**Error**: `R2 binding "MY_BUCKET" not found in Worker env`

**Solution**: Ensure your Worker has an `r2_buckets` binding and that `mountBucket()` uses the binding name, not the bucket's dashboard name:

```jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "r2_buckets": [
    {
      "binding": "MY_BUCKET",
      "bucket_name": "my-r2-bucket"
    }
  ]
}
```

```toml
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-r2-bucket"
```

### Credential-less R2 mount fails immediately

**Solution**: Ensure your Worker entrypoint exports `ContainerProxy`. If you are using an older Wrangler version, you may also need the `enable_ctx_exports` compatibility flag.

### Missing credentials error

**Error**: `MissingCredentialsError: No credentials found`

**Solution**: This error only applies when you mount a remote S3-compatible endpoint by setting `endpoint`. Set credentials as Worker secrets:

```sh
npx wrangler secret put R2_ACCESS_KEY_ID
npx wrangler secret put R2_SECRET_ACCESS_KEY
```

or

```sh
npx wrangler secret put AWS_ACCESS_KEY_ID
npx wrangler secret put AWS_SECRET_ACCESS_KEY
```

### Mount failed error

**Error**: `S3FSMountError: mount failed`

**Common causes**:

* Incorrect endpoint URL
* Invalid credentials
* Missing `ContainerProxy` export, or on older Wrangler versions missing `enable_ctx_exports`
* Bucket does not exist
* Network connectivity issues

Verify your binding or endpoint configuration:

```js
try {
	await sandbox.mountBucket("MY_BUCKET", "/data");
} catch (error) {
	console.error("Mount failed:", error.message);
	// Check binding name, ContainerProxy export, or remote endpoint configuration
}
```

```typescript
try {
	await sandbox.mountBucket('MY_BUCKET', '/data');
} catch (error) {
	console.error('Mount failed:', error.message);
	// Check binding name, ContainerProxy export, or remote endpoint configuration
}
```

### Path already mounted error

**Error**: `InvalidMountConfigError: Mount path already in use`

**Solution**: Unmount first or use a different path:

```js
// Unmount existing
await sandbox.unmountBucket("/data");

// Or use different path
await sandbox.mountBucket("bucket2", "/storage", { endpoint: "..." });
```

```typescript
// Unmount existing
await sandbox.unmountBucket('/data');

// Or use different path
await sandbox.mountBucket('bucket2', '/storage', { endpoint: '...' });
```

### Slow file access

File operations on mounted buckets are slower than local filesystem due to network latency.

**Solution**: Copy frequently accessed files locally:

```js
// Copy to local filesystem
await sandbox.exec("cp", {
	args: ["/data/large-dataset.csv", "/workspace/dataset.csv"],
});

// Work with local copy (faster)
await sandbox.exec("python", {
	args: ["process.py", "/workspace/dataset.csv"],
});

// Save results back to bucket
await sandbox.exec("cp", {
	args: ["/workspace/results.json", "/data/results/output.json"],
});
```

```typescript
// Copy to local filesystem
await sandbox.exec('cp', { args: ['/data/large-dataset.csv', '/workspace/dataset.csv'] });

// Work with local copy (faster)
await sandbox.exec('python', { args: ['process.py', '/workspace/dataset.csv'] });

// Save results back to bucket
await sandbox.exec('cp', { args: ['/workspace/results.json', '/data/results/output.json'] });
```

## Best practices

* **Mount early** \- Mount buckets at sandbox initialization
* **Choose the right mount mode** \- Use R2 binding mounts when you want Worker-managed R2 access, or use `endpoint` for explicit R2, S3, GCS, and other S3-compatible providers
* **Secure credentials** \- Always use Worker secrets, never hardcode
* **Read-only when possible** \- Protect data with read-only mounts
* **Mount the narrowest path** \- Use prefixes to expose only the data a sandbox needs
* **Mount paths** \- Prefer `/data`, `/storage`, or `/mnt/*`; if you mount under `/workspace`, account for the mount overlaying that path in production
* **Handle errors** \- Wrap mount operations in `try...catch` blocks
* **Optimize access** \- Copy frequently accessed files locally

## Related resources

* [Persistent storage tutorial](https://developers.cloudflare.com/sandbox/tutorials/persistent-storage/) \- Complete R2 example
* [Backup and restore](https://developers.cloudflare.com/sandbox/guides/backup-restore/) \- Persist a project directory such as `/workspace`
* [Storage API reference](https://developers.cloudflare.com/sandbox/api/storage/) \- Full method documentation
* [Environment variables](https://developers.cloudflare.com/sandbox/configuration/environment-variables/) \- Credential configuration for remote endpoint mounts
* [Wrangler configuration](https://developers.cloudflare.com/sandbox/configuration/wrangler/) \- Configure R2 bindings and compatibility flags
* [R2 documentation](https://developers.cloudflare.com/r2/) \- Learn about Cloudflare R2
* [Outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/) \- Learn how `ContainerProxy` and outbound interception work

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/mount-buckets/#page","headline":"Mount buckets · Cloudflare Sandbox SDK docs","description":"Mount S3-compatible object storage as local filesystems for persistent data storage.","url":"https://developers.cloudflare.com/sandbox/guides/mount-buckets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-09-01","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Intercept and handle outbound HTTP from sandboxes using Workers.
title: Handle outbound traffic
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Handle outbound traffic

Last updated Aug 28, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Outbound handlers let you intercept and modify HTTP traffic from a sandbox with trusted code.

Use them to:

* Allow or deny specific origin destinations
* Safely inject authorization headers or tokens
* Transparently reroute traffic
* Add custom policy on outbound traffic (such as denying specific HTTP requests)
* [Connect to Workers bindings](https://developers.cloudflare.com/sandbox/guides/workers-connections/) like KV, R2, and Durable Objects

## Block outbound traffic

Use `enableInternet = false` to block public internet access by default:

```js
import { Sandbox } from "@cloudflare/sandbox";

export class MySandbox extends Sandbox {
	enableInternet = false;
}
```

```ts
import { Sandbox } from "@cloudflare/sandbox";

export class MySandbox extends Sandbox {
	enableInternet = false;
}
```

When `enableInternet` is `false`, only traffic you explicitly allow later on this page through `allowedHosts` or outbound handlers can leave the sandbox. Only ports `80`, `443`, and DNS are available, and DNS queries use Cloudflare's DNS servers.

Note

`enableInternet` takes effect when the sandbox starts. Changes to `outbound` handlers and related outbound policies can affect a live-running sandbox without restarting it.

## Block or allow traffic by host

You can filter outbound traffic with the `allowedHosts` and `deniedHosts` properties on the Sandbox class.

Note

Export `ContainerProxy` from your Worker entrypoint for outbound interception to work.

When `allowedHosts` is set, it becomes a deny-by-default allowlist. Any host or IP not in the list is denied, and only matching destinations can reach `outbound` or `outboundByHost` handlers.

`allowedHosts` and `deniedHosts` also support simple glob patterns where `*` matches any sequence of characters.

By default, a Sandbox allows internet access, and you can set `deniedHosts` to disallow specific hosts or IPs:

```js
import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };

export class MySandbox extends Sandbox {
	deniedHosts = ["some-nefarious-website.com", "141.101.64.0/18"];
}
```

```ts
import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };

export class MySandbox extends Sandbox {
	deniedHosts = ["some-nefarious-website.com", "141.101.64.0/18"];
}
```

You can also disable internet access by default, but allow specific hosts and IPs:

```js
import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };

export class MySandbox extends Sandbox {
	// default internet access to off unless overridden by 'allowedHosts' or outbound proxy
	enableInternet = false;

	// overrides enableInternet = false
	allowedHosts = ["allowed.com"];
}
```

```ts
import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };

export class MySandbox extends Sandbox {
	// default internet access to off unless overridden by 'allowedHosts' or outbound proxy
	enableInternet = false;

	// overrides enableInternet = false
	allowedHosts = ["allowed.com"];
}
```

## Define outbound handlers

Outbound handlers are programmable egress proxies that run on the same machine as the sandbox. They have access to all Workers bindings.

Use `outbound` to intercept all outbound HTTP and HTTPS traffic:

```js
import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };

export class MySandbox extends Sandbox {}

MySandbox.outbound = async (request, env, ctx) => {
	if (request.method !== "GET") {
		console.log(`Blocked ${request.method} to ${request.url}`);
		return new Response("Method Not Allowed", { status: 405 });
	}
	return fetch(request);
};
```

```ts
import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };

export class MySandbox extends Sandbox {}

MySandbox.outbound = async (
	request: Request,
	env: Env,
	ctx: OutboundHandlerContext,
) => {
	if (request.method !== "GET") {
		console.log(`Blocked ${request.method} to ${request.url}`);
		return new Response("Method Not Allowed", { status: 405 });
	}
	return fetch(request);
};
```

Note

HTTP requests to the outbound handler remain secure because they run on the same machine as the sandbox. You can upgrade requests to HTTPS from the Worker itself to prevent plain-text traffic from reaching the internet.

Use `outboundByHost` to map specific domain names or IP addresses to handler functions:

```js
import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };

export class MySandbox extends Sandbox {}

MySandbox.outboundByHost = {
	"my.worker": async (request, env, ctx) => {
		// Run arbitrary Workers logic from this hostname
		return await someWorkersFunction(request.body);
	},
};
```

```ts
import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };

export class MySandbox extends Sandbox {}

MySandbox.outboundByHost = {
	"my.worker": async (
		request: Request,
		env: Env,
		ctx: OutboundHandlerContext,
	) => {
		// Run arbitrary Workers logic from this hostname
		return await someWorkersFunction(request.body);
	},
};
```

Calls to `http://my.worker` from the sandbox invoke the handler, which runs inside the Workers runtime, outside the sandbox.

`deniedHosts` and `allowedHosts` are evaluated before any outbound handler. If you use `allowedHosts`, include the hostname there for either `outbound` or `outboundByHost` to run. `outboundByHost` handlers take precedence over catch-all `outbound` handlers.

## Securely inject credentials

Because outbound handlers run in the Workers runtime — outside the sandbox — they can hold secrets that the sandbox itself never sees. The sandbox makes a plain HTTP request, and the handler attaches the credential before forwarding it to the upstream service.

```js
export class MySandbox extends Sandbox {}

MySandbox.outboundByHost = {
	"github.com": (request, env, ctx) => {
		const requestWithAuth = new Request(request);
		requestWithAuth.headers.set("x-auth-token", env.SECRET);
		return fetch(requestWithAuth);
	},
};
```

```ts
export class MySandbox extends Sandbox {}

MySandbox.outboundByHost = {
	"github.com": (request: Request, env: Env, ctx: OutboundHandlerContext) => {
		const requestWithAuth = new Request(request);
		requestWithAuth.headers.set("x-auth-token", env.SECRET);
		return fetch(requestWithAuth);
	},
};
```

This is especially useful for agentic workloads where you cannot fully trust the code running inside the sandbox. With this pattern:

* **No token is exposed to the sandbox.** The secret lives in the Worker's environment and is never passed into the sandbox.
* **No token rotation inside the sandbox.** Rotate the secret in your Worker's environment and every request picks it up immediately.
* **Per-host and per-instance rules.** Combine `outboundByHost` with `ctx.containerId` to scope credentials or permissions to a specific sandbox instance.

Here, `ctx.containerId` looks up a per-instance key from KV:

```js
export class MySandbox extends Sandbox {}

MySandbox.outboundByHost = {
	"my-internal-vcs.dev": async (request, env, ctx) => {
		const authKey = await env.KEYS.get(ctx.containerId);

		const requestWithAuth = new Request(request);
		requestWithAuth.headers.set("x-auth-token", authKey);
		return fetch(requestWithAuth);
	},
};
```

```ts
export class MySandbox extends Sandbox {}

MySandbox.outboundByHost = {
	"my-internal-vcs.dev": async (
		request: Request,
		env: Env,
		ctx: OutboundHandlerContext,
	) => {
		const authKey = await env.KEYS.get(ctx.containerId);

		const requestWithAuth = new Request(request);
		requestWithAuth.headers.set("x-auth-token", authKey);
		return fetch(requestWithAuth);
	},
};
```

## HTTPS traffic

Sandboxes intercept HTTPS traffic by default — `interceptHttps` is set to `true` on the Sandbox class.

When HTTPS interception is active, an ephemeral CA file is created at `/etc/cloudflare/certs/cloudflare-containers-ca.crt` once the sandbox starts.

The Sandbox runtime makes a best effort to trust this CA automatically regardless of distro. On startup, it checks common system CA bundle locations across major Linux families and configures common CA environment variables so runtimes like Node.js, `curl`, Python `requests`, and Git trust the certificate automatically.

Note

HTTP communication to the outbound handler is encrypted by the networking stack. For traffic that stays within the Cloudflare Developer Platform, plain HTTP is secure.

## Non-HTTP traffic

Outbound handlers only intercept HTTP and HTTPS traffic. Traffic on ports other than `80` and `443` is never routed through `outbound` or `outboundByHost`.

If you set `enableInternet = false`, that traffic is denied. DNS queries are the one exception, but they only go to Cloudflare's DNS servers. That prevents using arbitrary DNS destinations for data exfiltration.

## Change policies at runtime

Use `outboundHandlers` to define named handlers, then assign them to specific hosts at runtime using `setOutboundByHost()`. You can also apply a handler globally with `setOutboundHandler()`.

You can also manage runtime policy with `setOutboundByHosts()`, `setAllowedHosts()`, `setDeniedHosts()`, `allowHost()`, `denyHost()`, `removeAllowedHost()`, and `removeDeniedHost()`.

This lets a trusted Worker hold credentials without exposing them to an untrusted sandbox:

```js
import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };

export class MySandbox extends Sandbox {}

MySandbox.outboundHandlers = {
	authenticatedGithub: async (request, env, ctx) => {
		const githubToken = env.GITHUB_TOKEN;
		return authenticateGitHttpsRequest(request, githubToken, ctx.containerId);
	},
};
```

```ts
import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";
export { ContainerProxy };

export class MySandbox extends Sandbox {}

MySandbox.outboundHandlers = {
	authenticatedGithub: async (
		request: Request,
		env: Env,
		ctx: OutboundHandlerContext,
	) => {
		const githubToken = env.GITHUB_TOKEN;
		return authenticateGitHttpsRequest(request, githubToken, ctx.containerId);
	},
};
```

Apply handlers to hosts programmatically from your Worker:

```js
import { Sandbox, ContainerProxy, getSandbox } from "@cloudflare/sandbox";
export { ContainerProxy };

export default {
	async fetch(request, env) {
		const sandbox = getSandbox(env.Sandbox, "agent-session");

		// Give the sandbox access to github.com during setup
		await sandbox.setOutboundByHost("github.com", "authenticatedGithub");
		await sandbox.exec("node setup.js");

		// Remove access once setup is complete
		await sandbox.removeOutboundByHost("github.com");
	},
};
```

```ts
import { Sandbox, ContainerProxy, getSandbox } from "@cloudflare/sandbox";
export { ContainerProxy };

export default {
	async fetch(request: Request, env: Env) {
		const sandbox = getSandbox(env.Sandbox, "agent-session");

		// Give the sandbox access to github.com during setup
		await sandbox.setOutboundByHost("github.com", "authenticatedGithub");
		await sandbox.exec("node setup.js");

		// Remove access once setup is complete
		await sandbox.removeOutboundByHost("github.com");
	},
};
```

## Handler precedence

Requests are evaluated in this order:

1. `deniedHosts` is checked first. Matching hosts or IPs are denied immediately.
2. `allowedHosts` is checked next. When it is set, any host or IP not in the list is denied. Matching hosts continue to outbound handlers, or egress to the public internet if no handler is set.
3. Instance-level rules set with `setOutboundByHost()` are checked before class-level `outboundByHost` rules.
4. Per-host handlers always take precedence over catch-all handlers, so `outboundByHost` runs before `outbound`.
5. Instance-level handlers set with `setOutboundHandler()` are checked before the class-level `outbound` handler.
6. If no handler matches, the request can still egress to the public internet when it matched `allowedHosts` or `enableInternet = true`. Otherwise, it is denied.

## Local development

`wrangler dev` supports outbound interception. A sidecar process is spawned inside the sandbox's network namespace. It applies `TPROXY` rules to route matching traffic to the local Workerd instance, mirroring production behavior.

## Related resources

* [Connect to Workers bindings](https://developers.cloudflare.com/sandbox/guides/workers-connections/) — Access KV, R2, Durable Objects, and other bindings from a sandbox
* [Handle outbound traffic (Containers)](https://developers.cloudflare.com/containers/guides/outbound-traffic/) — Container SDK API for outbound handlers
* [Sandbox options](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/) — Configure sandbox behavior
* [Environment variables](https://developers.cloudflare.com/sandbox/configuration/environment-variables/) — Configure secrets and environment variables

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/outbound-traffic/#page","headline":"Handle outbound traffic · Cloudflare Sandbox SDK docs","description":"Intercept and handle outbound HTTP from sandboxes using Workers.","url":"https://developers.cloudflare.com/sandbox/guides/outbound-traffic/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-28","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Set up wildcard DNS, routes, and TLS so exposePort preview URLs work on your domain.
title: Configure preview URLs on a custom domain
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Configure preview URLs on a custom domain

Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Set up wildcard DNS, routes, and TLS so `exposePort()` preview URLs work on your domain. To deploy the Worker and sandbox image, refer to [Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/).

Only required for preview URLs

Custom domain setup is only needed if you use `exposePort()` to expose services from sandboxes. If your application does not use `exposePort()`, you can deploy to `.workers.dev` without this configuration.

For public URLs without custom-domain setup, [sandbox.tunnels](https://developers.cloudflare.com/sandbox/api/tunnels/) is an alternative: quick tunnels for development, named tunnels for stable hostnames in production.

Sandbox SDK 1.0 preview

Command examples that use `startProcess` are stable-only. On **`@next`**, use argv `exec` process handles. Refer to [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/). Package and image still must match on the same release line.

Preview URLs need wildcard DNS because each exposed port gets a unique subdomain: `https://8080-abc123.yourdomain.com`.

The `.workers.dev` domain does not support wildcard subdomains, so preview URLs that must be reachable on a public hostname outside local development need a custom domain.

Subdomain depth matters for TLS

If your Worker runs on a subdomain (for example, `sandbox.yourdomain.com`), preview URLs become second-level wildcards like `*.sandbox.yourdomain.com`. Cloudflare's Universal SSL only covers first-level wildcards (`*.yourdomain.com`), so you need a certificate covering `*.sandbox.yourdomain.com`. Without it, preview URLs will fail with TLS handshake errors.

You have three options:

* **Run the Worker on the apex domain** (`yourdomain.com`) so preview URLs stay at the first level (`*.yourdomain.com`), which Universal SSL covers automatically. This is the simplest option.
* **Use [Advanced Certificate Manager](https://developers.cloudflare.com/ssl/edge-certificates/advanced-certificate-manager/)** ($10/month) to provision a certificate for `*.sandbox.yourdomain.com` through the Cloudflare dashboard.
* **Upload a custom certificate** from a provider like [Let's Encrypt ↗](https://letsencrypt.org/) (free). Generate a wildcard certificate for `*.sandbox.yourdomain.com` using the DNS-01 challenge, then upload it via the Cloudflare dashboard under **SSL/TLS > Edge Certificates > [Custom Certificates](https://developers.cloudflare.com/ssl/edge-certificates/custom-certificates/)**. You will need to renew it before expiry.

## Prerequisites

* Active Cloudflare zone with a domain
* Worker that uses `exposePort()`
* [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) installed
* Sandbox app already [deployable](https://developers.cloudflare.com/sandbox/guides/deploy/) (Worker + image)

## Setup

### Create a wildcard DNS record

In the Cloudflare dashboard, go to your domain and create an A record:

* **Type**: A
* **Name**: `*` (wildcard)
* **IPv4 address**: `192.0.2.0`
* **Proxy status**: Proxied (orange cloud)

This routes all subdomains through Cloudflare's proxy. The IP address `192.0.2.0` is a documentation address (RFC 5737) that Cloudflare recognizes when proxied.

### Configure Worker routes

Add a wildcard route to your Wrangler configuration:

```jsonc
{
	"$schema": "./node_modules/wrangler/config-schema.json",
	"name": "my-sandbox-app",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-09-12",
	"routes": [
		{
			"pattern": "*.yourdomain.com/*",
			"zone_name": "yourdomain.com"
		}
	]
}
```

```toml
"$schema" = "./node_modules/wrangler/config-schema.json"
name = "my-sandbox-app"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-12"

[[routes]]
pattern = "*.yourdomain.com/*"
zone_name = "yourdomain.com"
```

Replace `yourdomain.com` with your actual domain. This routes all subdomain requests to your Worker and enables Cloudflare to provision SSL certificates automatically.

### Apply the route

Redeploy the Worker so the route configuration takes effect:

```sh
npx wrangler deploy
```

If this deploy also changes your sandbox image or package, follow [Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/) for rollout and package/image pairing. For route-only changes you can still use a normal deploy. Use `--containers-rollout=none` only when you intentionally skip container image and instance updates.

## Verify

Test that preview URLs work:

```typescript
// Extract hostname from request
const { hostname } = new URL(request.url);

const sandbox = getSandbox(env.Sandbox, "test-sandbox");
await sandbox.startProcess("python -m http.server 8080");
const exposed = await sandbox.exposePort(8080, { hostname });

console.log(exposed.url);
// https://8080-test-sandbox.yourdomain.com
```

Visit the URL in your browser to confirm your service is accessible.

## Troubleshooting

* **CustomDomainRequiredError**: Verify your Worker is not deployed only to `.workers.dev` and that the wildcard DNS record and route are configured correctly.
* **SSL/TLS errors**: Wait a few minutes for certificate provisioning. Verify the DNS record is proxied and SSL/TLS mode is set to "Full" or "Full (strict)" in your dashboard. If your Worker is on a subdomain (for example, `sandbox.yourdomain.com`), Universal SSL will not cover the second-level wildcard `*.sandbox.yourdomain.com`. Refer to the [TLS caution](#subdomain-depth-matters-for-tls) at the top of this page for options.
* **Preview URL not resolving**: Confirm the wildcard DNS record exists and is proxied. Wait 30–60 seconds for DNS propagation.
* **Port not accessible**: Ensure your service binds to `0.0.0.0` (not `localhost`) and that `proxyToSandbox()` is called first in your Worker's fetch handler.

For detailed troubleshooting, see the [Workers routing documentation](https://developers.cloudflare.com/workers/configuration/routing/).

## Related

* [Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/) \- Deploy Worker and image
* [Preview URLs](https://developers.cloudflare.com/sandbox/concepts/preview-urls/) \- How preview URLs work
* [Expose services](https://developers.cloudflare.com/sandbox/guides/expose-services/) \- Patterns for exposing ports
* [Tunnels API](https://developers.cloudflare.com/sandbox/api/tunnels/) \- Zero-config `*.trycloudflare.com` URLs for development
* [Workers routing](https://developers.cloudflare.com/workers/configuration/routing/) \- Advanced routing configuration
* [Cloudflare DNS](https://developers.cloudflare.com/dns/) \- DNS management

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/#page","headline":"Configure preview URLs on a custom domain · Cloudflare Sandbox SDK docs","description":"Set up wildcard DNS, routes, and TLS so exposePort preview URLs work on your domain.","url":"https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: This outdated Sandbox SDK guide now points to the current guide for controlling sandbox outbound traffic.
title: Proxy requests to external APIs (outdated)
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Proxy requests to external APIs (outdated)

Last updated Aug 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/proxy-requests/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Outdated guide

This guide is outdated. For the current guidance, refer to [Handle outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/).

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)
---

---
description: Handle real-time output from commands and processes.
title: Stream output
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Stream output

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/streaming-output/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

This guide shows you how to handle real-time output from commands, processes, and code execution.

Coming soon: Sandbox SDK 1.0

This page documents streaming helpers on today's stable `@cloudflare/sandbox` package.

In the **1.0 preview** (`@next`), stream with process handle methods such as `logs()` after `exec(argv)`. Refer to [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/) or the [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/).

## When to use streaming

Use streaming when you need:

* **Real-time feedback** \- Show progress as it happens
* **Long-running operations** \- Builds, tests, installations that take time
* **Interactive applications** \- Chat bots, code execution, live demos
* **Large output** \- Process output incrementally instead of all at once
* **User experience** \- Prevent users from waiting with no feedback

Use non-streaming (`exec()`) for:

* **Quick operations** \- Commands that complete in seconds
* **Small output** \- When output fits easily in memory
* **Post-processing** \- When you need complete output before processing

## Stream command execution

Use `execStream()` to get real-time output:

```js
import { getSandbox, parseSSEStream } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const stream = await sandbox.execStream("npm run build");

for await (const event of parseSSEStream(stream)) {
	switch (event.type) {
		case "stdout":
			console.log(event.data);
			break;

		case "stderr":
			console.error(event.data);
			break;

		case "complete":
			console.log("Exit code:", event.exitCode);
			break;

		case "error":
			console.error("Failed:", event.error);
			break;
	}
}
```

```plaintext
import { getSandbox, parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';

const sandbox = getSandbox(env.Sandbox, 'my-sandbox');

const stream = await sandbox.execStream('npm run build');

for await (const event of parseSSEStream<ExecEvent>(stream)) {
  switch (event.type) {
    case 'stdout':
      console.log(event.data);
      break;

    case 'stderr':
      console.error(event.data);
      break;

    case 'complete':
      console.log('Exit code:', event.exitCode);
      break;

    case 'error':
      console.error('Failed:', event.error);
      break;
  }
}
```

## Stream to client

Return streaming output to users via Server-Sent Events:

```js
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		const sandbox = getSandbox(env.Sandbox, "builder");

		const stream = await sandbox.execStream("npm run build");

		return new Response(stream, {
			headers: {
				"Content-Type": "text/event-stream",
				"Cache-Control": "no-cache",
			},
		});
	},
};
```

```plaintext
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const sandbox = getSandbox(env.Sandbox, 'builder');

    const stream = await sandbox.execStream('npm run build');

    return new Response(stream, {
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache'
      }
    });
  }
};
```

Client-side consumption:

```js
// Browser JavaScript
const eventSource = new EventSource("/build");

eventSource.addEventListener("stdout", (event) => {
	const data = JSON.parse(event.data);
	console.log(data.data);
});

eventSource.addEventListener("complete", (event) => {
	const data = JSON.parse(event.data);
	console.log("Exit code:", data.exitCode);
	eventSource.close();
});
```

```plaintext
// Browser JavaScript
const eventSource = new EventSource('/build');

eventSource.addEventListener('stdout', (event) => {
  const data = JSON.parse(event.data);
  console.log(data.data);
});

eventSource.addEventListener('complete', (event) => {
  const data = JSON.parse(event.data);
  console.log('Exit code:', data.exitCode);
  eventSource.close();
});
```

## Stream process logs

Monitor background process output:

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const process = await sandbox.startProcess("node server.js");

const logStream = await sandbox.streamProcessLogs(process.id);

for await (const log of parseSSEStream(logStream)) {
	console.log(log.data);

	if (log.data.includes("Server listening")) {
		console.log("Server is ready");
		break;
	}
}
```

```plaintext
import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox';

const process = await sandbox.startProcess('node server.js');

const logStream = await sandbox.streamProcessLogs(process.id);

for await (const log of parseSSEStream<LogEvent>(logStream)) {
  console.log(log.data);

  if (log.data.includes('Server listening')) {
    console.log('Server is ready');
    break;
  }
}
```

## Handle errors

Check exit codes and handle stream errors:

```js
const stream = await sandbox.execStream("npm run build");

for await (const event of parseSSEStream(stream)) {
	switch (event.type) {
		case "stdout":
			console.log(event.data);
			break;

		case "error":
			throw new Error(`Build failed: ${event.error}`);

		case "complete":
			if (event.exitCode !== 0) {
				throw new Error(`Build failed with exit code ${event.exitCode}`);
			}
			break;
	}
}
```

```plaintext
const stream = await sandbox.execStream('npm run build');

for await (const event of parseSSEStream<ExecEvent>(stream)) {
  switch (event.type) {
    case 'stdout':
      console.log(event.data);
      break;

    case 'error':
      throw new Error(`Build failed: ${event.error}`);

    case 'complete':
      if (event.exitCode !== 0) {
        throw new Error(`Build failed with exit code ${event.exitCode}`);
      }
      break;
  }
}
```

## Best practices

* **Always consume streams** \- Don't let streams hang unconsumed
* **Handle all event types** \- Process stdout, stderr, complete, and error events
* **Check exit codes** \- Non-zero exit codes indicate failure
* **Provide feedback** \- Show progress to users for long operations

## Related resources

* [Commands API reference](https://developers.cloudflare.com/sandbox/api/commands/) \- Complete streaming API
* [Execute commands guide](https://developers.cloudflare.com/sandbox/guides/execute-commands/) \- Command execution patterns
* [Background processes guide](https://developers.cloudflare.com/sandbox/guides/background-processes/) \- Process log streaming
* [Code Interpreter guide](https://developers.cloudflare.com/sandbox/guides/code-execution/) \- Stream code execution output

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/streaming-output/#page","headline":"Stream output · Cloudflare Sandbox SDK docs","description":"Handle real-time output from commands and processes.","url":"https://developers.cloudflare.com/sandbox/guides/streaming-output/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Connect to WebSocket servers running in sandboxes.
title: WebSocket connections
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# WebSocket connections

Last updated May 26, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/websocket-connections/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

This guide shows you how to work with WebSocket servers running in your sandboxes.

## Choose your approach

**Expose via preview URL** \- Get a public URL for external clients to connect to. Best for public chat rooms, multiplayer games, or real-time dashboards.

**Connect with wsConnect()** \- Your Worker establishes the WebSocket connection. Best for custom routing logic, authentication gates, or when your Worker needs real-time data from sandbox services.

## Connect to WebSocket echo server

**Create the echo server:**

```typescript
Bun.serve({
	port: 8080,
	hostname: "0.0.0.0",
	fetch(req, server) {
		if (server.upgrade(req)) {
			return;
		}
		return new Response("WebSocket echo server");
	},
	websocket: {
		message(ws, message) {
			ws.send(`Echo: ${message}`);
		},
		open(ws) {
			console.log("Client connected");
		},
		close(ws) {
			console.log("Client disconnected");
		},
	},
});

console.log("WebSocket server listening on port 8080");
```

**Extend the Dockerfile:**

```dockerfile
FROM docker.io/cloudflare/sandbox:0.3.3

# Copy echo server into the container
COPY echo-server.ts /workspace/echo-server.ts

# Create custom startup script
COPY startup.sh /container-server/startup.sh
RUN chmod +x /container-server/startup.sh
```

**Create startup script:**

```bash
#!/bin/bash
# Start your WebSocket server in the background
bun /workspace/echo-server.ts &
# Start SDK's control plane (needed for the SDK to work)
exec bun dist/index.js
```

**Connect from your Worker:**

```js
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") {
			const sandbox = getSandbox(env.Sandbox, "echo-service");
			return await sandbox.wsConnect(request, 8080);
		}

		return new Response("WebSocket endpoint");
	},
};
```

```ts
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from "@cloudflare/sandbox";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') {
      const sandbox = getSandbox(env.Sandbox, 'echo-service');
      return await sandbox.wsConnect(request, 8080);
    }

    return new Response('WebSocket endpoint');

}
};
```

**Client connects:**

```javascript
const ws = new WebSocket('wss://your-worker.com');
ws.onmessage = (event) => console.log(event.data);
ws.send('Hello!'); // Receives: "Echo: Hello!"
```

## Expose WebSocket service via preview URL

Get a public URL for your WebSocket server:

```js
import { getSandbox, proxyToSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		// Auto-route all requests via proxyToSandbox first
		const proxyResponse = await proxyToSandbox(request, env);
		if (proxyResponse) return proxyResponse;

		// Extract hostname from request
		const { hostname } = new URL(request.url);
		const sandbox = getSandbox(env.Sandbox, "echo-service");

		// Expose the port to get preview URL
		const { url } = await sandbox.exposePort(8080, { hostname });

		// Return URL to clients
		if (request.url.includes("/ws-url")) {
			return Response.json({ url: url.replace("https", "wss") });
		}

		return new Response("Not found", { status: 404 });
	},
};
```

```ts
import { getSandbox, proxyToSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Auto-route all requests via proxyToSandbox first
    const proxyResponse = await proxyToSandbox(request, env);
    if (proxyResponse) return proxyResponse;

    // Extract hostname from request
    const { hostname } = new URL(request.url);
    const sandbox = getSandbox(env.Sandbox, 'echo-service');

    // Expose the port to get preview URL
    const { url } = await sandbox.exposePort(8080, { hostname });

    // Return URL to clients
    if (request.url.includes('/ws-url')) {
      return Response.json({ url: url.replace('https', 'wss') });
    }

    return new Response('Not found', { status: 404 });

}
};
```

Alternative: quick tunnels

Quick tunnels also handle WebSocket upgrades and do not require a custom domain, so they work on `.workers.dev`. Swap `sandbox.exposePort(8080, { hostname })` for `sandbox.tunnels.get(8080)` to get a `*.trycloudflare.com` URL.

**Client connects to preview URL:**

```javascript
// Get the preview URL
const response = await fetch('https://your-worker.com/ws-url');
const { url } = await response.json();

// Connect
const ws = new WebSocket(url);
ws.onmessage = (event) => console.log(event.data);
ws.send('Hello!'); // Receives: "Echo: Hello!"
```

## Connect from Worker to get real-time data

Your Worker can connect to a WebSocket service to get real-time data, even when the incoming request isn't a WebSocket:

```js
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

let initialized = false;

export default {
	async fetch(request, env) {
		// Get or create a sandbox instance
		const sandbox = getSandbox(env.Sandbox, "data-processor");

		// Check for WebSocket upgrade
		const upgrade = request.headers.get("Upgrade")?.toLowerCase();

		if (upgrade === "websocket") {
			// Initialize server on first connection
			if (!initialized) {
				await sandbox.writeFile(
					"/workspace/server.js",
					`Bun.serve({
            port: 8080,
            fetch(req, server) {
              server.upgrade(req);
            },
            websocket: {
              message(ws, msg) {
                ws.send(\`Echo: \${msg}\`);
              }
            }
          });`,
				);
				await sandbox.startProcess("bun /workspace/server.js");
				initialized = true;
			}
			// Connect to WebSocket server
			return await sandbox.wsConnect(request, 8080);
		}

		return new Response("Processed real-time data");
	},
};
```

```ts
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

let initialized = false;

export default {
  async fetch(request: Request, env: Env): Promise<Response> {

     // Get or create a sandbox instance
    const sandbox = getSandbox(env.Sandbox, 'data-processor');


    // Check for WebSocket upgrade
    const upgrade = request.headers.get('Upgrade')?.toLowerCase();

    if (upgrade === 'websocket') {
      // Initialize server on first connection
      if (!initialized) {
        await sandbox.writeFile(
          '/workspace/server.js',
          `Bun.serve({
            port: 8080,
            fetch(req, server) {
              server.upgrade(req);
            },
            websocket: {
              message(ws, msg) {
                ws.send(\`Echo: \${msg}\`);
              }
            }
          });`
        );
        await sandbox.startProcess(
          'bun /workspace/server.js'
        );
        initialized = true;
      }
      // Connect to WebSocket server
      return await sandbox.wsConnect(request, 8080);
    }

    return new Response('Processed real-time data');

}
};
```

This pattern is useful when you need streaming data from sandbox services but want to return HTTP responses to clients.

## Troubleshooting

### Upgrade failed

Verify request has WebSocket headers:

```js
console.log(request.headers.get("Upgrade")); // 'websocket'
console.log(request.headers.get("Connection")); // 'Upgrade'
```

```ts
console.log(request.headers.get('Upgrade'));    // 'websocket'
console.log(request.headers.get('Connection')); // 'Upgrade'
```

### Local development

Expose ports in Dockerfile for `wrangler dev`:

```dockerfile
FROM docker.io/cloudflare/sandbox:0.3.3

COPY echo-server.ts /workspace/echo-server.ts
COPY startup.sh /container-server/startup.sh
RUN chmod +x /container-server/startup.sh

# Required for local development
EXPOSE 8080
```

Note

Port exposure in Dockerfile is only required for local development. In production, all ports are automatically accessible.

## Related resources

* [Ports API reference](https://developers.cloudflare.com/sandbox/api/ports/) \- Complete API documentation
* [Preview URLs concept](https://developers.cloudflare.com/sandbox/concepts/preview-urls/) \- How preview URLs work
* [Tunnels API](https://developers.cloudflare.com/sandbox/api/tunnels/) \- Zero-config `*.trycloudflare.com` URLs for WebSocket services in development
* [Background processes guide](https://developers.cloudflare.com/sandbox/guides/background-processes/) \- Managing long-running services

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/websocket-connections/#page","headline":"WebSocket connections · Cloudflare Sandbox SDK docs","description":"Connect to WebSocket servers running in sandboxes.","url":"https://developers.cloudflare.com/sandbox/guides/websocket-connections/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-26","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Access KV, R2, Durable Objects, and other bindings from a sandbox.
title: Connect to Workers bindings
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Connect to Workers bindings

Last updated Apr 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/guides/workers-connections/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Sandboxes can access [Workers bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) — KV, R2, D1, Durable Objects, and others — through [outbound handlers](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/#define-outbound-handlers). An outbound handler intercepts HTTP requests from the sandbox and runs inside the Workers runtime, where all of your configured bindings are available.

The sandbox makes a plain HTTP request to a virtual hostname (for example, `http://my.kv/some-key`), and the outbound handler resolves it using the bound resource. No SDK or client library is required inside the sandbox.

## Use bindings in outbound handlers

Define an `outboundByHost` handler for each virtual hostname. The `env` argument gives you access to every binding declared in your Wrangler configuration.

```js
export class MySandbox extends Sandbox {}

MySandbox.outboundByHost = {
	"my.kv": async (request, env, ctx) => {
		const url = new URL(request.url);
		const key = url.pathname.slice(1);
		const value = await env.KV.get(key);
		return new Response(value ?? "", { status: value ? 200 : 404 });
	},
	"my.r2": async (request, env, ctx) => {
		const url = new URL(request.url);
		// Scope access to this sandbox's ID
		const path = `${ctx.containerId}${url.pathname}`;
		const object = await env.R2.get(path);
		return new Response(object?.body ?? null, { status: object ? 200 : 404 });
	},
};
```

```ts
export class MySandbox extends Sandbox {}

MySandbox.outboundByHost = {
	"my.kv": async (request: Request, env: Env, ctx: OutboundHandlerContext) => {
		const url = new URL(request.url);
		const key = url.pathname.slice(1);
		const value = await env.KV.get(key);
		return new Response(value ?? "", { status: value ? 200 : 404 });
	},
	"my.r2": async (request: Request, env: Env, ctx: OutboundHandlerContext) => {
		const url = new URL(request.url);
		// Scope access to this sandbox's ID
		const path = `${ctx.containerId}${url.pathname}`;
		const object = await env.R2.get(path);
		return new Response(object?.body ?? null, { status: object ? 200 : 404 });
	},
};
```

The sandbox calls `http://my.kv/some-key` and the handler resolves it using the KV binding. A call to `http://my.r2/file.png` reads from R2, scoped to the current sandbox instance.

Note

You can use `ctx.containerId` to apply different rules per sandbox instance — for example, to look up per-instance configuration from KV.

## Access Durable Object state

The `ctx` argument exposes `containerId`, which lets you interact with the sandbox's own Durable Object from an outbound handler.

```js
export class MySandbox extends Sandbox {}

MySandbox.outboundByHost = {
	"get-state.do": async (request, env, ctx) => {
		const id = env.MY_SANDBOX.idFromString(ctx.containerId);
		const stub = env.MY_SANDBOX.get(id);
		// Assumes getStateForKey is defined on your DO
		return stub.getStateForKey(request.body);
	},
};
```

```ts
export class MySandbox extends Sandbox {}

MySandbox.outboundByHost = {
	"get-state.do": async (
		request: Request,
		env: Env,
		ctx: { containerId: string },
	) => {
		const id = env.MY_SANDBOX.idFromString(ctx.containerId);
		const stub = env.MY_SANDBOX.get(id);
		// Assumes getStateForKey is defined on your DO
		return stub.getStateForKey(request.body);
	},
};
```

## Related resources

* [Handle outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/) — Block, allow, and intercept all outbound HTTP from a sandbox
* [Sandbox options](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/) — Configure sandbox behavior
* [Environment variables](https://developers.cloudflare.com/sandbox/configuration/environment-variables/) — Configure secrets and environment variables

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/guides/workers-connections/#page","headline":"Connect to Workers bindings · Cloudflare Sandbox SDK docs","description":"Access KV, R2, Durable Objects, and other bindings from a sandbox.","url":"https://developers.cloudflare.com/sandbox/guides/workers-connections/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Sandbox SDK API for executing code, managing files, running processes, and exposing services.
title: API reference
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# API reference

Last updated Sep 1, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Coming soon: Sandbox SDK 1.0

This is the API hub for today's stable `@cloudflare/sandbox` package.

For **`@cloudflare/sandbox@next`**, use the [1.0 preview API reference](https://developers.cloudflare.com/sandbox/1-0-preview/api/).

The Sandbox SDK provides a comprehensive API for executing code, managing files, running processes, and exposing services in isolated sandboxes.

### [Lifecycle](https://developers.cloudflare.com/sandbox/api/lifecycle/)

Create and manage sandbox containers. Get sandbox instances, configure options, and clean up resources.

### [Commands](https://developers.cloudflare.com/sandbox/api/commands/)

Execute commands and stream output. Run scripts, manage background processes, and capture execution results.

### [Files](https://developers.cloudflare.com/sandbox/api/files/)

Read, write, and manage files in the sandbox filesystem. Includes directory operations and file metadata.

### [File watching](https://developers.cloudflare.com/sandbox/api/file-watching/)

Monitor real-time filesystem changes using native inotify. Build development tools, hot-reload systems, and responsive file processing.

### [Code interpreter](https://developers.cloudflare.com/sandbox/api/interpreter/)

Execute Python and JavaScript code with rich outputs including charts, tables, and formatted data.

### [Ports](https://developers.cloudflare.com/sandbox/api/ports/)

Expose services running in the sandbox via preview URLs. Access web servers and APIs from the internet.

### [Tunnels](https://developers.cloudflare.com/sandbox/api/tunnels/)

Expose services on zero-config `*.trycloudflare.com` URLs via `sandbox.tunnels.get(port)`. Best for quick development and `.workers.dev`deployments.

### [Storage](https://developers.cloudflare.com/sandbox/api/storage/)

Mount S3-compatible buckets (R2, S3, GCS) as local filesystems for persistent data storage across sandbox lifecycles.

### [Backups](https://developers.cloudflare.com/sandbox/api/backups/)

Create point-in-time snapshots of directories and restore them from R2.

### [Sessions](https://developers.cloudflare.com/sandbox/api/sessions/)

Create isolated execution contexts within a sandbox. Each session maintains its own shell state, environment variables, and working directory.

### [Terminal](https://developers.cloudflare.com/sandbox/api/terminal/)

Connect browser-based terminal UIs to sandbox shells via WebSocket, with the xterm.js SandboxAddon for automatic reconnection and resize handling.

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/sandbox/api/#page","headline":"API reference · Cloudflare Sandbox SDK docs","description":"Sandbox SDK API for executing code, managing files, running processes, and exposing services.","url":"https://developers.cloudflare.com/sandbox/api/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-09-01","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Create and restore point-in-time snapshots of sandbox directories.
title: Backups
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Backups

Last updated Sep 1, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/backups/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Create point-in-time snapshots of sandbox directories and restore them from R2.

For setup, restore workflows, and generated-cache exclusions, refer to [Backup and restore](https://developers.cloudflare.com/sandbox/guides/backup-restore/). For overlay semantics, refer to [Directory backups](https://developers.cloudflare.com/sandbox/concepts/backup-restore/).

## Methods

### `createBackup()`

Create a snapshot of a directory and upload it to R2.

```ts
await sandbox.createBackup(options: BackupOptions): Promise<DirectoryBackup>
```

**Parameters**:

* `options` \- Backup configuration (see [BackupOptions](#backupoptions)):  
  * `dir` (required) - Absolute path to back up. Must be under `/workspace`, `/home`, `/tmp`, `/var/tmp`, or `/app`.
  * `name` (optional) - Human-readable name. Maximum 256 characters. Control characters are rejected.
  * `ttl` (optional) - Time-to-live in seconds. Default: `259200` (3 days). Must be a positive number.
  * `gitignore` (optional) - When `true`, exclude paths matching `.gitignore` rules if `dir` is inside a git repository. Default: `false`. If the directory is not in a git repository, no git exclusions apply. If `git` is not installed, the SDK logs a warning and continues without git-based exclusions.
  * `excludes` (optional) - Glob patterns to omit from the archive. Passed to `mksquashfs` as wildcard excludes. `**` globstars are normalized automatically. Default: `[]`.
  * `localBucket` (optional) - When `true`, use the `BACKUP_BUCKET` R2 binding instead of presigned URLs. Intended for `wrangler dev`. Default: `false`.
  * `compression` (optional) - Archive compression. Default format: `lz4`. Default threads: `8`. Format must be `gzip`, `lz4`, or `zstd`. `threads` must be a positive integer.
  * `multipart` (optional) - Use parallel multipart upload for large archives. Default: `true`.

**Returns**: `Promise<DirectoryBackup>` containing:

* `id` \- Unique backup identifier (UUID)
* `dir` \- Directory that was backed up
* `localBucket` (optional) - Whether the backup used local R2 binding mode

```js
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const backup = await sandbox.createBackup({ dir: "/workspace" });
await sandbox.restoreBackup(backup);
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const backup = await sandbox.createBackup({ dir: "/workspace" });
await sandbox.restoreBackup(backup);
```

**How it works**:

In production:

1. The container creates a compressed squashfs archive.
2. The container uploads the archive to R2 with a presigned URL.
3. Metadata is stored alongside the archive in R2.
4. The local archive is deleted.

With `localBucket: true`:

1. The container creates a compressed squashfs archive.
2. The archive is uploaded through the `BACKUP_BUCKET` R2 binding.
3. Metadata is stored alongside the archive in R2.
4. The local archive is deleted.

**Throws**:

* `InvalidBackupConfigError` \- If `dir` is not an allowed absolute path, the `BACKUP_BUCKET` binding is missing, or (in production) R2 presigned URL credentials are not configured
* `BackupCreateError` \- If archive creation or the upload to R2 fails

R2 binding required

Configure a `BACKUP_BUCKET` R2 binding in `wrangler.jsonc` before using backup methods. Production also requires `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `CLOUDFLARE_ACCOUNT_ID`, and `BACKUP_BUCKET_NAME`. Refer to [Backup and restore](https://developers.cloudflare.com/sandbox/guides/backup-restore/#prerequisites).

Path permissions

`mksquashfs` must read every file and subdirectory in `dir`. Restrictive permissions fail with `BackupCreateError`. Refer to [Fix path permissions](https://developers.cloudflare.com/sandbox/guides/backup-restore/#fix-path-permissions).

Partial writes

Partially written files may not be captured consistently. Completed writes are included.

---

### `restoreBackup()`

Restore a previously created backup.

```ts
await sandbox.restoreBackup(backup: DirectoryBackup): Promise<RestoreBackupResult>
```

**Parameters**:

* `backup` \- Handle returned by `createBackup()`. Contains `id` and `dir`. Restore writes into `backup.dir`, which may differ from the original backup path. (see [DirectoryBackup](#directorybackup))

**Returns**: `Promise<RestoreBackupResult>` containing:

* `success` \- Whether the restore succeeded
* `dir` \- Directory that was restored
* `id` \- Backup ID that was restored

```js
await sandbox.restoreBackup(backup);
```

```ts
await sandbox.restoreBackup(backup);
```

**How it works**:

In production:

1. Metadata is downloaded from R2 and the TTL is checked, with a 60-second buffer. An expired backup throws.
2. The container downloads the archive from R2 with a presigned URL.
3. The container mounts the archive with FUSE overlayfs.

With `localBucket: true`:

1. Metadata is downloaded from the `BACKUP_BUCKET` binding and the TTL is checked.
2. The archive is downloaded from the R2 binding.
3. The archive is extracted with `unsquashfs`.

**Throws**:

* `InvalidBackupConfigError` \- If `backup.id` is missing or not a UUID, or `backup.dir` is invalid
* `BackupNotFoundError` \- If the metadata or archive is not in R2
* `BackupExpiredError` \- If the TTL has elapsed
* `BackupRestoreError` \- If the container fails to restore

Copy-on-write

In production, the backup is a read-only lower layer and new writes go to a writable upper layer. In local development, the directory is replaced. For overlay constraints, refer to [Directory backups](https://developers.cloudflare.com/sandbox/concepts/backup-restore/).

Ephemeral mount

In production, the FUSE mount is lost when the sandbox sleeps or restarts. Restore again from the handle. Stop processes that write to the target directory before restoring.

## Behavior

* Concurrent backup and restore operations on the same sandbox are serialized.
* `DirectoryBackup` is serializable. Store it in KV, D1, or Durable Object storage.
* Overlapping backups are independent. Restoring a parent directory overwrites subdirectory mounts. Restore the parent first when restoring both.
* `ttl` is enforced at restore time only. Expired objects remain in R2 until you delete them or an [R2 lifecycle rule](https://developers.cloudflare.com/r2/buckets/object-lifecycles/) removes them.
* Backup objects use `backups/{id}/data.sqsh` and `backups/{id}/meta.json`.

## Types

### `BackupOptions`

```ts
interface BackupCompressionOptions {
	format?: "gzip" | "lz4" | "zstd";
	threads?: number;
}

interface BackupOptions {
	dir: string;
	name?: string;
	ttl?: number;
	gitignore?: boolean;
	excludes?: string[];
	localBucket?: boolean;
	compression?: BackupCompressionOptions;
	multipart?: boolean;
}
```

**Fields**:

* `dir` (required) - Absolute path under `/workspace`, `/home`, `/tmp`, `/var/tmp`, or `/app`
* `name` (optional) - Human-readable name. Maximum 256 characters. No control characters.
* `ttl` (optional) - Time-to-live in seconds. Default: `259200` (3 days). Must be a positive number.
* `gitignore` (optional) - When `true`, exclude `.gitignore` matches if `dir` is inside a git repository. Default: `false`.
* `excludes` (optional) - Glob patterns to omit. Example: `['node_modules/.cache', '*.log']`. Refer to [Exclude generated caches](https://developers.cloudflare.com/sandbox/guides/backup-restore/#exclude-generated-caches).
* `localBucket` (optional) - Use the `BACKUP_BUCKET` binding instead of presigned URLs. Default: `false`.
* `compression` (optional) - `format` defaults to `lz4`. `threads` defaults to `8`.
* `multipart` (optional) - Parallel multipart upload. Default: `true`.

### `DirectoryBackup`

```ts
interface DirectoryBackup {
	readonly id: string;
	readonly dir: string;
	readonly localBucket?: boolean;
}
```

**Fields**:

* `id` \- Unique backup identifier (UUID)
* `dir` \- Directory to restore into
* `localBucket` (optional) - Whether the backup used local R2 binding mode

### `RestoreBackupResult`

```ts
interface RestoreBackupResult {
	success: boolean;
	dir: string;
	id: string;
}
```

**Fields**:

* `success` \- Whether the restore succeeded
* `dir` \- Directory that was restored
* `id` \- Backup ID that was restored

## Related resources

* [Backup and restore](https://developers.cloudflare.com/sandbox/guides/backup-restore/) \- Setup and restore workflows
* [Directory backups](https://developers.cloudflare.com/sandbox/concepts/backup-restore/) \- Overlay restore and `EXDEV`
* [Storage API](https://developers.cloudflare.com/sandbox/api/storage/) \- Mount S3-compatible buckets
* [Files API](https://developers.cloudflare.com/sandbox/api/files/) \- Read and write files
* [Wrangler configuration](https://developers.cloudflare.com/sandbox/configuration/wrangler/) \- Configure bindings

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/api/backups/#page","headline":"Backups · Cloudflare Sandbox SDK docs","description":"Create and restore point-in-time snapshots of sandbox directories.","url":"https://developers.cloudflare.com/sandbox/api/backups/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-09-01","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Execute commands and manage background processes in Sandbox SDK containers.
title: Commands
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Commands

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/commands/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Execute commands and manage background processes in the sandbox's isolated container environment.

Coming soon: Sandbox SDK 1.0

This page documents today's stable `@cloudflare/sandbox` package (`exec` with string commands and buffered results, plus `startProcess` / `execStream`).

**Sandbox SDK 1.0** (preview on `@cloudflare/sandbox@next`) makes `exec()` argv-only and returns a process handle. Start new work on the preview when you can. Refer to [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/), the [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/), or [migrate to the preview](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/).

## Methods

### `exec()`

Execute a command and return the complete result.

```ts
const result = await sandbox.exec(command: string, options?: ExecOptions): Promise<ExecuteResponse>
```

**Parameters**:

* `command` \- The command to execute (can include arguments)
* `options` (optional):  
  * `stream` \- Enable streaming callbacks (default: `false`)
  * `onOutput` \- Callback for real-time output: `(stream: 'stdout' | 'stderr', data: string) => void`
  * `timeout` \- Maximum execution time in milliseconds
  * `env` \- Environment variables for this command: `Record<string, string | undefined>`
  * `cwd` \- Working directory for this command
  * `stdin` \- Data to pass to the command's standard input (enables arbitrary input without shell injection risks)

**Returns**: `Promise<ExecuteResponse>` with `success`, `stdout`, `stderr`, `exitCode`

```js
const result = await sandbox.exec("npm run build");

if (result.success) {
	console.log("Build output:", result.stdout);
} else {
	console.error("Build failed:", result.stderr);
}

// With streaming
await sandbox.exec("npm install", {
	stream: true,
	onOutput: (stream, data) => console.log(`[${stream}] ${data}`),
});

// With environment variables (undefined values are skipped)
await sandbox.exec("node app.js", {
	env: {
		NODE_ENV: "production",
		PORT: "3000",
		DEBUG_MODE: undefined, // Skipped, uses container default or unset
	},
});

// Pass input via stdin (no shell injection risks)
const result = await sandbox.exec("cat", {
	stdin: "Hello, world!",
});
console.log(result.stdout); // "Hello, world!"

// Process user input safely
const userInput = "user@example.com\nsecret123";
await sandbox.exec("python process_login.py", {
	stdin: userInput,
});
```

```plaintext
const result = await sandbox.exec('npm run build');

if (result.success) {
  console.log('Build output:', result.stdout);
} else {
  console.error('Build failed:', result.stderr);
}

// With streaming
await sandbox.exec('npm install', {
  stream: true,
  onOutput: (stream, data) => console.log(`[${stream}] ${data}`)
});

// With environment variables (undefined values are skipped)
await sandbox.exec('node app.js', {
  env: {
    NODE_ENV: 'production',
    PORT: '3000',
    DEBUG_MODE: undefined // Skipped, uses container default or unset
  }
});

// Pass input via stdin (no shell injection risks)
const result = await sandbox.exec('cat', {
  stdin: 'Hello, world!'
});
console.log(result.stdout); // "Hello, world!"

// Process user input safely
const userInput = 'user@example.com\nsecret123';
await sandbox.exec('python process_login.py', {
  stdin: userInput
});
```

Timeout behavior

When a command times out, the SDK raises an error on the caller side and closes the connection. The underlying process **continues running** inside the container. To stop a timed-out process, delete the session with [deleteSession()](https://developers.cloudflare.com/sandbox/api/sessions/#deletesession) or destroy the sandbox with [destroy()](https://developers.cloudflare.com/sandbox/api/lifecycle/#destroy).

Timeout precedence: per-command `timeout` on `exec()` \> session-level `commandTimeoutMs` on [createSession()](https://developers.cloudflare.com/sandbox/api/sessions/#createsession) \> global [COMMAND\_TIMEOUT\_MS](https://developers.cloudflare.com/sandbox/configuration/environment-variables/#command%5Ftimeout%5Fms) environment variable. If none are set, commands run without a timeout.

### `execStream()`

Execute a command and return a Server-Sent Events stream for real-time processing.

```ts
const stream = await sandbox.execStream(command: string, options?: ExecOptions): Promise<ReadableStream>
```

**Parameters**:

* `command` \- The command to execute
* `options` \- Same as `exec()` (including `stdin` support)

**Returns**: `Promise<ReadableStream>` emitting `ExecEvent` objects (`start`, `stdout`, `stderr`, `complete`, `error`)

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.execStream("npm run build");

for await (const event of parseSSEStream(stream)) {
	switch (event.type) {
		case "stdout":
			console.log("Output:", event.data);
			break;
		case "complete":
			console.log("Exit code:", event.exitCode);
			break;
		case "error":
			console.error("Failed:", event.error);
			break;
	}
}

// Stream with stdin input
const inputStream = await sandbox.execStream(
	'python -c "import sys; print(sys.stdin.read())"',
	{
		stdin: "Data from Workers!",
	},
);

for await (const event of parseSSEStream(inputStream)) {
	if (event.type === "stdout") {
		console.log("Python received:", event.data);
	}
}
```

```plaintext
import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';

const stream = await sandbox.execStream('npm run build');

for await (const event of parseSSEStream<ExecEvent>(stream)) {
  switch (event.type) {
    case 'stdout':
      console.log('Output:', event.data);
      break;
    case 'complete':
      console.log('Exit code:', event.exitCode);
      break;
    case 'error':
      console.error('Failed:', event.error);
      break;
  }
}

// Stream with stdin input
const inputStream = await sandbox.execStream('python -c "import sys; print(sys.stdin.read())"', {
  stdin: 'Data from Workers!'
});

for await (const event of parseSSEStream<ExecEvent>(inputStream)) {
  if (event.type === 'stdout') {
    console.log('Python received:', event.data);
  }
}
```

### `startProcess()`

Start a long-running background process.

```ts
const process = await sandbox.startProcess(command: string, options?: ProcessOptions): Promise<Process>
```

**Parameters**:

* `command` \- The command to start as a background process
* `options` (optional):  
  * `cwd` \- Working directory
  * `env` \- Environment variables: `Record<string, string | undefined>`
  * `stdin` \- Data to pass to the command's standard input
  * `timeout` \- Maximum execution time in milliseconds
  * `processId` \- Custom process ID
  * `encoding` \- Output encoding (default: `'utf8'`)
  * `autoCleanup` \- Whether to clean up process on sandbox sleep

**Returns**: `Promise<Process>` object with:

* `id` \- Unique process identifier
* `pid` \- System process ID
* `command` \- The command being executed
* `status` \- Current status (`'running'`, `'exited'`, etc.)
* `kill()` \- Stop the process
* `getStatus()` \- Get current status
* `getLogs()` \- Get accumulated logs
* `waitForPort()` \- Wait for process to listen on a port
* `waitForLog()` \- Wait for pattern in process output
* `waitForExit()` \- Wait for process to terminate and return exit code

```js
const server = await sandbox.startProcess("python -m http.server 8000");
console.log("Started with PID:", server.pid);

// With custom environment
const app = await sandbox.startProcess("node app.js", {
	cwd: "/workspace/my-app",
	env: { NODE_ENV: "production", PORT: "3000" },
});

// Start process with stdin input (useful for interactive applications)
const interactive = await sandbox.startProcess("python interactive_app.py", {
	stdin: "initial_config\nstart_mode\n",
});
```

```plaintext
const server = await sandbox.startProcess('python -m http.server 8000');
console.log('Started with PID:', server.pid);

// With custom environment
const app = await sandbox.startProcess('node app.js', {
  cwd: '/workspace/my-app',
  env: { NODE_ENV: 'production', PORT: '3000' }
});

// Start process with stdin input (useful for interactive applications)
const interactive = await sandbox.startProcess('python interactive_app.py', {
  stdin: 'initial_config\nstart_mode\n'
});
```

### `listProcesses()`

List all running processes.

```ts
const processes = await sandbox.listProcesses(): Promise<ProcessInfo[]>
```

```js
const processes = await sandbox.listProcesses();

for (const proc of processes) {
	console.log(`${proc.id}: ${proc.command} (PID ${proc.pid})`);
}
```

```plaintext
const processes = await sandbox.listProcesses();

for (const proc of processes) {
  console.log(`${proc.id}: ${proc.command} (PID ${proc.pid})`);
}
```

### `killProcess()`

Terminate a specific process and all of its child processes.

```ts
await sandbox.killProcess(processId: string, signal?: string): Promise<void>
```

**Parameters**:

* `processId` \- The process ID (from `startProcess()` or `listProcesses()`)
* `signal` \- Signal to send (default: `"SIGTERM"`)

Sends the signal to the entire process group, ensuring that both the main process and any child processes it spawned are terminated. This prevents orphaned processes from continuing to run after the parent is killed.

```js
const server = await sandbox.startProcess("python -m http.server 8000");
await sandbox.killProcess(server.id);

// Example with a process that spawns children
const script = await sandbox.startProcess(
	'bash -c "sleep 10 & sleep 10 & wait"',
);
// killProcess terminates both sleep commands and the bash process
await sandbox.killProcess(script.id);
```

```plaintext
const server = await sandbox.startProcess('python -m http.server 8000');
await sandbox.killProcess(server.id);

// Example with a process that spawns children
const script = await sandbox.startProcess('bash -c "sleep 10 & sleep 10 & wait"');
// killProcess terminates both sleep commands and the bash process
await sandbox.killProcess(script.id);
```

### `killAllProcesses()`

Terminate all running processes.

```ts
await sandbox.killAllProcesses(): Promise<void>
```

```js
await sandbox.killAllProcesses();
```

```plaintext
await sandbox.killAllProcesses();
```

### `streamProcessLogs()`

Stream logs from a running process in real-time.

```ts
const stream = await sandbox.streamProcessLogs(processId: string): Promise<ReadableStream>
```

**Parameters**:

* `processId` \- The process ID

**Returns**: `Promise<ReadableStream>` emitting `LogEvent` objects

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const server = await sandbox.startProcess("node server.js");
const logStream = await sandbox.streamProcessLogs(server.id);

for await (const log of parseSSEStream(logStream)) {
	console.log(`[${log.timestamp}] ${log.data}`);

	if (log.data.includes("Server started")) break;
}
```

```plaintext
import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox';

const server = await sandbox.startProcess('node server.js');
const logStream = await sandbox.streamProcessLogs(server.id);

for await (const log of parseSSEStream<LogEvent>(logStream)) {
  console.log(`[${log.timestamp}] ${log.data}`);

  if (log.data.includes('Server started')) break;
}
```

### `getProcessLogs()`

Get accumulated logs from a process.

```ts
const logs = await sandbox.getProcessLogs(processId: string): Promise<string>
```

**Parameters**:

* `processId` \- The process ID

**Returns**: `Promise<string>` with all accumulated output

```js
const server = await sandbox.startProcess("node server.js");
await new Promise((resolve) => setTimeout(resolve, 5000));

const logs = await sandbox.getProcessLogs(server.id);
console.log("Server logs:", logs);
```

```plaintext
const server = await sandbox.startProcess('node server.js');
await new Promise(resolve => setTimeout(resolve, 5000));

const logs = await sandbox.getProcessLogs(server.id);
console.log('Server logs:', logs);
```

## Standard input (stdin)

All command execution methods support passing data to a command's standard input via the `stdin` option. This enables secure processing of user input without shell injection risks.

### How stdin works

When you provide the `stdin` option:

1. The input data is written to a temporary file inside the container
2. The command receives this data through its standard input stream
3. The temporary file is automatically cleaned up after execution

This approach prevents shell injection attacks that could occur when embedding user data directly in commands.

```js
// Safe: User input goes through stdin, not shell parsing
const userInput = "user@domain.com; rm -rf /";
const result = await sandbox.exec("python validate_email.py", {
	stdin: userInput,
});

// Instead of unsafe: `python validate_email.py "${userInput}"`
// which could execute the embedded `rm -rf /` command
```

```plaintext
// Safe: User input goes through stdin, not shell parsing
const userInput = 'user@domain.com; rm -rf /';
const result = await sandbox.exec('python validate_email.py', {
  stdin: userInput
});

// Instead of unsafe: `python validate_email.py "${userInput}"`
// which could execute the embedded `rm -rf /` command
```

### Common patterns

**Processing form data:**

```js
const formData = JSON.stringify({
	username: "john_doe",
	email: "john@example.com",
});

const result = await sandbox.exec("python process_form.py", {
	stdin: formData,
});
```

```plaintext
const formData = JSON.stringify({
  username: 'john_doe',
  email: 'john@example.com'
});

const result = await sandbox.exec('python process_form.py', {
  stdin: formData
});
```

**Interactive command-line tools:**

```js
// Simulate user responses to prompts
const responses = "yes\nmy-app\n1.0.0\n";
const result = await sandbox.exec("npm init", {
	stdin: responses,
});
```

```plaintext
// Simulate user responses to prompts
const responses = 'yes\nmy-app\n1.0.0\n';
const result = await sandbox.exec('npm init', {
  stdin: responses
});
```

**Data transformation:**

```js
const csvData = "name,age,city\nJohn,30,NYC\nJane,25,LA";
const result = await sandbox.exec("python csv_processor.py", {
	stdin: csvData,
});

console.log("Processed data:", result.stdout);
```

```plaintext
const csvData = 'name,age,city\nJohn,30,NYC\nJane,25,LA';
const result = await sandbox.exec('python csv_processor.py', {
  stdin: csvData
});

console.log('Processed data:', result.stdout);
```

## Process readiness methods

The `Process` object returned by `startProcess()` includes methods to wait for the process to be ready before proceeding.

### `process.waitForPort()`

Wait for a process to listen on a port.

```ts
await process.waitForPort(port: number, options?: WaitForPortOptions): Promise<void>
```

**Parameters**:

* `port` \- The port number to check
* `options` (optional):  
  * `mode` \- Check mode: `'http'` (default) or `'tcp'`
  * `timeout` \- Maximum wait time in milliseconds
  * `interval` \- Check interval in milliseconds (default: `100`)
  * `path` \- HTTP path to check (default: `'/'`, HTTP mode only)
  * `status` \- Expected HTTP status range (default: `{ min: 200, max: 399 }`, HTTP mode only)

**HTTP mode** (default) makes an HTTP GET request and checks the response status:

```js
const server = await sandbox.startProcess("node server.js");

// Wait for server to be ready (HTTP mode)
await server.waitForPort(3000);

// Check specific endpoint and status
await server.waitForPort(8080, {
	path: "/health",
	status: { min: 200, max: 299 },
	timeout: 30000,
});
```

```plaintext
const server = await sandbox.startProcess('node server.js');

// Wait for server to be ready (HTTP mode)
await server.waitForPort(3000);

// Check specific endpoint and status
await server.waitForPort(8080, {
  path: '/health',
  status: { min: 200, max: 299 },
  timeout: 30000
});
```

**TCP mode** checks if the port accepts connections:

```js
const db = await sandbox.startProcess("redis-server");

// Wait for database to accept connections
await db.waitForPort(6379, {
	mode: "tcp",
	timeout: 10000,
});
```

```plaintext
const db = await sandbox.startProcess('redis-server');

// Wait for database to accept connections
await db.waitForPort(6379, {
  mode: 'tcp',
  timeout: 10000
});
```

**Throws**:

* `ProcessReadyTimeoutError` \- If port does not become ready within timeout
* `ProcessExitedBeforeReadyError` \- If process exits before becoming ready

### `process.waitForLog()`

Wait for a pattern to appear in process output.

```ts
const result = await process.waitForLog(pattern: string | RegExp, timeout?: number): Promise<WaitForLogResult>
```

**Parameters**:

* `pattern` \- String or RegExp to match in stdout/stderr
* `timeout` \- Maximum wait time in milliseconds (optional)

**Returns**: `Promise<WaitForLogResult>` with:

* `line` \- The matching line of output
* `matches` \- Array of capture groups (for RegExp patterns)

```js
const server = await sandbox.startProcess("node server.js");

// Wait for string pattern
const result = await server.waitForLog("Server listening");
console.log("Ready:", result.line);

// Wait for RegExp with capture groups
const result = await server.waitForLog(/Server listening on port (\d+)/);
console.log("Port:", result.matches[1]); // Extracted port number

// With timeout
await server.waitForLog("Ready", 30000);
```

```plaintext
const server = await sandbox.startProcess('node server.js');

// Wait for string pattern
const result = await server.waitForLog('Server listening');
console.log('Ready:', result.line);

// Wait for RegExp with capture groups
const result = await server.waitForLog(/Server listening on port (\d+)/);
console.log('Port:', result.matches[1]); // Extracted port number

// With timeout
await server.waitForLog('Ready', 30000);
```

**Throws**:

* `ProcessReadyTimeoutError` \- If pattern is not found within timeout
* `ProcessExitedBeforeReadyError` \- If process exits before pattern appears

### `process.waitForExit()`

Wait for a process to terminate and return the exit code.

```ts
const result = await process.waitForExit(timeout?: number): Promise<WaitForExitResult>
```

**Parameters**:

* `timeout` \- Maximum wait time in milliseconds (optional)

**Returns**: `Promise<WaitForExitResult>` with:

* `exitCode` \- The process exit code

```js
const build = await sandbox.startProcess("npm run build");

// Wait for build to complete
const result = await build.waitForExit();
console.log("Build finished with exit code:", result.exitCode);

// With timeout
const result = await build.waitForExit(60000); // 60 second timeout
```

```plaintext
const build = await sandbox.startProcess('npm run build');

// Wait for build to complete
const result = await build.waitForExit();
console.log('Build finished with exit code:', result.exitCode);

// With timeout
const result = await build.waitForExit(60000); // 60 second timeout
```

**Throws**:

* `ProcessReadyTimeoutError` \- If process does not exit within timeout

## Related resources

* [Background processes guide](https://developers.cloudflare.com/sandbox/guides/background-processes/) \- Managing long-running processes
* [Files API](https://developers.cloudflare.com/sandbox/api/files/) \- File operations

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/api/commands/#page","headline":"Commands · Cloudflare Sandbox SDK docs","description":"Execute commands and manage background processes in Sandbox SDK containers.","url":"https://developers.cloudflare.com/sandbox/api/commands/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Monitor sandbox filesystem changes in real-time using the Sandbox SDK watch API.
title: File watching
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# File watching

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/file-watching/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Coming soon: Sandbox SDK 1.0

This page documents file watching on today's stable `@cloudflare/sandbox` package, including `sessionId` and session helpers.

On the **1.0 preview** (`@next`), `watch` remains available without sessions. Refer to [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/) for the sessionless execution model.

Monitor filesystem changes in real-time using Linux's native inotify system. The `watch()` method returns a Server-Sent Events (SSE) stream of file change events that you consume with `parseSSEStream()`.

## Methods

### `watch()`

Watch a directory for filesystem changes. Returns an SSE stream of events.

```ts
const stream = await sandbox.watch(path: string, options?: WatchOptions): Promise<ReadableStream<Uint8Array>>
```

**Parameters**:

* `path` \- Absolute path or relative to `/workspace` (for example, `/app/src` or `src`)
* `options` (optional):  
  * `recursive` \- Watch subdirectories recursively (default: `true`)
  * `include` \- Glob patterns to include (for example, `['*.ts', '*.js']`). Cannot be used together with `exclude`.
  * `exclude` \- Glob patterns to exclude (default: `['.git', 'node_modules', '.DS_Store']`). Cannot be used together with `include`.
  * `sessionId` \- Session to run the watch in (if omittied, will use the default session unless `enableDefaultSession` is set to false)

**Returns**: `Promise<ReadableStream<Uint8Array>>` — an SSE stream of `FileWatchSSEEvent` objects

```js
import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src", {
	recursive: true,
	include: ["*.ts", "*.js"],
});

const controller = new AbortController();

for await (const event of parseSSEStream(stream, controller.signal)) {
	switch (event.type) {
		case "watching":
			console.log(`Watch established on ${event.path} (id: ${event.watchId})`);
			break;
		case "event":
			console.log(`${event.eventType}: ${event.path}`);
			break;
		case "error":
			console.error(`Watch error: ${event.error}`);
			break;
		case "stopped":
			console.log(`Watch stopped: ${event.reason}`);
			break;
	}
}

// Cancel the watch by aborting — cleans up the watcher server-side
controller.abort();
```

```ts
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src", {
	recursive: true,
	include: ["*.ts", "*.js"],
});

const controller = new AbortController();

for await (const event of parseSSEStream<FileWatchSSEEvent>(
	stream,
	controller.signal,
)) {
	switch (event.type) {
		case "watching":
			console.log(`Watch established on ${event.path} (id: ${event.watchId})`);
			break;
		case "event":
			console.log(`${event.eventType}: ${event.path}`);
			break;
		case "error":
			console.error(`Watch error: ${event.error}`);
			break;
		case "stopped":
			console.log(`Watch stopped: ${event.reason}`);
			break;
	}
}

// Cancel the watch by aborting — cleans up the watcher server-side
controller.abort();
```

Note

The `watch()` method is also available on sessions. When called on a session, the `sessionId` is set automatically:

```ts
const session = await sandbox.createSession();
const stream = await session.watch("/workspace/src", {
	include: ["*.ts"],
});
```

## Types

### `FileWatchSSEEvent`

Union type of all SSE events emitted by the watch stream.

```ts
type FileWatchSSEEvent =
	| { type: "watching"; path: string; watchId: string }
	| {
			type: "event";
			eventType: FileWatchEventType;
			path: string;
			isDirectory: boolean;
			timestamp: string;
	  }
	| { type: "error"; error: string }
	| { type: "stopped"; reason: string };
```

* **`watching`** — Emitted once when the watch is established. Contains the `watchId` and the `path` being watched.
* **`event`** — Emitted for each filesystem change. Contains the `eventType`, the `path` that changed, and whether it `isDirectory`.
* **`error`** — Emitted when the watch encounters an error.
* **`stopped`** — Emitted when the watch is stopped, with a `reason`.

### `FileWatchEventType`

Types of filesystem changes that can be detected.

```ts
type FileWatchEventType =
	| "create"
	| "modify"
	| "delete"
	| "move_from"
	| "move_to"
	| "attrib";
```

* **`create`** — File or directory was created
* **`modify`** — File content changed
* **`delete`** — File or directory was deleted
* **`move_from`** — File or directory was moved away (source of a rename/move)
* **`move_to`** — File or directory was moved here (destination of a rename/move)
* **`attrib`** — File or directory attributes changed (permissions, timestamps)

### `WatchOptions`

Configuration options for watching directories.

```ts
interface WatchOptions {
	/** Watch subdirectories recursively (default: true) */
	recursive?: boolean;
	/** Glob patterns to include. Cannot be used together with `exclude`. */
	include?: string[];
	/** Glob patterns to exclude. Cannot be used together with `include`. Default: ['.git', 'node_modules', '.DS_Store'] */
	exclude?: string[];
	/** Session to run the watch in. If omitted, the sandbox's implicit execution mode is used. */
	sessionId?: string;
}
```

Mutual exclusivity

`include` and `exclude` cannot be used together. Use `include` to allowlist patterns, or `exclude` to blocklist patterns. Requests that specify both are rejected with a validation error.

### `parseSSEStream()`

Converts a `ReadableStream<Uint8Array>` into a typed `AsyncGenerator` of events. Accepts an optional `AbortSignal` to cancel the stream.

```ts
function parseSSEStream<T>(
	stream: ReadableStream<Uint8Array>,
	signal?: AbortSignal,
): AsyncGenerator<T>;
```

**Parameters**:

* `stream` — The SSE stream returned by `watch()`
* `signal` (optional) — An `AbortSignal` to cancel the stream. When aborted, the reader is cancelled which propagates cleanup to the server.

Aborting the signal is the recommended way to stop a watch from outside the consuming loop:

```ts
const controller = new AbortController();

// Cancel after 60 seconds
setTimeout(() => controller.abort(), 60_000);

for await (const event of parseSSEStream<FileWatchSSEEvent>(
	stream,
	controller.signal,
)) {
	// process events
}
```

## Glob pattern support

The `include` and `exclude` options accept a limited set of glob tokens for predictable matching:

| Token | Meaning                                    | Example                |
| ----- | ------------------------------------------ | ---------------------- |
| \*    | Match any characters within a path segment | \*.ts matches index.ts |
| \*\*  | Match across directory boundaries          | \*\*/\*.test.ts        |
| ?     | Match a single character                   | ?.js matches a.js      |

Character classes (`[abc]`), brace expansion (`{a,b}`), and backslash escapes are not supported. Patterns containing these tokens are rejected with a validation error.

## Notes

Deterministic readiness

`watch()` blocks until the filesystem watcher is established on the server. When the promise resolves, the watcher is active and you can immediately perform filesystem actions that depend on the watch being in place.

Container lifecycle

File watchers are automatically stopped when the sandbox container sleeps or is destroyed. You do not need to manually cancel the stream on container shutdown.

Path requirements

All paths must exist when starting a watch. Watching non-existent paths returns an error. Create directories before watching them. All paths must resolve to within `/workspace`.

## Related resources

* [Watch filesystem changes guide](https://developers.cloudflare.com/sandbox/guides/file-watching/) — Patterns, best practices, and real-world examples
* [Manage files guide](https://developers.cloudflare.com/sandbox/guides/manage-files/) — File operations

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/api/file-watching/#page","headline":"File watching · Cloudflare Sandbox SDK docs","description":"Monitor sandbox filesystem changes in real-time using the Sandbox SDK watch API.","url":"https://developers.cloudflare.com/sandbox/api/file-watching/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Read, write, and manage files in the Sandbox SDK filesystem.
title: Files
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Files

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/files/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Read, write, and manage files in the sandbox filesystem. All paths are absolute (e.g., `/workspace/app.js`).

## Methods

### `writeFile()`

Write content to a file.

```ts
await sandbox.writeFile(path: string, content: string, options?: WriteFileOptions): Promise<void>
```

**Parameters**:

* `path` \- Absolute path to the file
* `content` \- Content to write
* `options` (optional):  
  * `encoding` \- File encoding (`"utf-8"` or `"base64"`, default: `"utf-8"`)

```js
await sandbox.writeFile("/workspace/app.js", `console.log('Hello!');`);

// Binary data
await sandbox.writeFile("/tmp/image.png", base64Data, { encoding: "base64" });
```

```plaintext
await sandbox.writeFile('/workspace/app.js', `console.log('Hello!');`);

// Binary data
await sandbox.writeFile('/tmp/image.png', base64Data, { encoding: 'base64' });
```

Base64 validation

When using `encoding: 'base64'`, content must contain only valid base64 characters (A-Z, a-z, 0-9, +, /, =). Invalid base64 content returns a validation error.

#### Large files and binary data

When using the [rpc transport](https://developers.cloudflare.com/sandbox/configuration/transport/) the `writeFile()` method supports passing a `ReadableStream` as the `content` parameter. This allows binary data and files greater than [32 MiB](https://developers.cloudflare.com/workers/runtime-apis/rpc/#limitations) to be written to the sandbox. It replaces the `"base64"` encoding option.

```js
// Requires SANDBOX_TRANSPORT to be "rpc" in wrangler.jsonc
const req = await fetch("https://example.com/archive.tar.gz");
await sandbox.writeFile('/workspace/archive.tar.gz', req.body);
```

### `readFile()`

Read a file from the sandbox. By default returns the content as a string. This is useful for small text files. For larger files and binary data use `encoding: "none"` to get back a `ReadableStream` with the file data.

```ts
const file = await sandbox.readFile(path: string, options?: ReadFileOptions): Promise<ReadFileResult | ReadFileStreamResult>
```

**Parameters**:

* `path` \- Absolute path to the file
* `options` (optional):  
  * `encoding` \- File encoding (`"utf-8"`, `"base64"` or `"none"`, default: auto-detected from MIME type)

**Returns**: `Promise<ReadFileResult | ReadFileStreamResult>`.

Encoding

The `"none"` encoding property was added in 0.10.1 and aims to improve support for streaming binary data. When `encoding: "none"` is provided the `content` field will be a `ReadableStream<Uint8Array>`. It is only supported with the [RPC transport](https://developers.cloudflare.com/sandbox/configuration/transport/).

```js
const file = await sandbox.readFile("/workspace/package.json");
const pkg = JSON.parse(file.content);

// Binary data (since 0.10.1 using `rpc` transport)
const { content, size, mimeType } = await sandbox.readFile(
	"/workspace/archive.tar.gz",
	{
		encoding: "none",
	},
);

// Example 1: Store on R2:
const stream = request.body.pipeThrough(new FixedLengthStream(size));
await env.MY_BUCKET.put("/bucket/archive.tar.gz", stream, {
	httpMetadata: { contentType: mimeType },
});

// Example 2: Stream an HTTP response:
return new Response(content, { headers: { "Content-Type": mimeType } });

// Older versions/transports used the base64 encoding for binary data:
const archive = await sandbox.readFile("/workspace/archive.tar.gz", {
	encoding: "base64",
});
console.log(archive.content); // => "<base64 encoded string>";
```

```plaintext
const file = await sandbox.readFile('/workspace/package.json');
const pkg = JSON.parse(file.content);

// Binary data (since 0.10.1 using `rpc` transport)
const { content, size, mimeType } = await sandbox.readFile("/workspace/archive.tar.gz", {
  encoding: "none"
});

// Example 1: Store on R2:
const stream = request.body.pipeThrough(new FixedLengthStream(size));
await env.MY_BUCKET.put('/bucket/archive.tar.gz', stream, {
  httpMetadata: { contentType: mimeType }
});

// Example 2: Stream an HTTP response:
return new Response(content, { headers: { "Content-Type": mimeType } });

// Older versions/transports used the base64 encoding for binary data:
const archive = await sandbox.readFile("/workspace/archive.tar.gz", {
  encoding: "base64"
});
console.log(archive.content); // => "<base64 encoded string>";
```

Encoding behavior

When `encoding` is specified, it overrides MIME-based auto-detection. Without `encoding`, the SDK detects the appropriate encoding from the file's MIME type.

### `exists()`

Check if a file or directory exists.

```ts
const result = await sandbox.exists(path: string): Promise<FileExistsResult>
```

**Parameters**:

* `path` \- Absolute path to check

**Returns**: `Promise<FileExistsResult>` with `exists` boolean

```js
const result = await sandbox.exists("/workspace/package.json");
if (result.exists) {
	const file = await sandbox.readFile("/workspace/package.json");
	// process file
}

// Check directory
const dirResult = await sandbox.exists("/workspace/src");
if (!dirResult.exists) {
	await sandbox.mkdir("/workspace/src");
}
```

```plaintext
const result = await sandbox.exists('/workspace/package.json');
if (result.exists) {
  const file = await sandbox.readFile('/workspace/package.json');
  // process file
}

// Check directory
const dirResult = await sandbox.exists('/workspace/src');
if (!dirResult.exists) {
  await sandbox.mkdir('/workspace/src');
}
```

Available on sessions

Both `sandbox.exists()` and `session.exists()` are supported.

### `mkdir()`

Create a directory.

```ts
await sandbox.mkdir(path: string, options?: MkdirOptions): Promise<void>
```

**Parameters**:

* `path` \- Absolute path to the directory
* `options` (optional):  
  * `recursive` \- Create parent directories if needed (default: `false`)

```js
await sandbox.mkdir("/workspace/src");

// Nested directories
await sandbox.mkdir("/workspace/src/components/ui", { recursive: true });
```

```plaintext
await sandbox.mkdir('/workspace/src');

// Nested directories
await sandbox.mkdir('/workspace/src/components/ui', { recursive: true });
```

### `deleteFile()`

Delete a file.

```ts
await sandbox.deleteFile(path: string): Promise<void>
```

**Parameters**:

* `path` \- Absolute path to the file

```js
await sandbox.deleteFile("/workspace/temp.txt");
```

```plaintext
await sandbox.deleteFile('/workspace/temp.txt');
```

### `renameFile()`

Rename a file.

```ts
await sandbox.renameFile(oldPath: string, newPath: string): Promise<void>
```

**Parameters**:

* `oldPath` \- Current file path
* `newPath` \- New file path

```js
await sandbox.renameFile("/workspace/draft.txt", "/workspace/final.txt");
```

```plaintext
await sandbox.renameFile('/workspace/draft.txt', '/workspace/final.txt');
```

### `moveFile()`

Move a file to a different directory.

```ts
await sandbox.moveFile(sourcePath: string, destinationPath: string): Promise<void>
```

**Parameters**:

* `sourcePath` \- Current file path
* `destinationPath` \- Destination path

```js
await sandbox.moveFile("/tmp/download.txt", "/workspace/data.txt");
```

```plaintext
await sandbox.moveFile('/tmp/download.txt', '/workspace/data.txt');
```

### `gitCheckout()`

Coming soon: Sandbox SDK 1.0

On `@next`, `gitCheckout` is **removed**. Clone and other git operations with argv `exec` (for example `['git', 'clone', url, dir]`). See the [Processes API](https://developers.cloudflare.com/sandbox/1-0-preview/api/processes/) and [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/).

Clone a git repository.

```ts
await sandbox.gitCheckout(repoUrl: string, options?: GitCheckoutOptions): Promise<void>
```

**Parameters**:

* `repoUrl` \- Git repository URL
* `options` (optional):  
  * `branch` \- Branch to checkout (default: repository default branch)
  * `targetDir` \- Directory to clone into (default: `/workspace/{repoName}`)
  * `depth` \- Clone depth for shallow clones (e.g., `1` for latest commit only)

```js
await sandbox.gitCheckout("https://github.com/user/repo");

// Specific branch
await sandbox.gitCheckout("https://github.com/user/repo", {
	branch: "develop",
	targetDir: "/workspace/my-project",
});

// Shallow clone (faster for large repositories)
await sandbox.gitCheckout("https://github.com/facebook/react", {
	depth: 1,
});
```

```plaintext
await sandbox.gitCheckout('https://github.com/user/repo');

// Specific branch
await sandbox.gitCheckout('https://github.com/user/repo', {
  branch: 'develop',
  targetDir: '/workspace/my-project'
});

// Shallow clone (faster for large repositories)
await sandbox.gitCheckout('https://github.com/facebook/react', {
  depth: 1
});
```

## Related resources

* [Manage files guide](https://developers.cloudflare.com/sandbox/guides/manage-files/) \- Detailed guide with best practices
* [Commands API](https://developers.cloudflare.com/sandbox/api/commands/) \- Execute commands

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/api/files/#page","headline":"Files · Cloudflare Sandbox SDK docs","description":"Read, write, and manage files in the Sandbox SDK filesystem.","url":"https://developers.cloudflare.com/sandbox/api/files/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Execute Python, JavaScript, and TypeScript code with rich output formats in Sandbox SDK.
title: Code interpreter
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Code interpreter

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/interpreter/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Execute Python, JavaScript, and TypeScript code with support for data visualizations, tables, and rich output formats. Contexts maintain state (variables, imports, functions) across executions.

Coming soon: Sandbox SDK 1.0

This page documents interpreter methods on today's stable `@cloudflare/sandbox` package.

In the **1.0 preview** (`@next`), attach `withInterpreter` from `@cloudflare/sandbox/interpreter`. Refer to [Code interpreter](https://developers.cloudflare.com/sandbox/1-0-preview/interpreter/) and the [Interpreter API](https://developers.cloudflare.com/sandbox/1-0-preview/api/interpreter/).

## Methods

### `createCodeContext()`

Create a persistent execution context for running code.

```ts
const context = await sandbox.createCodeContext(options?: CreateContextOptions): Promise<CodeContext>
```

**Parameters**:

* `options` (optional):  
  * `language` \- `"python" | "javascript" | "typescript"` (default: `"python"`)
  * `cwd` \- Working directory (default: `"/workspace"`)
  * `envVars` \- Environment variables
  * `timeout` \- Request timeout in milliseconds (default: 30000)

**Returns**: `Promise<CodeContext>` with `id`, `language`, `cwd`, `createdAt`, `lastUsed`

```js
const ctx = await sandbox.createCodeContext({
	language: "python",
	envVars: { API_KEY: env.API_KEY },
});
```

```plaintext
const ctx = await sandbox.createCodeContext({
  language: 'python',
  envVars: { API_KEY: env.API_KEY }
});
```

### `runCode()`

Execute code in a context and return the complete result.

```ts
const result = await sandbox.runCode(code: string, options?: RunCodeOptions): Promise<ExecutionResult>
```

**Parameters**:

* `code` \- The code to execute (required)
* `options` (optional):  
  * `context` \- Context to run in (recommended - see below)
  * `language` \- `"python" | "javascript" | "typescript"` (default: `"python"`)
  * `timeout` \- Execution timeout in milliseconds (default: 60000)
  * `onStdout`, `onStderr`, `onResult`, `onError` \- Streaming callbacks

**Returns**: `Promise<ExecutionResult>` with:

* `code` \- The executed code
* `logs` \- `stdout` and `stderr` arrays
* `results` \- Array of rich outputs (see [Rich Output Formats](#rich-output-formats))
* `error` \- Execution error if any
* `executionCount` \- Execution counter

**Recommended usage - create explicit context**:

```js
const ctx = await sandbox.createCodeContext({ language: "python" });

await sandbox.runCode("import math; radius = 5", { context: ctx });
const result = await sandbox.runCode("math.pi * radius ** 2", { context: ctx });

console.log(result.results[0].text); // "78.53981633974483"
```

```plaintext
const ctx = await sandbox.createCodeContext({ language: 'python' });

await sandbox.runCode('import math; radius = 5', { context: ctx });
const result = await sandbox.runCode('math.pi * radius ** 2', { context: ctx });

console.log(result.results[0].text); // "78.53981633974483"
```

Default context behavior

If no `context` is provided, a default context is automatically created/reused for the specified `language`. While convenient for quick tests, **explicitly creating contexts is recommended** for production use to maintain predictable state.

```js
const result = await sandbox.runCode(
	`
data = [1, 2, 3, 4, 5]
print(f"Sum: {sum(data)}")
sum(data)
`,
	{ language: "python" },
);

console.log(result.logs.stdout); // ["Sum: 15"]
console.log(result.results[0].text); // "15"
```

```plaintext
const result = await sandbox.runCode(`
data = [1, 2, 3, 4, 5]
print(f"Sum: {sum(data)}")
sum(data)
`, { language: 'python' });

console.log(result.logs.stdout); // ["Sum: 15"]
console.log(result.results[0].text); // "15"
```

**Error handling**:

```js
const result = await sandbox.runCode("x = 1 / 0", { language: "python" });

if (result.error) {
	console.error(result.error.name); // "ZeroDivisionError"
	console.error(result.error.value); // "division by zero"
	console.error(result.error.traceback); // Stack trace array
}
```

```plaintext
const result = await sandbox.runCode('x = 1 / 0', { language: 'python' });

if (result.error) {
  console.error(result.error.name);      // "ZeroDivisionError"
  console.error(result.error.value);     // "division by zero"
  console.error(result.error.traceback); // Stack trace array
}
```

**JavaScript and TypeScript features**:

JavaScript and TypeScript code execution supports top-level `await` and persistent variables across executions within the same context.

```js
const ctx = await sandbox.createCodeContext({ language: "javascript" });

// Execution 1: Fetch data with top-level await
await sandbox.runCode(
	`
const response = await fetch('https://api.example.com/data');
const data = await response.json();
`,
	{ context: ctx },
);

// Execution 2: Use the data from previous execution
const result = await sandbox.runCode("console.log(data)", { context: ctx });
console.log(result.logs.stdout); // Data persists across executions
```

```plaintext
const ctx = await sandbox.createCodeContext({ language: 'javascript' });

// Execution 1: Fetch data with top-level await
await sandbox.runCode(`
const response = await fetch('https://api.example.com/data');
const data = await response.json();
`, { context: ctx });

// Execution 2: Use the data from previous execution
const result = await sandbox.runCode('console.log(data)', { context: ctx });
console.log(result.logs.stdout); // Data persists across executions
```

Variables declared with `const`, `let`, or `var` persist across executions, enabling multi-step workflows:

```js
const ctx = await sandbox.createCodeContext({ language: "javascript" });

await sandbox.runCode("const x = 10", { context: ctx });
await sandbox.runCode("let y = 20", { context: ctx });
const result = await sandbox.runCode("x + y", { context: ctx });

console.log(result.results[0].text); // "30"
```

```plaintext
const ctx = await sandbox.createCodeContext({ language: 'javascript' });

await sandbox.runCode('const x = 10', { context: ctx });
await sandbox.runCode('let y = 20', { context: ctx });
const result = await sandbox.runCode('x + y', { context: ctx });

console.log(result.results[0].text); // "30"
```

### `listCodeContexts()`

List all active code execution contexts.

```ts
const contexts = await sandbox.listCodeContexts(): Promise<CodeContext[]>
```

```js
const contexts = await sandbox.listCodeContexts();
console.log(`Found ${contexts.length} contexts`);
```

```plaintext
const contexts = await sandbox.listCodeContexts();
console.log(`Found ${contexts.length} contexts`);
```

### `deleteCodeContext()`

Delete a code execution context and free its resources.

```ts
await sandbox.deleteCodeContext(contextId: string): Promise<void>
```

```js
const ctx = await sandbox.createCodeContext({ language: "python" });
await sandbox.runCode('print("Hello")', { context: ctx });
await sandbox.deleteCodeContext(ctx.id);
```

```plaintext
const ctx = await sandbox.createCodeContext({ language: 'python' });
await sandbox.runCode('print("Hello")', { context: ctx });
await sandbox.deleteCodeContext(ctx.id);
```

## Rich Output Formats

Results include: `text`, `html`, `png`, `jpeg`, `svg`, `latex`, `markdown`, `json`, `chart`, `data`

**Charts (matplotlib)**:

```js
const result = await sandbox.runCode(
	`
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
plt.plot(x, np.sin(x))
plt.show()
`,
	{ language: "python" },
);

if (result.results[0]?.png) {
	const imageBuffer = Buffer.from(result.results[0].png, "base64");
	return new Response(imageBuffer, {
		headers: { "Content-Type": "image/png" },
	});
}
```

```plaintext
const result = await sandbox.runCode(`
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
plt.plot(x, np.sin(x))
plt.show()
`, { language: 'python' });

if (result.results[0]?.png) {
  const imageBuffer = Buffer.from(result.results[0].png, 'base64');
  return new Response(imageBuffer, {
    headers: { 'Content-Type': 'image/png' }
  });
}
```

**Tables (pandas)**:

```js
const result = await sandbox.runCode(
	`
import pandas as pd
df = pd.DataFrame({'Name': ['Alice', 'Bob'], 'Age': [25, 30]})
df
`,
	{ language: "python" },
);

if (result.results[0]?.html) {
	return new Response(result.results[0].html, {
		headers: { "Content-Type": "text/html" },
	});
}
```

```plaintext
const result = await sandbox.runCode(`
import pandas as pd
df = pd.DataFrame({'Name': ['Alice', 'Bob'], 'Age': [25, 30]})
df
`, { language: 'python' });

if (result.results[0]?.html) {
  return new Response(result.results[0].html, {
    headers: { 'Content-Type': 'text/html' }
  });
}
```

## Related resources

* [Build an AI Code Executor](https://developers.cloudflare.com/sandbox/tutorials/ai-code-executor/) \- Complete tutorial
* [Commands API](https://developers.cloudflare.com/sandbox/api/commands/) \- Lower-level command execution
* [Files API](https://developers.cloudflare.com/sandbox/api/files/) \- File operations

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/api/interpreter/#page","headline":"Code interpreter · Cloudflare Sandbox SDK docs","description":"Execute Python, JavaScript, and TypeScript code with rich output formats in Sandbox SDK.","url":"https://developers.cloudflare.com/sandbox/api/interpreter/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Create, configure, and manage Sandbox SDK container instances and their resources.
title: Lifecycle
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Lifecycle

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/lifecycle/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Create and manage sandbox containers. Get sandbox instances, configure options, and clean up resources.

Coming soon: Sandbox SDK 1.0

This page documents lifecycle helpers on today's stable `@cloudflare/sandbox` package.

In the **1.0 preview** (`@next`), remove transport options on `getSandbox()` and do not rely on `enableDefaultSession`. Keep `sleepAfter`, `keepAlive`, `containerTimeouts`, `normalizeId`, and `destroy` when you need them. Process and terminal handles are container-local after stop or replace. Refer to [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/) and [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/).

## Methods

### `getSandbox()`

Get or create a sandbox instance by ID.

```ts
const sandbox = getSandbox(
  binding: DurableObjectNamespace<Sandbox>,
  sandboxId: string,
  options?: SandboxOptions
): Sandbox
```

**Parameters**:

* `binding` \- The Durable Object namespace binding from your Worker environment
* `sandboxId` \- Unique identifier for this sandbox. The same ID always returns the same sandbox instance. In user-facing apps, scope IDs to a single user.
* `options` (optional) - See [SandboxOptions](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/) for all available options:  
  * `enableDefaultSession` \- Use the default session for operations without an explicit `sessionId`. Set to `false` to evaluate each call in isolation (default: `true`)
  * `sleepAfter` \- Duration of inactivity before automatic sleep (default: `"10m"`)
  * `keepAlive` \- Prevent automatic sleep entirely. Persists across hibernation (default: `false`)
  * `containerTimeouts` \- Configure container startup timeouts
  * `normalizeId` \- Lowercase sandbox IDs for preview URL compatibility (default: `false`)

**Returns**: `Sandbox` instance

Note

The container starts lazily on first operation. Calling `getSandbox()` returns immediately—the container only spins up when you execute a command, write a file, or perform other operations. See [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/concepts/sandboxes/) for details.

Implicit execution mode

By default, sandbox methods that do not specify a `sessionId` run in the sandbox's default session and preserve shell state between calls. It is recommended to set `enableDefaultSession` to `false` to ensure operations run in isolation. The `createSession()` API exists when sessions are required. Default sessions will be removed in a future version of the Sandbox SDK.

```js
import { getSandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		const sandbox = getSandbox(env.Sandbox, "user-123");
		const result = await sandbox.exec("python script.py");
		return Response.json(result);
	},
};
```

```plaintext
import { getSandbox } from '@cloudflare/sandbox';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const sandbox = getSandbox(env.Sandbox, 'user-123');
    const result = await sandbox.exec('python script.py');
    return Response.json(result);
  }
};
```

Caution

When using `keepAlive: true`, you **must** call `destroy()` when finished to prevent containers running indefinitely.

---

### `setKeepAlive()`

Enable or disable keepAlive mode dynamically after sandbox creation.

```ts
await sandbox.setKeepAlive(keepAlive: boolean): Promise<void>
```

**Parameters**:

* `keepAlive` \- `true` to prevent automatic sleep, `false` to allow normal sleep behavior

When enabled, the sandbox automatically sends heartbeat pings every 30 seconds to prevent container eviction. When disabled, the sandbox returns to normal sleep behavior based on the `sleepAfter` configuration.

```js
const sandbox = getSandbox(env.Sandbox, "user-123");

// Enable keepAlive for a long-running process
await sandbox.setKeepAlive(true);
await sandbox.startProcess("python long_running_analysis.py");

// Later, disable keepAlive when done
await sandbox.setKeepAlive(false);
```

```plaintext
const sandbox = getSandbox(env.Sandbox, 'user-123');

// Enable keepAlive for a long-running process
await sandbox.setKeepAlive(true);
await sandbox.startProcess('python long_running_analysis.py');

// Later, disable keepAlive when done
await sandbox.setKeepAlive(false);
```

Heartbeat mechanism

When keepAlive is enabled, the sandbox automatically sends lightweight ping requests to the container every 30 seconds to prevent eviction. This happens transparently without affecting your application code.

Resource management

Containers with `keepAlive: true` will not automatically timeout. Always disable keepAlive or call `destroy()` when done to prevent containers running indefinitely.

---

### `destroy()`

Destroy the sandbox container and free up resources.

```ts
await sandbox.destroy(): Promise<void>
```

Immediately terminates the container and permanently deletes all state:

* All files in `/workspace`, `/tmp`, and `/home`
* All running processes
* All sessions (including the default session)
* Network connections and exposed ports

```js
async function executeCode(code) {
	const sandbox = getSandbox(env.Sandbox, `temp-${Date.now()}`);

	try {
		await sandbox.writeFile("/tmp/code.py", code);
		const result = await sandbox.exec("python /tmp/code.py");
		return result.stdout;
	} finally {
		await sandbox.destroy();
	}
}
```

```plaintext
async function executeCode(code: string): Promise<string> {
  const sandbox = getSandbox(env.Sandbox, `temp-${Date.now()}`);

  try {
    await sandbox.writeFile('/tmp/code.py', code);
    const result = await sandbox.exec('python /tmp/code.py');
    return result.stdout;
  } finally {
    await sandbox.destroy();
  }
}
```

Note

Containers automatically sleep after 10 minutes of inactivity but still count toward account limits. Use `destroy()` to immediately free up resources.

---

## Related resources

* [Sandbox lifecycle concept](https://developers.cloudflare.com/sandbox/concepts/sandboxes/) \- Understanding container lifecycle and state
* [Sandbox options configuration](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/) \- Configure `keepAlive` and other options
* [Sessions API](https://developers.cloudflare.com/sandbox/api/sessions/) \- Create execution contexts within a sandbox

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/api/lifecycle/#page","headline":"Lifecycle · Cloudflare Sandbox SDK docs","description":"Create, configure, and manage Sandbox SDK container instances and their resources.","url":"https://developers.cloudflare.com/sandbox/api/lifecycle/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Expose sandbox services via public preview URLs using the Sandbox SDK ports API.
title: Ports
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Ports

Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/ports/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Coming soon: Sandbox SDK 1.0

This page documents ports and preview URLs on today's stable `@cloudflare/sandbox` package.

Examples that use `startProcess` are stable-only. On **`@next`**, start services with `exec(argv)` then `waitForPort` / expose or tunnels — [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/) and [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/).

Production requires custom domain

Preview URLs require a custom domain with wildcard DNS routing in production. See [Configure preview URLs on a custom domain](https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/).

Prefer \`sandbox.tunnels\` for public URLs

For most public-URL use cases — development, `.workers.dev` deployments, and production traffic — [sandbox.tunnels](https://developers.cloudflare.com/sandbox/api/tunnels/) is the recommended option. Use named tunnels for stable hostnames on a zone you control, or quick tunnels for zero-config `*.trycloudflare.com` URLs. `exposePort()` is appropriate when you want the Worker itself to front the request (for example, to inject authentication, rewrite responses, or call sandbox-only RPC methods on the same hostname).

Expose services running in your sandbox via public preview URLs. See [Preview URLs concept](https://developers.cloudflare.com/sandbox/concepts/preview-urls/) for details.

## Module functions

### `proxyToSandbox()`

Route incoming HTTP and WebSocket requests to the correct sandbox container. Call this at the top of your Worker's `fetch` handler, before any application logic, so that it intercepts and forwards preview URL requests automatically.

```ts
proxyToSandbox(request: Request, env: Env): Promise<Response | null>
```

**Parameters**:

* `request` \- The incoming `Request` object from the `fetch` handler.
* `env` \- The `Env` object containing your Sandbox binding.

**Returns**: `Promise<Response | null>` — a `Response` if the request matched a preview URL and was routed to the sandbox, or `null` if the request did not match and should be handled by your application logic.

The function inspects the request hostname to determine whether it matches the subdomain pattern of an exposed port (for example, `8080-sandbox-id-token.yourdomain.com`). If it matches, `proxyToSandbox()` proxies the request to the correct Durable Object, and the sandbox service handles it. Both HTTP and WebSocket upgrade requests are supported.

```js
import { proxyToSandbox, getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		// Always call proxyToSandbox first to handle preview URL requests
		const proxyResponse = await proxyToSandbox(request, env);
		if (proxyResponse) return proxyResponse;

		// Your application routes
		const sandbox = getSandbox(env.Sandbox, "my-sandbox");
		// ...
		return new Response("Not found", { status: 404 });
	},
};
```

```ts
import { proxyToSandbox, getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Always call proxyToSandbox first to handle preview URL requests
    const proxyResponse = await proxyToSandbox(request, env);
    if (proxyResponse) return proxyResponse;

    // Your application routes
    const sandbox = getSandbox(env.Sandbox, 'my-sandbox');
    // ...
    return new Response('Not found', { status: 404 });
  }
};
```

Note

`proxyToSandbox` is a module-level function imported directly from `@cloudflare/sandbox` — it is not a method on a `Sandbox` instance. It requires the Sandbox Durable Object binding (`env.Sandbox`) to look up and route requests to the correct container.

## Methods

### `exposePort()`

Expose a port and get a preview URL for accessing services running in the sandbox.

```ts
const response = await sandbox.exposePort(port: number, options: ExposePortOptions): Promise<ExposePortResponse>
```

**Parameters**:

* `port` \- Port number to expose (1024-65535)
* `options`:  
  * `hostname` \- Your Worker's domain name (e.g., `'example.com'`). Required to construct preview URLs with wildcard subdomains like `https://8080-sandbox-abc123token.example.com`. Cannot be a `.workers.dev` domain as it doesn't support wildcard DNS patterns.
  * `name` \- Friendly name for the port (optional)
  * `token` \- Custom token for the preview URL (optional). Must be 1-16 characters containing only lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (\_). If not provided, a random 16-character token is generated automatically.

**Returns**: `Promise<ExposePortResponse>` with `port`, `url` (preview URL), `name`

```js
// Extract hostname from request
const { hostname } = new URL(request.url);

// Basic usage with auto-generated token
await sandbox.startProcess("python -m http.server 8000");
const exposed = await sandbox.exposePort(8000, { hostname });

console.log("Available at:", exposed.url);
// https://8000-sandbox-id-abc123random.yourdomain.com

// With custom token for stable URLs across restarts
const stable = await sandbox.exposePort(8080, {
	hostname,
	token: "my_service_v1", // 1-16 chars: a-z, 0-9, _
});
console.log("Stable URL:", stable.url);
// https://8080-sandbox-id-my_service_v1.yourdomain.com

// With custom token for stable URLs across deployments
await sandbox.startProcess("node api.js");
const api = await sandbox.exposePort(3000, {
	hostname,
	name: "api",
	token: "prod-api-v1", // URL stays same across restarts
});

console.log("Stable API URL:", api.url);
// https://3000-sandbox-id-prod-api-v1.yourdomain.com

// Multiple services with custom tokens
await sandbox.startProcess("npm run dev");
const frontend = await sandbox.exposePort(5173, {
	hostname,
	name: "frontend",
	token: "dev-ui",
});
```

```ts
// Extract hostname from request
const { hostname } = new URL(request.url);

// Basic usage with auto-generated token
await sandbox.startProcess('python -m http.server 8000');
const exposed = await sandbox.exposePort(8000, { hostname });

console.log('Available at:', exposed.url);
// https://8000-sandbox-id-abc123random.yourdomain.com

// With custom token for stable URLs across restarts
const stable = await sandbox.exposePort(8080, {
  hostname,
  token: 'my_service_v1' // 1-16 chars: a-z, 0-9, _
});
console.log('Stable URL:', stable.url);
// https://8080-sandbox-id-my_service_v1.yourdomain.com

// With custom token for stable URLs across deployments
await sandbox.startProcess('node api.js');
const api = await sandbox.exposePort(3000, {
  hostname,
  name: 'api',
  token: 'prod-api-v1'  // URL stays same across restarts
});

console.log('Stable API URL:', api.url);
// https://3000-sandbox-id-prod-api-v1.yourdomain.com

// Multiple services with custom tokens
await sandbox.startProcess('npm run dev');
const frontend = await sandbox.exposePort(5173, {
  hostname,
  name: 'frontend',
  token: 'dev-ui'
});
```

Local development

When using `wrangler dev`, you must add `EXPOSE` directives to your Dockerfile for each port. See [Expose Services guide](https://developers.cloudflare.com/sandbox/guides/expose-services/#local-development) for details.

## Custom Tokens for Stable URLs

Custom tokens enable consistent preview URLs across container restarts and deployments. This is useful for:

* **Production environments** \- Share stable URLs with users or teams
* **Development workflows** \- Maintain bookmarks and integrations
* **CI/CD pipelines** \- Reference consistent URLs in tests or deployment scripts

**Token Requirements:**

* 1-16 characters in length
* Only lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (\_)
* Must be unique per sandbox (cannot reuse tokens across different ports)

```js
// Production API with stable URL
const { url } = await sandbox.exposePort(8080, {
	hostname: "api.example.com",
	token: "v1-stable", // Always the same URL
});

// Error: Token collision prevention
await sandbox.exposePort(8081, { hostname, token: "v1-stable" });
// Throws: Token 'v1-stable' is already in use by port 8080

// Success: Re-exposing same port with same token (idempotent)
await sandbox.exposePort(8080, { hostname, token: "v1-stable" });
// Works - same port, same token
```

```ts
// Production API with stable URL
const { url } = await sandbox.exposePort(8080, {
  hostname: 'api.example.com',
  token: 'v1-stable'  // Always the same URL
});

// Error: Token collision prevention
await sandbox.exposePort(8081, { hostname, token: 'v1-stable' });
// Throws: Token 'v1-stable' is already in use by port 8080

// Success: Re-exposing same port with same token (idempotent)
await sandbox.exposePort(8080, { hostname, token: 'v1-stable' });
// Works - same port, same token
```

### `validatePortToken()`

Validate if a token is authorized to access a specific exposed port. Useful for custom authentication or routing logic.

```ts
const isValid = await sandbox.validatePortToken(port: number, token: string): Promise<boolean>
```

**Parameters**:

* `port` \- Port number to check
* `token` \- Token to validate

**Returns**: `Promise<boolean>` \- `true` if token is valid for the port, `false` otherwise

```js
// Custom validation in your Worker
export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		// Extract token from custom header or query param
		const customToken = request.headers.get("x-access-token");

		if (customToken) {
			const sandbox = getSandbox(env.Sandbox, "my-sandbox");
			const isValid = await sandbox.validatePortToken(8080, customToken);

			if (!isValid) {
				return new Response("Invalid token", { status: 403 });
			}
		}

		// Handle preview URL routing
		const proxyResponse = await proxyToSandbox(request, env);
		if (proxyResponse) return proxyResponse;

		// Your application routes
		return new Response("Not found", { status: 404 });
	},
};
```

```ts
// Custom validation in your Worker
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    
    // Extract token from custom header or query param
    const customToken = request.headers.get('x-access-token');
    
    if (customToken) {
      const sandbox = getSandbox(env.Sandbox, 'my-sandbox');
      const isValid = await sandbox.validatePortToken(8080, customToken);
      
      if (!isValid) {
        return new Response('Invalid token', { status: 403 });
      }
    }
    
    // Handle preview URL routing
    const proxyResponse = await proxyToSandbox(request, env);
    if (proxyResponse) return proxyResponse;
    
    // Your application routes
    return new Response('Not found', { status: 404 });
  }
};
```

### `unexposePort()`

Remove an exposed port and close its preview URL.

```ts
await sandbox.unexposePort(port: number): Promise<void>
```

**Parameters**:

* `port` \- Port number to unexpose

```js
await sandbox.unexposePort(8000);
```

```ts
await sandbox.unexposePort(8000);
```

### `getExposedPorts()`

Get information about all currently exposed ports.

```ts
const response = await sandbox.getExposedPorts(): Promise<GetExposedPortsResponse>
```

**Returns**: `Promise<GetExposedPortsResponse>` with `ports` array (containing `port`, `url`, `name`)

```js
const { ports } = await sandbox.getExposedPorts();

for (const port of ports) {
	console.log(`${port.name || port.port}: ${port.url}`);
}
```

```plaintext
const { ports } = await sandbox.getExposedPorts();

for (const port of ports) {
  console.log(`${port.name || port.port}: ${port.url}`);
}
```

### `wsConnect()`

Connect to WebSocket servers running in the sandbox. Use this when your Worker needs to establish WebSocket connections with services in the sandbox.

**Common use cases:**

* Route incoming WebSocket upgrade requests with custom authentication or authorization
* Connect from your Worker to get real-time data from sandbox services

For exposing WebSocket services via public preview URLs, use `exposePort()` with `proxyToSandbox()` instead. See [WebSocket Connections guide](https://developers.cloudflare.com/sandbox/guides/websocket-connections/) for examples.

```ts
const response = await sandbox.wsConnect(request: Request, port: number): Promise<Response>
```

**Parameters**:

* `request` \- Incoming WebSocket upgrade request
* `port` \- Port number (1024-65535, excluding 3000)

**Returns**: `Promise<Response>` \- WebSocket response establishing the connection

```js
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") {
			const sandbox = getSandbox(env.Sandbox, "my-sandbox");
			return await sandbox.wsConnect(request, 8080);
		}

		return new Response("WebSocket endpoint", { status: 200 });
	},
};
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') {
      const sandbox = getSandbox(env.Sandbox, 'my-sandbox');
      return await sandbox.wsConnect(request, 8080);
    }

    return new Response('WebSocket endpoint', { status: 200 });
  }
};
```

## Related resources

* [Preview URLs concept](https://developers.cloudflare.com/sandbox/concepts/preview-urls/) \- How preview URLs work
* [Expose Services guide](https://developers.cloudflare.com/sandbox/guides/expose-services/) \- Full workflow for starting services, exposing ports, and routing requests
* [WebSocket Connections guide](https://developers.cloudflare.com/sandbox/guides/websocket-connections/) \- WebSocket routing via preview URLs
* [Commands API](https://developers.cloudflare.com/sandbox/api/commands/) \- Start background processes
* [Tunnels API](https://developers.cloudflare.com/sandbox/api/tunnels/) \- Zero-config `*.trycloudflare.com` URLs for quick development

```plaintext

```

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/api/ports/#page","headline":"Ports · Cloudflare Sandbox SDK docs","description":"Expose sandbox services via public preview URLs using the Sandbox SDK ports API.","url":"https://developers.cloudflare.com/sandbox/api/ports/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Create shell sessions with independent working directories and environment variables within a sandbox.
title: Sessions
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Sessions

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/sessions/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Create shell sessions within a sandbox. Each session maintains its own shell state, environment variables, and working directory, while sharing the sandbox filesystem and process space. For more information, refer to [Session management](https://developers.cloudflare.com/sandbox/concepts/sessions/).

Coming soon: Sandbox SDK 1.0

This page documents today's stable `@cloudflare/sandbox` package.

**Sandbox SDK 1.0** (preview on `@next`) removes core session execution APIs (`createSession`, `ExecutionSession`, default sessions). Pass `cwd` and `env` on each `exec`, or use one explicit shell argv script. Refer to [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/) or [migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/#drop-session-apis).

Note

By default, for backwards compatibility, every sandbox has a default session that maintains shell state. It is recommended to set `enableDefaultSession` to `false` on `getSandbox()` so operations without an explicit `sessionId` run in isolation. Create additional sessions for separate workflows inside the same user workspace, such as development and runtime processes using the `createSession()` method. Use separate sandboxes for separate users. For sandbox-level operations like creating containers or destroying the entire sandbox, refer to the [Lifecycle API](https://developers.cloudflare.com/sandbox/api/lifecycle/).

## Methods

### `createSession()`

Create a new shell session.

```ts
const session = await sandbox.createSession(options?: SessionOptions): Promise<ExecutionSession>
```

**Parameters**:

* `options` (optional):  
  * `id` \- Custom session ID (auto-generated if not provided)
  * `env` \- Environment variables for this session: `Record<string, string | undefined>`
  * `cwd` \- Working directory (default: `"/workspace"`)
  * `commandTimeoutMs` \- Maximum time in milliseconds that any command in this session can run before timing out. Individual commands can override this with the `timeout` option on `exec()`.

**Returns**: `Promise<ExecutionSession>` with all sandbox methods bound to this session

```js
// Separate workflow environments
const prodSession = await sandbox.createSession({
	id: "prod",
	env: { NODE_ENV: "production", API_URL: "https://api.example.com" },
	cwd: "/workspace/prod",
});

const testSession = await sandbox.createSession({
	id: "test",
	env: {
		NODE_ENV: "test",
		API_URL: "http://localhost:3000",
		DEBUG_MODE: undefined, // Skipped, not set in this session
	},
	cwd: "/workspace/test",
});

// Run in parallel
const [prodResult, testResult] = await Promise.all([
	prodSession.exec("npm run build"),
	testSession.exec("npm run build"),
]);

// Session with a default command timeout
const session = await sandbox.createSession({
	commandTimeoutMs: 5000, // 5s timeout for all commands
});

await session.exec("sleep 10"); // Times out after 5s

// Per-command timeout overrides session-level timeout
await session.exec("sleep 10", { timeout: 3000 }); // Times out after 3s
```

```ts
// Separate workflow environments
const prodSession = await sandbox.createSession({
  id: 'prod',
  env: { NODE_ENV: 'production', API_URL: 'https://api.example.com' },
  cwd: '/workspace/prod'
});

const testSession = await sandbox.createSession({
  id: 'test',
  env: {
    NODE_ENV: 'test',
    API_URL: 'http://localhost:3000',
    DEBUG_MODE: undefined // Skipped, not set in this session
  },
  cwd: '/workspace/test'
});

// Run in parallel
const [prodResult, testResult] = await Promise.all([
  prodSession.exec('npm run build'),
  testSession.exec('npm run build')
]);

// Session with a default command timeout
const session = await sandbox.createSession({
  commandTimeoutMs: 5000 // 5s timeout for all commands
});

await session.exec('sleep 10'); // Times out after 5s

// Per-command timeout overrides session-level timeout
await session.exec('sleep 10', { timeout: 3000 }); // Times out after 3s
```

### `getSession()`

Retrieve an existing session by ID.

```ts
const session = await sandbox.getSession(sessionId: string): Promise<ExecutionSession>
```

**Parameters**:

* `sessionId` \- ID of an existing session

**Returns**: `Promise<ExecutionSession>` bound to the specified session

```js
// First request - create a task-specific session
const session = await sandbox.createSession({ id: "build" });
await session.exec("git clone https://github.com/user/repo.git");
await session.exec("cd repo && npm install");

// Second request - resume session (environment and cwd preserved)
const session = await sandbox.getSession("build");
const result = await session.exec("cd repo && npm run build");
```

```ts
// First request - create a task-specific session
const session = await sandbox.createSession({ id: 'build' });
await session.exec('git clone https://github.com/user/repo.git');
await session.exec('cd repo && npm install');

// Second request - resume session (environment and cwd preserved)
const session = await sandbox.getSession('build');
const result = await session.exec('cd repo && npm run build');
```

---

### `deleteSession()`

Delete a session and clean up its resources.

```ts
const result = await sandbox.deleteSession(sessionId: string): Promise<SessionDeleteResult>
```

**Parameters**:

* `sessionId` \- ID of the session to delete (cannot be `"default"`)

**Returns**: `Promise<SessionDeleteResult>` containing:

* `success` \- Whether deletion succeeded
* `sessionId` \- ID of the deleted session
* `timestamp` \- Deletion timestamp

```js
// Create a temporary session for a specific task
const tempSession = await sandbox.createSession({ id: "temp-task" });

try {
	await tempSession.exec("npm run heavy-task");
} finally {
	// Clean up the session when done
	await sandbox.deleteSession("temp-task");
}
```

```ts
// Create a temporary session for a specific task
const tempSession = await sandbox.createSession({ id: 'temp-task' });

try {
  await tempSession.exec('npm run heavy-task');
} finally {
  // Clean up the session when done
  await sandbox.deleteSession('temp-task');
}
```

Caution

Deleting a session immediately terminates all running commands. The default session cannot be deleted.

---

### `setEnvVars()`

Set environment variables in the sandbox.

```ts
await sandbox.setEnvVars(envVars: Record<string, string | undefined>): Promise<void>
```

**Parameters**:

* `envVars` \- Key-value pairs of environment variables to set or unset  
  * `string` values: Set the environment variable
  * `undefined` or `null` values: Unset the environment variable

Caution

Call `setEnvVars()` **before** any other sandbox operations to ensure environment variables are available from the start.

```js
const sandbox = getSandbox(env.Sandbox, "user-123");

// Set environment variables first
await sandbox.setEnvVars({
	API_KEY: env.OPENAI_API_KEY,
	DATABASE_URL: env.DATABASE_URL,
	NODE_ENV: "production",
	OLD_TOKEN: undefined, // Unsets OLD_TOKEN if previously set
});

// Now commands can access these variables
await sandbox.exec("python script.py");
```

```ts
const sandbox = getSandbox(env.Sandbox, 'user-123');

// Set environment variables first
await sandbox.setEnvVars({
  API_KEY: env.OPENAI_API_KEY,
  DATABASE_URL: env.DATABASE_URL,
  NODE_ENV: 'production',
  OLD_TOKEN: undefined // Unsets OLD_TOKEN if previously set
});

// Now commands can access these variables
await sandbox.exec('python script.py');
```

---

## ExecutionSession methods

The `ExecutionSession` object has all sandbox methods bound to the specific session:

| Category             | Methods                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Commands**         | [exec()](https://developers.cloudflare.com/sandbox/api/commands/#exec), [execStream()](https://developers.cloudflare.com/sandbox/api/commands/#execstream)                                                                                                                                                                                                                                                                                                                                                                                                           |
| **Processes**        | [startProcess()](https://developers.cloudflare.com/sandbox/api/commands/#startprocess), [listProcesses()](https://developers.cloudflare.com/sandbox/api/commands/#listprocesses), [killProcess()](https://developers.cloudflare.com/sandbox/api/commands/#killprocess), [killAllProcesses()](https://developers.cloudflare.com/sandbox/api/commands/#killallprocesses), [getProcessLogs()](https://developers.cloudflare.com/sandbox/api/commands/#getprocesslogs), [streamProcessLogs()](https://developers.cloudflare.com/sandbox/api/commands/#streamprocesslogs) |
| **Files**            | [writeFile()](https://developers.cloudflare.com/sandbox/api/files/#writefile), [readFile()](https://developers.cloudflare.com/sandbox/api/files/#readfile), [mkdir()](https://developers.cloudflare.com/sandbox/api/files/#mkdir), [deleteFile()](https://developers.cloudflare.com/sandbox/api/files/#deletefile), [renameFile()](https://developers.cloudflare.com/sandbox/api/files/#renamefile), [moveFile()](https://developers.cloudflare.com/sandbox/api/files/#movefile), [gitCheckout()](https://developers.cloudflare.com/sandbox/api/files/#gitcheckout)  |
| **Environment**      | [setEnvVars()](https://developers.cloudflare.com/sandbox/api/sessions/#setenvvars)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| **Terminal**         | [terminal()](https://developers.cloudflare.com/sandbox/api/terminal/#terminal)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| **Code Interpreter** | [createCodeContext()](https://developers.cloudflare.com/sandbox/api/interpreter/#createcodecontext), [runCode()](https://developers.cloudflare.com/sandbox/api/interpreter/#runcode), [listCodeContexts()](https://developers.cloudflare.com/sandbox/api/interpreter/#listcodecontexts), [deleteCodeContext()](https://developers.cloudflare.com/sandbox/api/interpreter/#deletecodecontext)                                                                                                                                                                         |

## Related resources

* [Session management concept](https://developers.cloudflare.com/sandbox/concepts/sessions/) \- How sessions work
* [Commands API](https://developers.cloudflare.com/sandbox/api/commands/) \- Execute commands

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/api/sessions/#page","headline":"Sessions · Cloudflare Sandbox SDK docs","description":"Create shell sessions with independent working directories and environment variables within a sandbox.","url":"https://developers.cloudflare.com/sandbox/api/sessions/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Mount S3-compatible storage buckets into the Sandbox SDK filesystem for persistent data access.
title: Storage
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Storage

Last updated Jun 8, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/storage/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Mount S3-compatible storage buckets (R2, S3, GCS) into the sandbox filesystem for persistent data access. `mountBucket()` supports R2 binding mounts, local R2 binding sync during development, and remote S3-compatible endpoint mounts.

## Methods

### `mountBucket()`

Mount an S3-compatible bucket to a local path in the sandbox.

```ts
await sandbox.mountBucket(
  bucket: string,
  mountPath: string,
  options?: MountBucketOptions
): Promise<void>
```

**Parameters**:

* `bucket` \- Bucket identifier  
  * When `options.endpoint` is omitted, pass the Worker R2 binding name (for example, `"MY_BUCKET"`)
  * When `options.endpoint` is provided, pass the remote bucket name (for example, `"my-r2-bucket"`)
* `mountPath` \- Local filesystem path to mount at (e.g., `"/data"`)
* `options` (optional) - Mount configuration (see [MountBucketOptions](#mountbucketoptions))

```js
// Mount an R2 bucket by Worker binding name
await sandbox.mountBucket("MY_BUCKET", "/data");

// Read/write files directly
const data = await sandbox.readFile("/data/config.json");
await sandbox.writeFile("/data/results.json", JSON.stringify(data));

// Mount a remote S3-compatible bucket, including explicit R2 endpoints
await sandbox.mountBucket("my-bucket", "/storage", {
	endpoint: "https://s3.amazonaws.com",
	credentials: {
		accessKeyId: env.AWS_ACCESS_KEY_ID,
		secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
	},
});

// Mount an R2 bucket during local development with wrangler dev
await sandbox.mountBucket("MY_BUCKET", "/local-data", {
	localBucket: true,
});

// Mount a prefix from an R2 binding
await sandbox.mountBucket("MY_BUCKET", "/user-data", {
	prefix: "/users/user-123",
	readOnly: true,
});
```

```plaintext
// Mount an R2 bucket by Worker binding name
await sandbox.mountBucket('MY_BUCKET', '/data');

// Read/write files directly
const data = await sandbox.readFile('/data/config.json');
await sandbox.writeFile('/data/results.json', JSON.stringify(data));

// Mount a remote S3-compatible bucket, including explicit R2 endpoints
await sandbox.mountBucket('my-bucket', '/storage', {
  endpoint: 'https://s3.amazonaws.com',
  credentials: {
    accessKeyId: env.AWS_ACCESS_KEY_ID,
    secretAccessKey: env.AWS_SECRET_ACCESS_KEY
  }
});

// Mount an R2 bucket during local development with wrangler dev
await sandbox.mountBucket('MY_BUCKET', '/local-data', {
  localBucket: true
});

// Mount a prefix from an R2 binding
await sandbox.mountBucket('MY_BUCKET', '/user-data', {
  prefix: '/users/user-123',
  readOnly: true
});
```

**Throws**:

* `InvalidMountPointError` \- Invalid mount path or conflicts with existing mounts
* `BucketAccessError` \- Bucket does not exist or insufficient permissions

Authentication

Authentication depends on the mount mode:

1. Omit `endpoint` to mount an R2 bucket by Worker binding name in production
2. Set `localBucket: true` to use the same R2 binding during local development
3. Set `endpoint` to mount a remote S3-compatible bucket, then provide explicit `credentials` or rely on environment variables (`R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` or `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`)

Endpoint-based mounts remain supported for explicit R2 endpoint configuration and for other S3-compatible providers.

See the [Mount Buckets guide](https://developers.cloudflare.com/sandbox/guides/mount-buckets/) for detailed authentication options.

### `unmountBucket()`

Unmount a previously mounted bucket.

```ts
await sandbox.unmountBucket(mountPath: string): Promise<void>
```

**Parameters**:

* `mountPath` \- Path where the bucket is mounted (e.g., `"/data"`)

```js
// Mount, process, unmount
await sandbox.mountBucket("MY_BUCKET", "/data");
await sandbox.exec("python process.py");

// Unmount
await sandbox.unmountBucket("/data");
```

```plaintext
// Mount, process, unmount
await sandbox.mountBucket('MY_BUCKET', '/data');
await sandbox.exec('python process.py');

// Unmount
await sandbox.unmountBucket('/data');
```

Automatic cleanup

Mounted buckets are automatically unmounted when the container is destroyed.

## Types

### `MountBucketOptions`

```ts
interface RemoteMountBucketOptions {
  endpoint: string;
  provider?: BucketProvider;
  credentials?: BucketCredentials;
  credentialProxy?: boolean;
  readOnly?: boolean;
  s3fsOptions?: string[];
  prefix?: string;
}

interface LocalMountBucketOptions {
  localBucket: true;
  prefix?: string;
  readOnly?: boolean;
}

interface R2BindingMountBucketOptions {
  endpoint?: never;
  prefix?: string;
  readOnly?: boolean;
  s3fsOptions?: string[];
}

type MountBucketOptions =
  | RemoteMountBucketOptions
  | LocalMountBucketOptions
  | R2BindingMountBucketOptions;
```

`mountBucket()` supports these three modes:

* **R2 binding mount** \- Omit `endpoint` to mount by Worker binding name in production

  * Uses credential-less egress interception for R2
  * Supports `prefix`, `readOnly`, and `s3fsOptions`
* **Local R2 binding mount** \- Set `localBucket: true` during `wrangler dev`

  * Uses the Worker R2 binding directly through local synchronization
  * Supports `prefix` and `readOnly`
* **Remote endpoint mount** \- Set `endpoint` to mount any S3-compatible provider

  * Supports explicit `credentials` or environment variable auto-detection
  * Set `credentialProxy: true` to keep credentials out of the container (egress interception)
  * Supports `provider`, `prefix`, `readOnly`, and `s3fsOptions`

**Field details**:

* `endpoint` (remote endpoint mode only) - S3-compatible endpoint URL

  * R2: `'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com'`
  * S3: `'https://s3.amazonaws.com'`
  * GCS: `'https://storage.googleapis.com'`
* `localBucket` (local development mode only) - Mount an R2 bucket using the Worker's R2 binding during local development with `wrangler dev`

  * When `true`, the SDK syncs the R2 binding directly instead of using an S3 endpoint
* `provider` (remote endpoint mode only) - Storage provider hint

  * Enables provider-specific optimizations
  * Values: `'r2'`, `'s3'`, `'gcs'`
* `credentials` (remote endpoint mode only) - API credentials

  * Contains `accessKeyId` and `secretAccessKey`
  * If not provided, uses environment variables
* `credentialProxy` (remote endpoint mode only) - Route S3 requests through the Durable Object for signing

  * When `true`, credentials are never written to the container's disk. The Durable Object intercepts and re-signs all outbound S3 requests at the network layer before forwarding them upstream.
  * Supports [AWS SigV4 ↗](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) signing for S3-compatible endpoints (including R2) and HMAC signing for Google Cloud Storage
  * Requires `ContainerProxy` to be exported from your Worker entrypoint
  * Default: `false` (backwards compatibility — recommended to set to `true`; will become the default in a future version)
* `readOnly` (optional) - Mount in read-only mode

  * Default: `false`
* `prefix` (optional) - Subdirectory within the bucket to mount

  * When specified, only contents under this prefix are visible at the mount point
  * Must start with `/` (for example, `/data/uploads` or `/data/uploads/`)
  * Default: Mount entire bucket
* `s3fsOptions` (R2 binding and remote endpoint modes only) - Advanced s3fs mount flags

  * Type: `string[]`
  * Example: `['use_cache=/tmp/cache', 'stat_cache_expire=1']`

### `BucketProvider`

Storage provider hint for automatic s3fs flag optimization.

```ts
type BucketProvider = "r2" | "s3" | "gcs";
```

* `'r2'` \- Cloudflare R2 (recommended, applies `nomixupload` flag)
* `'s3'` \- Amazon S3
* `'gcs'` \- Google Cloud Storage

## Related resources

* [Mount Buckets guide](https://developers.cloudflare.com/sandbox/guides/mount-buckets/) \- Complete bucket mounting walkthrough
* [Files API](https://developers.cloudflare.com/sandbox/api/files/) \- Read and write files

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/api/storage/#page","headline":"Storage · Cloudflare Sandbox SDK docs","description":"Mount S3-compatible storage buckets into the Sandbox SDK filesystem for persistent data access.","url":"https://developers.cloudflare.com/sandbox/api/storage/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-08","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Connect browser-based terminal UIs to sandbox shells via WebSocket.
title: Terminal
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Terminal

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/terminal/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Connect browser-based terminal UIs to sandbox shells via WebSocket. The server-side `terminal()` method proxies WebSocket connections to the container, and the client-side `SandboxAddon` integrates with xterm.js for terminal rendering.

Sandbox SDK 1.0 preview

This page documents terminal helpers on today's stable `@cloudflare/sandbox` package.

On **`@cloudflare/sandbox@next`**, terminals use `createTerminal`, `getTerminal`, and `terminal.connect`, with xterm `terminalId`. Refer to [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/) and [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/).

## Server-side methods

### `terminal()`

Proxy a WebSocket upgrade request to create a terminal connection.

```ts
const response = await sandbox.terminal(request: Request, options?: PtyOptions): Promise<Response>
```

**Parameters**:

* `request` \- WebSocket upgrade request from the browser (must include `Upgrade: websocket` header)
* `options` (optional):  
  * `cols` \- Terminal width in columns (default: `80`)
  * `rows` \- Terminal height in rows (default: `24`)

**Returns**: `Promise<Response>` — WebSocket upgrade response

```js
// In your Worker's fetch handler
return await sandbox.terminal(request, { cols: 120, rows: 30 });
```

```ts
// In your Worker's fetch handler
return await sandbox.terminal(request, { cols: 120, rows: 30 });
```

Works with both [default and explicitly created sessions](https://developers.cloudflare.com/sandbox/concepts/sessions/):

```js
// Default session
return await sandbox.terminal(request);

// Specific session
const session = await sandbox.getSession("dev");
return await session.terminal(request);
```

```ts
// Default session
return await sandbox.terminal(request);

// Specific session
const session = await sandbox.getSession('dev');
return await session.terminal(request);
```

## Client-side addon

The `@cloudflare/sandbox/xterm` module provides `SandboxAddon` for xterm.js, which handles the WebSocket connection, reconnection, and terminal resize forwarding.

### `SandboxAddon`

```ts
import { SandboxAddon } from '@cloudflare/sandbox/xterm';

const addon = new SandboxAddon(options: SandboxAddonOptions);
```

**Options**:

* `getWebSocketUrl(params)` \- Build the WebSocket URL for each connection attempt. Receives:  
  * `sandboxId` \- Target sandbox ID
  * `sessionId` (optional) - Target session ID
  * `origin` \- WebSocket origin derived from `window.location` (for example, `wss://example.com`)
* `reconnect` \- Enable automatic reconnection with exponential backoff (default: `true`)
* `onStateChange(state, error?)` \- Callback for connection state changes

```js
import { Terminal } from "@xterm/xterm";
import { SandboxAddon } from "@cloudflare/sandbox/xterm";

const terminal = new Terminal({ cursorBlink: true });
terminal.open(document.getElementById("terminal"));

const addon = new SandboxAddon({
	getWebSocketUrl: ({ sandboxId, sessionId, origin }) => {
		const params = new URLSearchParams({ id: sandboxId });
		if (sessionId) params.set("session", sessionId);
		return `${origin}/ws/terminal?${params}`;
	},
	onStateChange: (state, error) => {
		console.log(`Terminal ${state}`, error);
	},
});

terminal.loadAddon(addon);
addon.connect({ sandboxId: "my-sandbox" });
```

```ts
import { Terminal } from '@xterm/xterm';
import { SandboxAddon } from '@cloudflare/sandbox/xterm';

const terminal = new Terminal({ cursorBlink: true });
terminal.open(document.getElementById('terminal'));

const addon = new SandboxAddon({
  getWebSocketUrl: ({ sandboxId, sessionId, origin }) => {
    const params = new URLSearchParams({ id: sandboxId });
    if (sessionId) params.set('session', sessionId);
    return `${origin}/ws/terminal?${params}`;
  },
  onStateChange: (state, error) => {
    console.log(`Terminal ${state}`, error);
  }
});

terminal.loadAddon(addon);
addon.connect({ sandboxId: 'my-sandbox' });
```

### `connect()`

Establish a connection to a sandbox terminal.

```ts
addon.connect(target: ConnectionTarget): void
```

**Parameters**:

* `target`:  
  * `sandboxId` \- Sandbox to connect to
  * `sessionId` (optional) - Session within the sandbox

Calling `connect()` with a new target disconnects from the current target and connects to the new one. Calling it with the same target while already connected is a no-op.

### `disconnect()`

Close the connection and stop any reconnection attempts.

```ts
addon.disconnect(): void
```

### Properties

| Property  | Type                           | Description        |                          |
| --------- | ------------------------------ | ------------------ | ------------------------ |
| state     | 'disconnected' \| 'connecting' | 'connected'        | Current connection state |
| sandboxId | string \| undefined            | Current sandbox ID |                          |
| sessionId | string \| undefined            | Current session ID |                          |

## WebSocket protocol

The `SandboxAddon` handles the WebSocket protocol automatically. These details are for building custom terminal clients without the addon. For a complete example, refer to [Connect without xterm.js](https://developers.cloudflare.com/sandbox/guides/browser-terminals/#connect-without-xtermjs).

### Connection lifecycle

1. Client opens a WebSocket to your Worker endpoint. Set `binaryType` to `arraybuffer`.
2. The server replays any **buffered output** from a previous connection as binary frames. This may arrive before the `ready` message.
3. The server sends a `ready` status message — the terminal is now accepting input.
4. Binary frames flow in both directions: UTF-8 encoded keystrokes from the client, terminal output (including ANSI escape sequences) from the server.
5. If the client disconnects, the PTY stays alive. Reconnecting to the same session replays buffered output so the terminal appears unchanged.

### Control messages (client to server)

Send JSON text frames to control the terminal.

**Resize** — update terminal dimensions (both `cols` and `rows` must be positive):

```json
{ "type": "resize", "cols": 120, "rows": 30 }
```

### Status messages (server to client)

The server sends JSON text frames for lifecycle events.

**Ready** — the PTY is initialized. Buffered output (if any) has already been sent:

```json
{ "type": "ready" }
```

**Exit** — the shell process has terminated:

```json
{ "type": "exit", "code": 0, "signal": "SIGTERM" }
```

**Error** — an error occurred (for example, invalid control message or session not found):

```json
{ "type": "error", "message": "Session not found" }
```

## Types

```ts
interface PtyOptions {
	cols?: number;
	rows?: number;
}

type ConnectionState = "disconnected" | "connecting" | "connected";

interface ConnectionTarget {
	sandboxId: string;
	sessionId?: string;
}

interface SandboxAddonOptions {
	getWebSocketUrl: (params: {
		sandboxId: string;
		sessionId?: string;
		origin: string;
	}) => string;
	reconnect?: boolean;
	onStateChange?: (state: ConnectionState, error?: Error) => void;
}
```

## Related resources

* [Terminal connections](https://developers.cloudflare.com/sandbox/concepts/terminal/) — How terminal connections work
* [Browser terminals](https://developers.cloudflare.com/sandbox/guides/browser-terminals/) — Step-by-step setup guide
* [Sessions API](https://developers.cloudflare.com/sandbox/api/sessions/) — Session management
* [Commands API](https://developers.cloudflare.com/sandbox/api/commands/) — Non-interactive command execution

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/api/terminal/#page","headline":"Terminal · Cloudflare Sandbox SDK docs","description":"Connect browser-based terminal UIs to sandbox shells via WebSocket.","url":"https://developers.cloudflare.com/sandbox/api/terminal/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Expose sandbox services on the public internet with quick tunnels (*.trycloudflare.com) or named tunnels bound to a hostname on your Cloudflare zone.
title: Tunnels
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Tunnels

Last updated Jun 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/api/tunnels/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

The `sandbox.tunnels` namespace exposes a service running inside a sandbox on the public internet through a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/). The SDK runs `cloudflared` inside the container and opens a persistent QUIC connection to Cloudflare's edge.

Two flavors are available:

* **Quick tunnels** (`sandbox.tunnels.get(port)`) — zero-config. Cloudflare assigns a random `*.trycloudflare.com` hostname for each new `cloudflared` process. No Cloudflare account, API token, DNS record, or custom domain required. URLs change on every container restart.
* **Named tunnels** (`sandbox.tunnels.get(port, { name })`) — bind a stable hostname `<name>.<your-zone>` on a zone you control. The hostname survives container restarts and is shared across sandboxes that request the same `name`. Requires a Cloudflare API token, an account, and a zone.

When to use quick vs. named tunnels

Use **quick tunnels** for local development, demos, and short-lived `.workers.dev` deployments where you do not need a stable URL. Use **named tunnels** for everything else — they are the recommended option for production traffic, webhook receivers, OAuth callbacks, and any URL that needs to be bookmarked. [exposePort()](https://developers.cloudflare.com/sandbox/api/ports/) remains an alternative when you want the Worker itself (rather than Cloudflare's edge) to front the request.

## Requirements

Both tunnel flavors require:

* **RPC transport.** Calling `sandbox.tunnels` on HTTP/Websocket transports throws `"RPC transport required"`. See [Transport configuration](https://developers.cloudflare.com/sandbox/configuration/transport/).

Named tunnels additionally require a Cloudflare API token, account, and zone — refer to [Named tunnels: prerequisites](#prerequisites).

## Methods

### `tunnels.get()`

Return a tunnel record for `port`. The SDK spawns a fresh `cloudflared` process inside the container if not already running. The method is idempotent: repeated calls with the same `(port, options)` return the same record.

```ts
const tunnel = await sandbox.tunnels.get(
  port: number,
  options?: { name?: string }
): Promise<TunnelInfo>
```

**Parameters**:

* `port` — Port number inside the sandbox to expose (1024-65535, excluding reserved ports). The service to tunnel to must already be listening on `0.0.0.0:<port>` inside the container.
* `options.name` _(optional)_ — Single DNS label (lowercase letters, digits, internal hyphens; 1–63 chars; no dots). When set, provisions a [named tunnel](#named-tunnels) bound to `<name>.<your-zone>`. When omitted, provisions a quick tunnel.

**Returns**: `Promise<TunnelInfo>` — the tunnel record. See [TunnelInfo](#tunnelinfo).

Calling `get(port)` with different `options` on a port that already has a tunnel throws. Call [destroy(port)](#tunnelsdestroy) first.

```js
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		const sandbox = getSandbox(env.Sandbox, "my-sandbox");

		await sandbox.startProcess("python -m http.server 8080");

		const tunnel = await sandbox.tunnels.get(8080);
		console.log(tunnel.url);
		// → https://random-words-here.trycloudflare.com

		// Repeated calls for the same port return the same record.
		const same = await sandbox.tunnels.get(8080);
		console.log(same.url === tunnel.url); // true

		return Response.json({ url: tunnel.url });
	},
};
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const sandbox = getSandbox(env.Sandbox, "my-sandbox");

    await sandbox.startProcess("python -m http.server 8080");

    const tunnel = await sandbox.tunnels.get(8080);
    console.log(tunnel.url);
    // → https://random-words-here.trycloudflare.com

    // Repeated calls for the same port return the same record.
    const same = await sandbox.tunnels.get(8080);
    console.log(same.url === tunnel.url); // true

    return Response.json({ url: tunnel.url });

},
};
```

### `tunnels.list()`

Return every tunnel currently tracked for this sandbox.

```ts
const tunnels = await sandbox.tunnels.list(): Promise<TunnelInfo[]>
```

**Returns**: `Promise<TunnelInfo[]>` — an array of [TunnelInfo](#tunnelinfo) records. Empty when no tunnels are active.

```js
const tunnels = await sandbox.tunnels.list();

for (const tunnel of tunnels) {
	console.log(`port ${tunnel.port} → ${tunnel.url}`);
}
```

```ts
const tunnels = await sandbox.tunnels.list();

for (const tunnel of tunnels) {
console.log(`port ${tunnel.port} → ${tunnel.url}`);
}
```

### `tunnels.destroy()`

Tear down a tunnel. Accepts either the port number or the `TunnelInfo` record returned by `get()`. Idempotent — destroying an unknown port resolves successfully.

```ts
await sandbox.tunnels.destroy(portOrInfo: number | TunnelInfo): Promise<void>
```

**Parameters**:

* `portOrInfo` — Either the port number or the `TunnelInfo` record returned by [get()](#tunnelsget).

```js
const tunnel = await sandbox.tunnels.get(8080);

// Tear down by port number...
await sandbox.tunnels.destroy(8080);

// ...or by the record.
await sandbox.tunnels.destroy(tunnel);
```

```ts
const tunnel = await sandbox.tunnels.get(8080);

// Tear down by port number...
await sandbox.tunnels.destroy(8080);

// ...or by the record.
await sandbox.tunnels.destroy(tunnel);
```

## Types

### `TunnelInfo`

Quick tunnels omit `name`; named tunnels carry the label passed via `options.name`.

| Field     | Type   | Description                                                                                        |
| --------- | ------ | -------------------------------------------------------------------------------------------------- |
| id        | string | Tunnel identifier. quick-<random> for quick tunnels, the Cloudflare Tunnel UUID for named tunnels. |
| port      | number | Port number inside the sandbox that the tunnel proxies to.                                         |
| url       | string | Public URL — https://<random>.trycloudflare.com (quick) or https://<name>.<your-zone> (named).     |
| hostname  | string | Hostname component of url.                                                                         |
| createdAt | string | ISO-8601 timestamp of when the tunnel was created.                                                 |
| name      | string | **Named tunnels only.** The label passed via options.name. Absent on quick tunnels.                |

```ts
type TunnelInfo = QuickTunnelInfo | NamedTunnelInfo;

interface QuickTunnelInfo {
  id: string;
  port: number;
  url: string;
  hostname: string;
  createdAt: string;
  name?: never;
}

interface NamedTunnelInfo {
  id: string;
  port: number;
  url: string;
  hostname: string;
  createdAt: string;
  name: string;
}
```

## Named tunnels

Named tunnels bind a user-controlled hostname — `<name>.<your-zone>` — backed by a managed [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/) and a proxied `CNAME` record on your zone. Unlike quick tunnels, the URL is **stable across container restarts** and **shared across sandboxes** that call `get(port, { name })` with the same `name`.

### How they differ from quick tunnels

| Aspect                    | Quick tunnel                                        | Named tunnel                                                                                                                              |
| ------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Hostname                  | Random \*.trycloudflare.com, assigned by Cloudflare | <name>.<your-zone>, chosen by you                                                                                                         |
| Stability                 | Changes on every container restart                  | Stable; persists across restarts and sandbox lifecycles                                                                                   |
| Cloudflare account        | Not required                                        | Required (API token + zone)                                                                                                               |
| Cloudflare-side resources | None                                                | Managed [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/) \+ proxied DNS CNAME |
| Uptime guarantee          | None (debug aid)                                    | Backed by your zone's standard Cloudflare SLA                                                                                             |
| TLS certificate           | Cloudflare-owned wildcard                           | Universal SSL on <name>.<your-zone> (single DNS label only)                                                                               |
| Server-Sent Events        | Not supported (edge buffers text/event-stream)      | Supported                                                                                                                                 |

### Prerequisites

To provision a named tunnel, you need:

1. A **Cloudflare account** with a **zone** (a domain you control on Cloudflare DNS).
2. A **Cloudflare API token** with the correct scopes.
3. The **account ID** and **zone ID** — the SDK can infer both from the token when the token is scoped to exactly one of each.

#### Create the API token

Create a token from **My Profile** \> **API Tokens** \> **Create Token** \> **Custom token** with the following permissions:

| Scope                                                      | Used for                                                                  |
| ---------------------------------------------------------- | ------------------------------------------------------------------------- |
| **Account** · **Cloudflare Tunnel** · **Edit**             | Create, look up, and delete tunnels.                                      |
| **Zone** · **DNS** · **Edit**                              | Upsert and delete the proxied CNAME for <name>.<your-zone>.               |
| **Zone** · **Zone** · **Read**                             | Look up the zone's name to derive <name>.<your-zone>.                     |
| **Account** · **Account Settings** · **Read** _(optional)_ | Lets the SDK infer the account ID from the token when not set explicitly. |

Under **Account Resources**, scope the token to the account that will own the tunnel. Under **Zone Resources**, scope it to the specific zone you want to bind to.

Both [User API Tokens](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) and [Account API Tokens](https://developers.cloudflare.com/fundamentals/api/get-started/account-owned-tokens/) (secret prefixed `cfat_`) are supported — the SDK detects the token kind and uses the appropriate introspection endpoint.

#### Create the token with the REST API

You can create the token without using the dashboard. The permission group IDs are stable; the snippet below uses placeholders — fetch the current IDs from [GET /user/tokens/permission\_groups](https://developers.cloudflare.com/api/operations/permission-groups-list-permission-groups/).

```bash
curl -X POST "https://api.cloudflare.com/client/v4/user/tokens" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "sandbox-named-tunnels",
    "policies": [
      {
        "effect": "allow",
        "resources": { "com.cloudflare.api.account.<ACCOUNT_ID>": "*" },
        "permission_groups": [{ "id": "<TUNNEL_EDIT_GROUP_ID>" }]
      },
      {
        "effect": "allow",
        "resources": { "com.cloudflare.api.account.zone.<ZONE_ID>": "*" },
        "permission_groups": [
          { "id": "<DNS_EDIT_GROUP_ID>" },
          { "id": "<ZONE_READ_GROUP_ID>" }
        ]
      }
    ]
  }'
```

#### Bind the token and IDs to the Worker

The SDK reads `CLOUDFLARE_API_TOKEN` from the Worker environment and attempts to derive the account ID and zone ID from the token automatically. If the token is associated with multiple accounts or zones the SDK cannot pick one unambiguously, and you must set `CLOUDFLARE_ACCOUNT_ID` and/or `CLOUDFLARE_ZONE_ID` explicitly.

| Variable                | Required?                                | Notes                                       |
| ----------------------- | ---------------------------------------- | ------------------------------------------- |
| CLOUDFLARE\_API\_TOKEN  | Yes                                      | Store as a secret with wrangler secret put. |
| CLOUDFLARE\_ACCOUNT\_ID | Only if the token sees multiple accounts | Inferred from the token otherwise.          |
| CLOUDFLARE\_ZONE\_ID    | Only if the token sees multiple zones    | Inferred from the token otherwise.          |

```bash
npx wrangler secret put CLOUDFLARE_API_TOKEN
```

For local development, place the variables in `.dev.vars` (gitignored). For production, set the non-secret IDs (when needed) under `vars` in your Wrangler config:

```jsonc
{
  "vars": {
    "CLOUDFLARE_ACCOUNT_ID": "<account-id>",
    "CLOUDFLARE_ZONE_ID": "<zone-id>"
  }
}
```

When inference fails, the SDK throws a clear error naming the variable to set.

### Example

```js
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		const sandbox = getSandbox(env.Sandbox, "my-sandbox");

		// Reuse an existing app process across container restarts, or start it.
		let proc = await sandbox.getProcess("app");
		if (!proc) {
			try {
				proc = await sandbox.startProcess("python -m http.server 8080", {
					processId: "app",
				});
			} catch (err) {
				if (err?.code !== "PROCESS_ALREADY_EXISTS") throw err;
				proc = await sandbox.getProcess("app");
			}
		}

		// Provision (or reuse) https://app.example.com pointing at port 8080.
		const tunnel = await sandbox.tunnels.get(8080, { name: "app" });
		console.log(tunnel.url); // → https://app.example.com

		return Response.json({ url: tunnel.url });
	},
};
```

```ts
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const sandbox = getSandbox(env.Sandbox, "my-sandbox");

    // Reuse an existing app process across container restarts, or start it.
    let proc = await sandbox.getProcess('app');
    if (!proc) {
      try {
        proc = await sandbox.startProcess('python -m http.server 8080', { processId: 'app' });
      } catch (err) {
        if ((err as { code?: string })?.code !== 'PROCESS_ALREADY_EXISTS') throw err;
        proc = await sandbox.getProcess('app');
      }
    }

    // Provision (or reuse) https://app.example.com pointing at port 8080.
    const tunnel = await sandbox.tunnels.get(8080, { name: "app" });
    console.log(tunnel.url); // → https://app.example.com

    return Response.json({ url: tunnel.url });
  },
};
```

\`name\` must be a single DNS label

`name` must match `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$` and be 1–63 characters — no dots, no uppercase, no leading/trailing hyphens. This restriction exists because Cloudflare Universal SSL only issues certificates for `<label>.<zone>`. Multi-label hostnames need [Advanced Certificate Manager](https://developers.cloudflare.com/ssl/edge-certificates/advanced-certificate-manager/) or a delegated subdomain zone, which are out of scope for `sandbox.tunnels`.

### Lifecycle

Named tunnels are designed to outlive the container that provisioned them:

1. **First call** to `sandbox.tunnels.get(port, { name })`:  
  * Resolves `<name>.<your-zone>` from the configured zone ID.
  * Creates a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/) resource named `sandbox-<sandbox-id>-<name>` tagged with the sandbox ID.
  * Upserts a proxied `CNAME` from `<name>.<your-zone>` to `<tunnel-id>.cfargotunnel.com`.
  * Spawns `cloudflared` inside the container with the tunnel's token.
2. **Subsequent calls** with the same `(port, name)` return the cached record without contacting Cloudflare.
3. **Container restart** (Durable Object eviction, deploy, crash):  
  * `cloudflared` dies with the container, but the Cloudflare Tunnel and DNS record are preserved.
  * On the next `get(port, { name })`, the SDK rediscovers the tagged tunnel via the Cloudflare API and respawns `cloudflared`. The hostname is unchanged.
4. **Explicit teardown** with `sandbox.tunnels.destroy(port)`:  
  * Stops `cloudflared` inside the container.
  * Deletes the Cloudflare Tunnel resource.
  * Deletes the proxied `CNAME` record.
5. **Sandbox destroy** with `sandbox.destroy()` tears down every tunnel the sandbox provisioned, including the Cloudflare-side resources, before stopping the container.

If `destroy()` fails to reach the Cloudflare API (for example, the token was revoked between `get()` and `destroy()`), the SDK logs a warning naming the orphaned `tunnelId` and `dnsRecordId` so you can clean up manually from the dashboard.

### Cloudflare resources and tagging

Named tunnels create resources **on your Cloudflare account, outside the sandbox container**. They are not stored in Durable Object storage and do not count against sandbox quotas, but they do show up in the Cloudflare dashboard and consume your account's tunnel and DNS quotas.

For each `(sandbox, name)` pair, the SDK creates:

| Resource           | Name / location                   | Identifier                                         |
| ------------------ | --------------------------------- | -------------------------------------------------- |
| Cloudflare Tunnel  | **Networking** \> **Tunnels**     | sandbox-<sandbox-id>-<name>                        |
| Proxied DNS record | Your zone, **DNS** \> **Records** | CNAME <name>.<zone> → <tunnel-id>.cfargotunnel.com |

Both resources are tagged so you can audit, query, and bulk-clean them from the dashboard or API:

* **Tunnel metadata**: `{ sandboxId, createdBy: 'sandbox-sdk', name, port }`
* **DNS record comment**: `sandbox-<sandbox-id>`
* **Resource tag** _(Enterprise plans only)_: `sandboxId:<sandbox-id>`

On non-Enterprise plans, Cloudflare rejects resource tags; the SDK detects this and retries the request without tags. The DNS comment and tunnel metadata still apply, so you can always trace a resource back to its sandbox.

To list every tunnel created by the SDK for a given account:

```bash
curl "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/cfd_tunnel?name=sandbox-" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
```

## Limitations

**Both tunnel flavors:**

* **WARP / Zero Trust egress.** If your local machine runs Cloudflare WARP or another Zero Trust egress policy, outbound traffic to `api.trycloudflare.com` and the cloudflared edge can be blocked. When that happens, `tunnels.get()` hangs on the edge handshake and eventually times out. Disable WARP or add an egress exception for these destinations.
* **Brief DNS warm-up.** The first request through a brand-new URL can take a couple of seconds while DNS propagates, even after `get()` resolves.

**Quick tunnels only:**

* **URLs do not survive container restart.** Cloudflare assigns the hostname during `cloudflared`'s startup handshake, so every restart yields a new URL. The SDK clears its tunnel cache when the container starts, so the next `tunnels.get(port)` returns a fresh record. Use a [named tunnel](#named-tunnels) for a stable hostname.
* **No uptime guarantee.** Cloudflare positions `trycloudflare.com` as a debug aid, not a production target.
* **No Server-Sent Events.** The `trycloudflare.com` edge buffers `text/event-stream` responses, so SSE events never reach the client. WebSockets work normally. Use a [named tunnel](#named-tunnels) if your service streams SSE.

**Named tunnels only:**

* **Single DNS label.** `name` must not contain dots. Universal SSL only covers `<name>.<your-zone>`.
* **Counts against your zone's quotas.** Each named tunnel creates a Cloudflare Tunnel and a DNS record on your account. See [Cloudflare Tunnel limits](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/).
* **Cleanup requires the API token.** If `destroy()` runs after the token has been revoked, the Cloudflare-side resources are orphaned. The SDK logs the orphan IDs so you can remove them manually.

## Related resources

* [Preview URLs concept](https://developers.cloudflare.com/sandbox/concepts/preview-urls/) — Worker-fronted preview URLs and how they differ from quick tunnels.
* [Ports API](https://developers.cloudflare.com/sandbox/api/ports/) — `exposePort()` and the Worker-fronted preview URL flow.
* [Expose services guide](https://developers.cloudflare.com/sandbox/guides/expose-services/) — End-to-end walkthrough for exposing services in production.
* [Transport configuration](https://developers.cloudflare.com/sandbox/configuration/transport/) — RPC vs. route-based transport.

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/api/tunnels/#page","headline":"Tunnels · Cloudflare Sandbox SDK docs","description":"Expose sandbox services on the public internet with quick tunnels (*.trycloudflare.com) or named tunnels bound to a hostname on your Cloudflare zone.","url":"https://developers.cloudflare.com/sandbox/api/tunnels/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-23","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Learn how the Sandbox SDK works, including architecture, lifecycle, security, and sessions.
title: Concepts
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Concepts

Last updated Sep 1, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/concepts/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

These pages explain how the Sandbox SDK works, why it's designed the way it is, and the concepts you need to understand to use it effectively.

* [Architecture](https://developers.cloudflare.com/sandbox/concepts/architecture/) \- How the SDK is structured and why
* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/concepts/sandboxes/) \- Understanding sandbox states and behavior
* [Container runtime](https://developers.cloudflare.com/sandbox/concepts/containers/) \- How code executes in isolated containers
* [Session management](https://developers.cloudflare.com/sandbox/concepts/sessions/) \- When and how to use sessions
* [Preview URLs](https://developers.cloudflare.com/sandbox/concepts/preview-urls/) \- How to expose sandboxed services on the public internet.
* [Security model](https://developers.cloudflare.com/sandbox/concepts/security/) \- Isolation, validation, and safety mechanisms
* [Terminal connections](https://developers.cloudflare.com/sandbox/concepts/terminal/) \- How browser terminal connections work
* [Directory backups](https://developers.cloudflare.com/sandbox/concepts/backup-restore/) \- Overlay restore, local extract, and cross-device renames

## Related resources

* [Tutorials](https://developers.cloudflare.com/sandbox/tutorials/) \- Learn by building complete applications
* [How-to guides](https://developers.cloudflare.com/sandbox/guides/) \- Solve specific problems
* [API reference](https://developers.cloudflare.com/sandbox/api/) \- Technical details and method signatures

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/sandbox/concepts/#page","headline":"Concepts · Cloudflare Sandbox SDK docs","description":"Learn how the Sandbox SDK works, including architecture, lifecycle, security, and sessions.","url":"https://developers.cloudflare.com/sandbox/concepts/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-09-01","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Sandbox SDK combines Workers, Durable Objects, and Containers for secure code execution.
title: Architecture
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Architecture

Last updated Aug 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/concepts/architecture/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Sandbox SDK lets you execute untrusted code safely from your Workers. It combines three Cloudflare technologies to provide secure, stateful, and isolated execution:

* **Workers** \- Your application logic that calls the Sandbox SDK
* **Durable Objects** \- Persistent sandbox instances with unique identities
* **Containers** \- Isolated Linux environments where code actually runs

## Architecture overview

flowchart TB
    accTitle: Sandbox SDK Architecture
    accDescr: Three-layer architecture showing how Cloudflare Sandbox SDK combines Workers, Durable Objects, and Containers for secure code execution

    subgraph UserSpace["<b>Your Worker</b>"]
        Worker["Application code using the methods exposed by the Sandbox SDK"]
    end

    subgraph SDKSpace["<b>Sandbox SDK Implementation</b>"]
        DO["Sandbox Durable Object routes requests & maintains state"]
        Container["Isolated Ubuntu container executes untrusted code safely"]

        DO -->|HTTP API| Container
    end

    Worker -->|RPC call via the Durable Object stub returned by `getSandbox`| DO

    style UserSpace fill:#fff8f0,stroke:#f6821f,stroke-width:2px
    style SDKSpace fill:#f5f5f5,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5
    style Worker fill:#ffe8d1,stroke:#f6821f,stroke-width:2px
    style DO fill:#dce9f7,stroke:#1d8cf8,stroke-width:2px
    style Container fill:#d4f4e2,stroke:#17b26a,stroke-width:2px

### Layer 1: Client SDK

The developer-facing API you use in your Workers:

```typescript
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const result = await sandbox.exec("python script.py");
```

**Purpose**: Provide a clean, type-safe TypeScript interface for all sandbox operations.

### Layer 2: Durable Object

Manages sandbox lifecycle and routing:

```typescript
export class Sandbox extends DurableObject<Env> {
	// Extends Cloudflare Container for isolation
	// Routes requests between client and container
	// Manages preview URLs and state
}
```

**Purpose**: Provide persistent, stateful sandbox instances with unique identities.

**Why Durable Objects**:

* **Persistent identity** \- Same sandbox ID always routes to same instance
* **Container management** \- Durable Object owns and manages the container lifecycle
* **Geographic distribution** \- Sandboxes run close to users
* **Automatic scaling** \- Cloudflare manages provisioning

### Layer 3: Container Runtime

Executes code in isolation with full Linux capabilities.

**Purpose**: Safely execute untrusted code.

**Why containers**:

* **VM-based isolation** \- Each sandbox runs in its own VM
* **Full environment** \- Ubuntu Linux with Python, Node.js, Git, etc.

## Communication transports

The SDK supports three transport protocols for communication between the Durable Object and container:

### HTTP transport (default)

Each SDK method makes a separate HTTP request to the container API. Simple, reliable, and works for most use cases.

```typescript
// Default behavior - uses HTTP
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
await sandbox.exec("python script.py");
```

### RPC transport

Multiplexes all SDK calls over a single persistent connection. It avoids [subrequest limits](https://developers.cloudflare.com/workers/platform/limits/#subrequests) when making many concurrent operations.

Enable RPC transport by setting the `SANDBOX_TRANSPORT` variable in your Worker's configuration:

```jsonc
{
	"vars": {
		"SANDBOX_TRANSPORT": "rpc"
	},
}
```

```toml
[vars]
SANDBOX_TRANSPORT = "rpc"
```

### WebSocket transport

WebSocket transport is deprecated. Use RPC transport for new applications.

The transport layer is transparent to your application code — all SDK methods work identically regardless of transport. For details on when to use each transport and configuration examples, refer to [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/).

## Request flow

When you execute a command:

```typescript
await sandbox.exec("python script.py");
```

**HTTP transport flow**:

1. **Client SDK** validates parameters and sends HTTP request to Durable Object
2. **Durable Object** authenticates and forwards HTTP request to container
3. **Container Runtime** validates inputs, executes command, captures output
4. **Response flows back** through all layers with proper error transformation

**RPC transport flow**:

1. **Client SDK** validates parameters and sends the request to the Durable Object
2. **Durable Object** maintains the persistent connection to the container and multiplexes concurrent requests
3. **Container Runtime** adapts RPC messages to HTTP-style request and response handling
4. **Response flows back** over the same connection with proper error transformation

The Durable Object establishes the persistent connection to the container on first SDK call and reuses it for all subsequent operations, reducing overhead for high-frequency operations.

## Related resources

* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/concepts/sandboxes/) \- How sandboxes are created and managed
* [Container runtime](https://developers.cloudflare.com/sandbox/concepts/containers/) \- Inside the execution environment
* [Security model](https://developers.cloudflare.com/sandbox/concepts/security/) \- How isolation and validation work
* [Session management](https://developers.cloudflare.com/sandbox/concepts/sessions/) \- Advanced state management

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/concepts/architecture/#page","headline":"Architecture · Cloudflare Sandbox SDK docs","description":"Sandbox SDK combines Workers, Durable Objects, and Containers for secure code execution.","url":"https://developers.cloudflare.com/sandbox/concepts/architecture/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-06","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Production restore mounts a copy-on-write overlay. Local restore extracts the archive instead.
title: Directory backups
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Directory backups

Last updated Sep 1, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/concepts/backup-restore/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Backup and restore snapshot a sandbox directory into an R2 archive, then bring that tree back later. The public API is the same in production and in `wrangler dev`. The restore mechanism is not.

Use backups when you want a project directory such as `/workspace` to return later. Use [bucket mounts](https://developers.cloudflare.com/sandbox/guides/mount-buckets/) when a separate storage path such as `/data` should persist independently of the sandbox filesystem.

## Production restore

In production, `restoreBackup()` mounts the squashfs archive with FUSE overlayfs:

* The backup is a read-only lower layer.
* New writes go to a writable upper layer.
* The original archive in R2 does not change.
* Restoring the same handle again discards the upper layer.

The overlay exists only while the container is running. When the sandbox sleeps or the container restarts, the mount is gone and the directory is empty. Store the `DirectoryBackup` handle and restore again.

## Local restore

With `localBucket: true`, `wrangler dev` extracts the archive with `unsquashfs`. The target directory is replaced. There is no overlay, so local restore does not reproduce production FUSE behavior.

## Cross-device renames

Overlayfs treats the lower and upper layers as different devices. A rename that moves a directory from the restored lower layer into the writable upper layer can fail with `EXDEV` (`cross-device link not permitted`).

Vite does this with `node_modules/.vite/deps`. Omit that directory from the backup, or delete it after restore.

For the procedure, refer to [Exclude generated caches](https://developers.cloudflare.com/sandbox/guides/backup-restore/#exclude-generated-caches).

## Related resources

* [Backup and restore](https://developers.cloudflare.com/sandbox/guides/backup-restore/) \- Create, restore, and exclude caches
* [Backups API](https://developers.cloudflare.com/sandbox/api/backups/) \- Method signatures and options
* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/concepts/sandboxes/) \- What happens when a sandbox sleeps

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/concepts/backup-restore/#page","headline":"Directory backups · Cloudflare Sandbox SDK docs","description":"Production restore mounts a copy-on-write overlay. Local restore extracts the archive instead.","url":"https://developers.cloudflare.com/sandbox/concepts/backup-restore/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-09-01","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Sandbox SDK containers run isolated Linux environments with Python, Node.js, and common dev tools.
title: Container runtime
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Container runtime

Last updated Aug 28, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/concepts/containers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Coming soon: Sandbox SDK 1.0

This page documents containers on today's stable `@cloudflare/sandbox` package.

Examples that use `startProcess` apply to the stable package. On **`@next`**, long-running work uses `exec(argv)` process handles — [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/).

Each sandbox runs in an isolated Linux container with Python, Node.js, and common development tools pre-installed. For a complete list of pre-installed software and how to customize the container image, see [Dockerfile reference](https://developers.cloudflare.com/sandbox/configuration/dockerfile/).

## Runtime software installation

Install additional software at runtime using standard package managers:

```bash
# Python packages
pip install scikit-learn tensorflow

# Node.js packages
npm install express

# System packages (requires apt-get update first)
apt-get update && apt-get install -y redis-server
```

## Filesystem

The container provides a standard Linux filesystem. You can read and write anywhere you have permissions.

**Standard directories**:

* `/workspace` \- Default working directory for user code
* `/tmp` \- Temporary files
* `/home` \- User home directory
* `/usr/bin`, `/usr/local/bin` \- Executable binaries

**Example**:

```typescript
await sandbox.writeFile('/workspace/app.py', 'print("Hello")');
await sandbox.writeFile('/tmp/cache.json', '{}');
await sandbox.exec('ls -la /workspace');
```

## Process management

Processes run as you'd expect in a regular Linux environment.

**Foreground processes** (`exec()`):

```typescript
const result = await sandbox.exec('npm test');
// Waits for completion, returns output
```

**Background processes** (`startProcess()`):

```typescript
const process = await sandbox.startProcess('node server.js');
// Returns immediately, process runs in background
```

## Network capabilities

**Outbound connections** work:

```bash
curl https://api.example.com/data
pip install requests
npm install express
```

**Inbound connections** require port exposure:

```typescript
const { hostname } = new URL(request.url);
await sandbox.startProcess('python -m http.server 8000');
const exposed = await sandbox.exposePort(8000, { hostname });
console.log(exposed.url); // Public URL
```

Local development

When using `wrangler dev`, you must add `EXPOSE` directives to your Dockerfile for each port. See [Local development with ports](https://developers.cloudflare.com/sandbox/guides/expose-services/#local-development).

**Localhost** works within sandbox:

```bash
redis-server &      # Start server
redis-cli ping      # Connect locally
```

## Security

**Between sandboxes** (isolated):

* Each sandbox is a separate container
* Filesystem, memory and network are all isolated

**Within sandbox** (shared):

* All processes see the same files
* Processes can communicate with each other
* Environment variables are session-scoped

To run untrusted code, use separate sandboxes per user:

```typescript
const sandbox = getSandbox(env.Sandbox, `user-${userId}`);
```

## Limitations

**Cannot**:

* Load kernel modules or access host hardware

## Related resources

* [Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/) \- Deploy and keep package and image aligned
* [Deploy Containers](https://developers.cloudflare.com/containers/guides/deploy/) \- Containers deploy path
* [Architecture](https://developers.cloudflare.com/sandbox/concepts/architecture/) \- How containers fit in the system
* [Security model](https://developers.cloudflare.com/sandbox/concepts/security/) \- Container isolation details
* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/concepts/sandboxes/) \- Container lifecycle management
* [Docker-in-Docker](https://developers.cloudflare.com/sandbox/guides/docker-in-docker/) \- Run Docker containers inside a Sandbox

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/concepts/containers/#page","headline":"Container runtime · Cloudflare Sandbox SDK docs","description":"Sandbox SDK containers run isolated Linux environments with Python, Node.js, and common dev tools.","url":"https://developers.cloudflare.com/sandbox/concepts/containers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-28","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Sandbox SDK preview URLs provide public HTTPS access to services running inside sandboxes.
title: Preview URLs
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Preview URLs

Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/concepts/preview-urls/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

# Quick deployment

For quick preview deployments we recommend using [Cloudflare Tunnel ↗](https://developers.cloudflare.com/tunnel/) to generate preview URLs to your web services. These work across local development, workers.dev and production usage.

```ts
await sandbox.startProcess("python -m http.server 8000");
const tunnel = await sandbox.tunnels.get(8000);
console.log(tunnel.url);
// https://acute-llama-dancing-roundly.trycloudflare.app

// Request will be routed directly to the webserver running on the sandbox.
const req = await fetch(`${tunnel.url}/api/users`); // => GET http://localhost:8000/api/users
```

Cloudflare Tunnel support currently has the following limitations:

* No control over generated URL.
* No authentication mechanism beyond randomly generated URL.
* Each URL uses an additional `cloudflared` process on the sandbox.

Production requires custom domain

We are working on production deployments, custom hostnames and authentication for Cloudflare Tunnel support. In the mean time we recommend using `exposePort()` and `proxyToSandbox()` documented below under [Production usage, stable URLs and custom domains](#).

See the [tunnels API reference](https://developers.cloudflare.com/sandbox/api/tunnels/) for the full API and feature set.

# Production usage, stable URLs & custom domains

For production use we recommend using the `exposePort()` API and routing traffic through your worker.

Custom domain for exposePort production URLs

Local development does not need a custom domain or wildcard DNS for `exposePort()` preview URLs. For production, you need a custom domain with wildcard DNS routing. Refer to [Configure preview URLs on a custom domain](https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/).

Preview URLs provide public HTTPS access to services running inside sandboxes. When you expose a port, you get a unique URL that proxies requests to your service.

```typescript
// Extract hostname from request
const { hostname } = new URL(request.url);

await sandbox.startProcess("python -m http.server 8000");
const exposed = await sandbox.exposePort(8000, { hostname });

console.log(exposed.url);
// Production: https://8000-sandbox-id-abc123random4567.yourdomain.com
// Local dev: http://8000-sandbox-id-abc123random4567.localhost:{port}/
```

## URL Format

**Production**: `https://{port}-{sandbox-id}-{token}.yourdomain.com`

* With auto-generated token: `https://8080-abc123-random16chars12.yourdomain.com`
* With custom token: `https://8080-abc123-my_api_v1.yourdomain.com`

**Local development**: `http://{port}-{sandbox-id}-{token}.localhost:{dev-server-port}`

## Token Types

### Auto-generated tokens (default)

When no custom token is specified, a random 16-character token is generated:

```typescript
const exposed = await sandbox.exposePort(8000, { hostname });
// https://8000-sandbox-id-abc123random4567.yourdomain.com
```

URLs with auto-generated tokens change when you unexpose and re-expose a port.

### Custom tokens for stable URLs

For production deployments or shared URLs, specify a custom token to maintain consistency across container restarts:

```typescript
const stable = await sandbox.exposePort(8000, {
	hostname,
	token: "api_v1",
});
// https://8000-sandbox-id-api_v1.yourdomain.com
// Same URL every time ✓
```

**Token requirements:**

* 1-16 characters long
* Lowercase letters (a-z), numbers (0-9), and underscores (\_) only
* Must be unique within each sandbox

**Use cases for custom tokens:**

* Production APIs with stable endpoints
* Sharing demo URLs with external users
* Documentation with consistent examples
* Integration testing with predictable URLs

## ID Case Sensitivity

Preview URLs extract the sandbox ID from the hostname to route requests. Since hostnames are case-insensitive (per RFC 3986), they're always lowercased: `8080-MyProject-123.yourdomain.com` becomes `8080-myproject-123.yourdomain.com`.

**The problem**: If you create a sandbox with `"MyProject-123"`, it exists as a Durable Object with that exact ID. But the preview URL routes to `"myproject-123"` (lowercased from the hostname). These are different Durable Objects, so your sandbox is unreachable via preview URL.

```typescript
// Problem scenario
const sandbox = getSandbox(env.Sandbox, "MyProject-123");
// Durable Object ID: "MyProject-123"
await sandbox.exposePort(8080, { hostname });
// Preview URL: 8080-myproject-123-token123.yourdomain.com
// Routes to: "myproject-123" (different DO - doesn't exist!)
```

**The solution**: Use `normalizeId: true` to lowercase IDs when creating sandboxes:

```typescript
const sandbox = getSandbox(env.Sandbox, "MyProject-123", {
	normalizeId: true,
});
// Durable Object ID: "myproject-123" (lowercased)
// Preview URL: 8080-myproject-123-token123.yourdomain.com
// Routes to: "myproject-123" (same DO - works!)
```

Without `normalizeId: true`, `exposePort()` throws an error when the ID contains uppercase letters.

**Best practice**: Use lowercase IDs from the start (`'my-project-123'`). See [Sandbox options - normalizeId](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/#normalizeid) for details.

## Request Routing

You must call `proxyToSandbox()` first in your Worker's fetch handler to route preview URL requests:

```typescript
import { proxyToSandbox, getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		// Handle preview URL routing first
		const proxyResponse = await proxyToSandbox(request, env);
		if (proxyResponse) return proxyResponse;

		// Your application routes
		// ...
	},
};
```

Requests flow: Browser → Your Worker → Durable Object (sandbox) → Your Service.

## Multiple Ports

Expose multiple services simultaneously:

```typescript
// Extract hostname from request
const { hostname } = new URL(request.url);

await sandbox.startProcess("node api.js"); // Port 3000
await sandbox.startProcess("node admin.js"); // Port 3001

const api = await sandbox.exposePort(3000, { hostname, name: "api" });
const admin = await sandbox.exposePort(3001, { hostname, name: "admin" });

// Each gets its own URL with unique tokens:
// https://3000-abc123-random16chars01.yourdomain.com
// https://3001-abc123-random16chars02.yourdomain.com
```

## What Works

* HTTP/HTTPS requests
* WebSocket connections
* Server-Sent Events
* All HTTP methods (GET, POST, PUT, DELETE, etc.)
* Request and response headers

## What Does Not Work

* Raw TCP/UDP connections
* Custom protocols (must wrap in HTTP)
* Ports outside range 1024-65535
* Port 3000 (used internally by the SDK)

## WebSocket Support

Preview URLs support WebSocket connections. When a WebSocket upgrade request hits an exposed port, the routing layer automatically handles the connection handshake.

```typescript
// Extract hostname from request
const { hostname } = new URL(request.url);

// Start a WebSocket server
await sandbox.startProcess("bun run ws-server.ts 8080");
const { url } = await sandbox.exposePort(8080, { hostname });

// Clients connect using WebSocket protocol
// Browser: new WebSocket('wss://8080-abc123-token123.yourdomain.com')

// Your Worker routes automatically
export default {
	async fetch(request, env) {
		const proxyResponse = await proxyToSandbox(request, env);
		if (proxyResponse) return proxyResponse;
	},
};
```

For custom routing scenarios where your Worker needs to control which sandbox or port to connect to based on request properties, see `wsConnect()` in the [Ports API](https://developers.cloudflare.com/sandbox/api/ports/#wsconnect).

## Security

Caution

Preview URLs are publicly accessible by default, but require a valid access token that is generated when you expose a port.

**Built-in security**:

* **Token-based access** \- Each exposed port gets a unique token in the URL (for example, `https://8080-sandbox-abc123token456.yourdomain.com`)
* **HTTPS in production** \- All traffic is encrypted with TLS. Certificates are provisioned automatically for first-level wildcards (`*.yourdomain.com`). If your Worker runs on a subdomain, refer to the [TLS note for custom domains](https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/#subdomain-depth-matters-for-tls).
* **Unpredictable URLs** \- Auto-generated tokens are randomly generated and difficult to guess
* **Token collision prevention** \- Custom tokens are validated to ensure uniqueness within each sandbox

**Add application-level authentication**:

For additional security, implement authentication within your application:

```python
from flask import Flask, request, abort

app = Flask(__name__)

@app.route('/data')
def get_data():
    # Check for your own authentication token
    auth_token = request.headers.get('Authorization')
    if auth_token != 'Bearer your-secret-token':
        abort(401)
    return {'data': 'protected'}
```

This adds a second layer of security on top of the URL token.

## Troubleshooting

### URL Not Accessible

Check if service is running and listening:

```typescript
// 1. Is service running?
const processes = await sandbox.listProcesses();

// 2. Is port exposed?
const ports = await sandbox.getExposedPorts();

// 3. Is service binding to 0.0.0.0 (not 127.0.0.1)?
// Good:
app.run((host = "0.0.0.0"), (port = 3000));

// Bad (localhost only):
app.run((host = "127.0.0.1"), (port = 3000));
```

### Production Errors

For custom domain issues, refer to [preview URL custom domain troubleshooting](https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/#troubleshooting).

### Local Development

Local development limitation

When using `wrangler dev`, you must expose ports in your Dockerfile:

```dockerfile
FROM docker.io/cloudflare/sandbox:0.3.3

# Required for local development
EXPOSE 3000
EXPOSE 8080
```

Without `EXPOSE`, you'll see: `connect(): Connection refused: container port not found`

This is **only required for local development**. In production, all container ports are automatically accessible.

## Related Resources

* [Configure preview URLs on a custom domain](https://developers.cloudflare.com/sandbox/guides/preview-urls-custom-domain/) \- Wildcard DNS and TLS for `exposePort()`
* [Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/) \- Worker and container image deploys
* [Expose Services](https://developers.cloudflare.com/sandbox/guides/expose-services/) \- Practical patterns for exposing ports
* [Ports API](https://developers.cloudflare.com/sandbox/api/ports/) \- Complete API reference
* [Tunnels API](https://developers.cloudflare.com/sandbox/api/tunnels/) \- Zero-config `*.trycloudflare.com` URLs as an alternative for development
* [Security Model](https://developers.cloudflare.com/sandbox/concepts/security/) \- Security best practices

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/concepts/preview-urls/#page","headline":"Preview URLs · Cloudflare Sandbox SDK docs","description":"Sandbox SDK preview URLs provide public HTTPS access to services running inside sandboxes.","url":"https://developers.cloudflare.com/sandbox/concepts/preview-urls/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Sandbox SDK sandboxes transition through running, sleeping, and destroyed states based on activity.
title: Sandbox lifecycle
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Sandbox lifecycle

Last updated Sep 1, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/concepts/sandboxes/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

A sandbox is an isolated execution environment where your code runs. Each sandbox:

* Has a unique identifier (sandbox ID)
* Contains an isolated filesystem
* Runs in a dedicated Linux container
* Maintains state while the container is active
* Exists as a Cloudflare Durable Object

Coming soon: Sandbox SDK 1.0

This page documents sandbox lifecycle on today's stable `@cloudflare/sandbox` package.

The **1.0 preview** keeps the same sandbox ID / container split and makes process and terminal handles fail closed after container stop or replace. Refer to [Sandbox lifecycle (1.0 preview)](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/) and [Errors and recovery](https://developers.cloudflare.com/sandbox/1-0-preview/errors/).

## Lifecycle states

### Creation

A sandbox is created the first time you reference its ID:

```typescript
const sandbox = getSandbox(env.Sandbox, "user-123");
await sandbox.exec('echo "Hello"'); // First request creates sandbox
```

### Active

The sandbox container is running and processing requests. All state remains available: files, running processes, shell sessions, and environment variables.

### Idle

After a period of inactivity (10 minutes by default, configurable via [sleepAfter](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/)), the container stops to free resources. When the next request arrives, a fresh container starts. All previous state is lost and the environment resets to its initial state.

**Note**: Containers with [keepAlive: true](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/#keepalive) never enter the idle state. They automatically send heartbeat pings every 30 seconds to prevent eviction.

### Destruction

Sandboxes are explicitly destroyed or automatically cleaned up:

```typescript
await sandbox.destroy();
// All files, processes, and state deleted permanently
```

## Container lifetime and state

Sandbox state exists only while the container is active. Understanding this is critical for building reliable applications.

**While the container is active** (typically minutes to hours of activity):

* Files written to `/workspace`, `/tmp`, `/home` remain available
* Background processes continue running
* Shell sessions maintain their working directory and environment
* Code interpreter contexts retain variables and imports

**When the container stops** (due to inactivity or explicit destruction):

* All files are deleted
* All processes terminate
* All shell state resets
* All code interpreter contexts are cleared

The next request creates a fresh container with a clean environment.

## Naming strategies

### Per-user sandboxes

```typescript
const sandbox = getSandbox(env.Sandbox, `user-${userId}`);
```

Use this pattern for interactive environments, playgrounds, and notebooks where each user returns to their own active workspace.

### Per-session sandboxes

```typescript
const sessionId = `session-${Date.now()}-${Math.random()}`;
const sandbox = getSandbox(env.Sandbox, sessionId);
// Later:
await sandbox.destroy();
```

Use this pattern for one-time execution, CI/CD, and tests that need a clean environment.

### Per-task sandboxes

```typescript
const sandbox = getSandbox(env.Sandbox, `build-${repoName}-${commit}`);
```

Idempotent operations with clear task-to-sandbox mapping. Good for builds, pipelines, and background jobs.

## Request routing

The first request to a sandbox determines its geographic location. Subsequent requests route to the same location.

**For global apps**:

* Option 1: Multiple sandboxes per user with region suffix (`user-123-us`, `user-123-eu`)
* Option 2: Single sandbox per user (simpler, but some users see higher latency)

## Lifecycle management

### When to destroy

```typescript
try {
	const sandbox = getSandbox(env.Sandbox, sessionId);
	await sandbox.exec("npm run build");
} finally {
	await sandbox.destroy(); // Clean up temporary sandboxes
}
```

**Destroy when**: Session ends, task completes, resources no longer needed

**Do not destroy**: Personal environments, long-running services

### Managing keepAlive containers

Containers with [keepAlive: true](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/#keepalive) require explicit management since they do not timeout automatically:

```typescript
const sandbox = getSandbox(env.Sandbox, 'persistent-task', {
  keepAlive: true
});

// Later, when done with long-running work
await sandbox.setKeepAlive(false); // Allow normal timeout behavior
// Or explicitly destroy:
await sandbox.destroy();
```

### Handling container restarts

Containers restart after inactivity or failures. Design your application to handle state loss:

```typescript
// Check if required files exist before using them
const files = await sandbox.listFiles("/workspace");
if (!files.includes("data.json")) {
	// Reinitialize: container restarted and lost previous state
	await sandbox.writeFile("/workspace/data.json", initialData);
}

await sandbox.exec("python process.py");
```

## Version compatibility

The SDK automatically checks that your npm package version matches the Docker container image version. **Version mismatches can cause features to break or behave unexpectedly.**

**What happens**:

* On sandbox startup, the SDK queries the container's version
* If versions do not match, a warning is logged
* Some features may not work correctly if versions are incompatible

**When you might see warnings**:

* You updated the npm package (`npm install @cloudflare/sandbox@latest`) but forgot to update the `FROM` line in your Dockerfile

**How to fix**: Update your Dockerfile to match your npm package version. For example, if using `@cloudflare/sandbox@0.7.0`:

```dockerfile
# Default image (JavaScript/TypeScript)
FROM docker.io/cloudflare/sandbox:0.7.0

# Or Python image if you need Python support
FROM docker.io/cloudflare/sandbox:0.7.0-python
```

See [Dockerfile reference](https://developers.cloudflare.com/sandbox/configuration/dockerfile/) for details on image variants and extending the base image.

## Best practices

* **Name consistently** \- Use clear, predictable naming schemes
* **Clean up temporary sandboxes** \- Always destroy when done
* **Reuse user workspaces** \- One long-lived sandbox per user is often sufficient
* **Batch operations** \- Combine commands: `npm install && npm test && npm build`
* **Design for ephemeral state** \- Containers restart after inactivity, losing all state

## Related resources

* [Architecture](https://developers.cloudflare.com/sandbox/concepts/architecture/) \- How sandboxes fit in the system
* [Container runtime](https://developers.cloudflare.com/sandbox/concepts/containers/) \- What runs inside sandboxes
* [Session management](https://developers.cloudflare.com/sandbox/concepts/sessions/) \- Advanced state isolation
* [Directory backups](https://developers.cloudflare.com/sandbox/concepts/backup-restore/) \- Why restored files do not survive sleep unless you restore again
* [Lifecycle API](https://developers.cloudflare.com/sandbox/api/lifecycle/) \- Create and manage sandboxes
* [Sessions API](https://developers.cloudflare.com/sandbox/api/sessions/) \- Create and manage execution sessions

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/concepts/sandboxes/#page","headline":"Sandbox lifecycle · Cloudflare Sandbox SDK docs","description":"Sandbox SDK sandboxes transition through running, sleeping, and destroyed states based on activity.","url":"https://developers.cloudflare.com/sandbox/concepts/sandboxes/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-09-01","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Sandbox SDK uses VM-level isolation, input validation, and network controls to run untrusted code safely.
title: Security model
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Security model

Last updated Aug 28, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/concepts/security/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

The Sandbox SDK is built on [Containers](https://developers.cloudflare.com/containers/), which run each sandbox in its own VM for strong isolation.

## Container isolation

Each sandbox runs in a separate VM, providing complete isolation:

* **Filesystem isolation** \- Sandboxes cannot access other sandboxes' files
* **Process isolation** \- Processes in one sandbox cannot see or affect others
* **Network isolation** \- Sandboxes have separate network stacks
* **Resource limits** \- CPU, memory, and disk quotas are enforced per sandbox

For complete security details about the underlying container platform, see [Containers architecture](https://developers.cloudflare.com/containers/concepts/architecture/).

## Within a sandbox

All code within a single sandbox shares resources:

* **Filesystem** \- All processes see the same files
* **Processes** \- All sessions can see all processes
* **Network** \- Processes can communicate via localhost

For complete isolation, use separate sandboxes per user:

```typescript
// Good - Each user in separate sandbox
const userSandbox = getSandbox(env.Sandbox, `user-${userId}`);

// Bad - Users sharing one sandbox
const shared = getSandbox(env.Sandbox, 'shared');
// Users can read each other's files!
```

## Input validation

### Command injection

Always validate user input before using it in commands:

```typescript
// Dangerous - user input directly in command
const filename = userInput;
await sandbox.exec(`cat ${filename}`);
// User could input: "file.txt; rm -rf /"

// Safe - validate input
const filename = userInput.replace(/[^a-zA-Z0-9._-]/g, '');
await sandbox.exec(`cat ${filename}`);

// Better - use file API
await sandbox.writeFile('/tmp/input', userInput);
await sandbox.exec('cat /tmp/input');
```

## Authentication

### Sandbox access

Sandbox IDs provide basic access control but aren't cryptographically secure. Add application-level authentication:

```typescript
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const userId = await authenticate(request);
    if (!userId) {
      return new Response('Unauthorized', { status: 401 });
    }

    // User can only access their sandbox
    const sandbox = getSandbox(env.Sandbox, userId);
    return Response.json({ authorized: true });
  }
};
```

### Preview URLs

Preview URLs include randomly generated tokens. Anyone with the URL can access the service.

To revoke access, unexpose the port:

```typescript
await sandbox.unexposePort(8080);
```

### Quick tunnel URLs

Quick tunnels (`sandbox.tunnels.get(port)`) return a `*.trycloudflare.com` URL with a random hostname assigned by Cloudflare — there is no separate access token. The hostname itself is the access control: anyone who knows the URL can reach the service. To revoke access, destroy the tunnel:

```typescript
await sandbox.tunnels.destroy(8080);
```

URLs do not survive a container restart, so a restart effectively rotates the hostname. As with preview URLs, add application-level authentication for any sensitive service. See the [Tunnels API](https://developers.cloudflare.com/sandbox/api/tunnels/) for details.

```python
from flask import Flask, request, abort
import os

app = Flask(__name__)

def check_auth():
    token = request.headers.get('Authorization')
    if token != f"Bearer {os.environ['AUTH_TOKEN']}":
        abort(401)

@app.route('/api/data')
def get_data():
    check_auth()
    return {'data': 'protected'}
```

## Secrets management

Use environment variables, not hardcoded secrets, for values the sandbox process must consume directly:

```typescript
// Bad - hardcoded in file
await sandbox.writeFile('/workspace/config.js', `
  const API_KEY = 'sk_live_abc123';
`);

// Good - use environment variables for values the sandbox process needs
await sandbox.startProcess('node app.js', {
  env: {
    API_KEY: env.API_KEY,  // From Worker environment binding
  }
});
```

For external API credentials that the sandbox does not need to read directly, keep the credential in the Worker and inject it with an outbound handler.

Clean up temporary sensitive data:

```typescript
try {
  await sandbox.writeFile('/tmp/sensitive.txt', secretData);
  await sandbox.exec('python process.py /tmp/sensitive.txt');
} finally {
  await sandbox.deleteFile('/tmp/sensitive.txt');
}
```

## Handle outbound traffic

Passing external API credentials directly to a sandbox — via environment variables or files — means the sandbox process holds a live credential that any code running inside it can read. Outbound handlers remove that exposure by keeping credentials in the Worker and injecting them into outbound requests.

The flow works as follows:

```txt
Sandbox request → Outbound handler (injects real credentials) → External API
```

The sandbox never sees the real credential. Rotate the secret in your Worker's environment and every request uses the updated value.

This pattern is useful when accessing GitHub for private repository operations, AI services, or object storage where you want to keep credentials out of the container entirely. For implementation details, refer to [Handle outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/).

## What the SDK protects against

* Sandbox-to-sandbox access (VM isolation)
* Resource exhaustion (enforced quotas)
* Container escapes (VM-based isolation)

## What you must implement

* Authentication and authorization
* Input validation and sanitization
* Rate limiting
* Application-level security (SQL injection, XSS, etc.)

## Best practices

**Use separate sandboxes for isolation**:

```typescript
const sandbox = getSandbox(env.Sandbox, `user-${userId}`);
```

**Validate all inputs**:

```typescript
const safe = input.replace(/[^a-zA-Z0-9._-]/g, '');
await sandbox.exec(`command ${safe}`);
```

**Use environment variables for secrets**:

```typescript
await sandbox.startProcess('node app.js', {
  env: { API_KEY: env.API_KEY }
});
```

**Clean up temporary resources**:

```typescript
try {
  const sandbox = getSandbox(env.Sandbox, sessionId);
  await sandbox.exec('npm test');
} finally {
  await sandbox.destroy();
}
```

## Related resources

* [Containers architecture](https://developers.cloudflare.com/containers/concepts/architecture/) \- Underlying platform security
* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/concepts/sandboxes/) \- Resource management

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/concepts/security/#page","headline":"Security model · Cloudflare Sandbox SDK docs","description":"Sandbox SDK uses VM-level isolation, input validation, and network controls to run untrusted code safely.","url":"https://developers.cloudflare.com/sandbox/concepts/security/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-28","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Sandbox SDK sessions are shell execution contexts within a single sandbox.
title: Session management
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Session management

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/concepts/sessions/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Sessions are bash shell execution contexts within a sandbox. Think of them as terminal tabs in the same computer.

* **Sandbox** \= A user or task workspace
* **Session** \= A shell in that workspace

Sessions are useful for organizing work inside one sandbox. They are not a security boundary between users because sessions share the same filesystem and process space.

Coming soon: Sandbox SDK 1.0

This page documents session behavior on today's stable `@cloudflare/sandbox` package.

**Sandbox SDK 1.0** (preview on `@next`) removes core session execution APIs. Pass `cwd` and `env` on each `exec`, or use one explicit shell argv script. Refer to [Process execution](https://developers.cloudflare.com/sandbox/1-0-preview/processes/) or [migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/#drop-session-apis).

## Default session

By default, every sandbox has a default session that maintains shell state between commands while the container is active:

```typescript
const sandbox = getSandbox(env.Sandbox, 'my-sandbox');

// These commands run in the default session
await sandbox.exec("cd /app");
await sandbox.exec("pwd");  // Output: /app

await sandbox.exec("export MY_VAR=hello");
await sandbox.exec("echo $MY_VAR");  // Output: hello
```

Working directory, environment variables, and exported variables carry over between commands. This state resets if the container restarts due to inactivity.

If you set `enableDefaultSession: false` when calling `getSandbox()`, operations without an explicit `sessionId` run in isolation instead of using the default session:

```typescript
const sandbox = getSandbox(env.Sandbox, 'my-sandbox', {
  enableDefaultSession: false
});

await sandbox.exec("cd /app");
await sandbox.exec("pwd");  // Output: /workspace (cd was not inherited)
```

Without the default session, the second command does not inherit shell state from the first command. It is recommended that you always apply this setting as it will become the default in a future Sandbox SDK release. Create or retrieve an explicit session when you want commands to share shell state.

### Automatic session creation

The container automatically creates sessions on first use. If you reference a non-existent session ID, the container creates it with default settings:

```typescript
// This session does not exist yet
const result = await sandbox.exec('echo hello', { sessionId: 'new-session' });
// Container automatically creates 'new-session' with defaults:
// - cwd: '/workspace'
// - env: {} (empty)
```

This behavior is particularly relevant after deleting a session:

```typescript
// Create and configure a session
const session = await sandbox.createSession({
  id: 'temp',
  env: { MY_VAR: 'value' }
});

// Delete the session
await sandbox.deleteSession('temp');

// Using the same session ID again works - auto-created with defaults
const result = await sandbox.exec('echo $MY_VAR', { sessionId: 'temp' });
// Output: (empty) - MY_VAR is not set in the freshly created session
```

This auto-creation means commands still run when they reference a non-existent session. However, custom configuration (environment variables, working directory) is lost after deletion.

## Creating sessions

Create additional sessions for separate workflows in the same sandbox:

```typescript
const buildSession = await sandbox.createSession({
  id: "build",
  env: { NODE_ENV: "production" },
  cwd: "/build"
});

const testSession = await sandbox.createSession({
  id: "test",
  env: { NODE_ENV: "test" },
  cwd: "/test"
});

// Different shell contexts
await buildSession.exec("npm run build");
await testSession.exec("npm test");
```

You can also set a default command timeout for all commands in a session:

```typescript
const session = await sandbox.createSession({
  id: "ci",
  commandTimeoutMs: 30000 // 30s timeout for all commands
});

await session.exec("npm test"); // Times out after 30s if still running
```

Individual commands can override the session timeout with the `timeout` option on `exec()`. For more details, refer to the [Sessions API](https://developers.cloudflare.com/sandbox/api/sessions/) and the [execute commands guide](https://developers.cloudflare.com/sandbox/guides/execute-commands/#timeouts).

## What is scoped to a session

Each session has its own:

**Shell environment**:

```typescript
await session1.exec("export MY_VAR=hello");
await session2.exec("echo $MY_VAR");  // Empty - different shell
```

**Working directory**:

```typescript
await session1.exec("cd /workspace/project1");
await session2.exec("pwd");  // Different working directory
```

**Environment variables** (set via `createSession` options):

```typescript
const session1 = await sandbox.createSession({
  env: { API_KEY: 'key-1' }
});
const session2 = await sandbox.createSession({
  env: { API_KEY: 'key-2' }
});
```

## What is shared across sessions

All sessions in a sandbox share:

**Filesystem**:

```typescript
await session1.writeFile('/workspace/file.txt', 'data');
await session2.readFile('/workspace/file.txt');  // Can read it
```

**Processes**:

```typescript
await session1.startProcess('node server.js');
await session2.listProcesses();  // Sees the server
```

## When to use sessions

**Use sessions when**:

* You need separate shell state for one user's tasks
* Running parallel operations with different environments
* Keeping AI agent credentials separate from app runtime

**Example - separate dev and runtime environments**:

```typescript
// Phase 1: AI agent writes code (with API keys)
const devSession = await sandbox.createSession({
  id: "dev",
  env: { ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY }
});
await devSession.exec('ai-tool "build a web server"');

// Phase 2: Run the code (without API keys)
const appSession = await sandbox.createSession({
  id: "app",
  env: { PORT: "3000" }
});
await appSession.exec("node server.js");
```

**Use separate sandboxes when**:

* You need complete isolation for untrusted code
* Different users need separate workspaces
* User data must stay separated
* Independent resource allocation is needed

## Best practices

### Session cleanup

**Clean up temporary sessions** to free resources while keeping the sandbox running:

```typescript
try {
  const session = await sandbox.createSession({ id: 'temp' });
  await session.exec('command');
} finally {
  await sandbox.deleteSession('temp');
}
```

**Default session cannot be deleted**:

```typescript
// This throws an error
await sandbox.deleteSession('default');
// Error: Cannot delete default session. Use sandbox.destroy() instead.
```

### Filesystem scope

**Sessions share the sandbox filesystem** \- file operations affect all sessions:

```typescript
// Bad - affects all sessions
await session.exec('rm -rf /workspace/*');

// For user data or untrusted code, use a separate sandbox
const userSandbox = getSandbox(env.Sandbox, `user-${userId}`);
```

## Related resources

* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/concepts/sandboxes/) \- Understanding sandbox management
* [Sessions API](https://developers.cloudflare.com/sandbox/api/sessions/) \- Complete session API reference

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/concepts/sessions/#page","headline":"Session management · Cloudflare Sandbox SDK docs","description":"Sandbox SDK sessions are shell execution contexts within a single sandbox.","url":"https://developers.cloudflare.com/sandbox/concepts/sessions/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Sandbox SDK terminal connections stream bidirectional data between browser UIs and container shells.
title: Terminal connections
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Terminal connections

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/concepts/terminal/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Terminal connections let browser-based UIs interact directly with sandbox shells. Instead of executing discrete commands with `exec()`, a terminal connection opens a persistent, bidirectional channel to a bash shell — the same model as SSH or a local terminal emulator.

Sandbox SDK 1.0 preview

This page describes terminal connections on today's stable `@cloudflare/sandbox` package.

On **`@cloudflare/sandbox@next`**, terminals use `createTerminal`, `getTerminal`, and `terminal.connect`. Refer to [Terminals](https://developers.cloudflare.com/sandbox/1-0-preview/terminals/) and [Terminals API](https://developers.cloudflare.com/sandbox/1-0-preview/api/terminals/).

## How terminal connections work

Terminal connections use WebSockets to stream raw bytes between a browser terminal (like [xterm.js ↗](https://xtermjs.org/)) and a pseudo-terminal (PTY) process running inside the sandbox container.

```txt
Browser (xterm.js) <-- WebSocket --> Worker <-- proxy --> Container PTY (bash)
```

1. The browser sends a WebSocket upgrade request to your Worker
2. Your Worker calls `sandbox.terminal(request)`, which proxies the upgrade to the container
3. The container spawns a bash shell attached to a PTY
4. Raw bytes flow bidirectionally — keystrokes in, terminal output out

This is fundamentally different from `exec()`:

* **`exec()`** runs a single command to completion and returns the result
* **`terminal()`** opens a persistent shell where users type commands interactively

## Output buffering

The container buffers terminal output in a ring buffer. When a client disconnects and reconnects, the server replays buffered output so the terminal appears unchanged. This means:

* Short network interruptions are invisible to users
* Reconnected terminals show previous output without re-running commands
* The buffer has a fixed size, so very old output may be lost

No client-side code is needed to handle buffering — the container manages it transparently.

## Automatic reconnection

Network interruptions are common in browser-based applications. Terminal connections handle this through a combination of server-side buffering (described above) and client-side reconnection with exponential backoff.

The `SandboxAddon` for xterm.js implements this automatically. If you are building a custom client, you are responsible for your own reconnection logic — the server-side buffering works regardless of which client connects. Refer to the [WebSocket protocol reference](https://developers.cloudflare.com/sandbox/api/terminal/#websocket-protocol) for details on the connection lifecycle.

## Session-specific terminals

Each [session](https://developers.cloudflare.com/sandbox/concepts/sessions/) can have its own terminal with independent shell state:

```typescript
const devSession = await sandbox.createSession({
	id: "dev",
	cwd: "/workspace/frontend",
	env: { NODE_ENV: "development" },
});

const testSession = await sandbox.createSession({
	id: "test",
	cwd: "/workspace",
	env: { NODE_ENV: "test" },
});

// Each session's terminal has its own working directory,
// environment variables, and command history
```

Multiple browser clients can connect to the same session's terminal simultaneously. They all see the same shell output and can send input. Use this pattern for intentional collaboration inside one workspace, not to separate independent users.

## WebSocket protocol

Terminal connections use binary WebSocket frames for terminal I/O (for performance) and JSON text frames for control and status messages (for structure). This keeps the data path fast while still allowing structured communication for operations like terminal resizing.

For the full protocol specification, including the connection lifecycle and message formats, refer to the [Terminal API reference](https://developers.cloudflare.com/sandbox/api/terminal/#websocket-protocol).

## When to use terminals vs commands

| Use case                                   | Approach                              |
| ------------------------------------------ | ------------------------------------- |
| Run a command and get the result           | exec() or execStream()                |
| Interactive shell for end users            | terminal()                            |
| Long-running process with real-time output | startProcess() \+ streamProcessLogs() |
| Collaborative terminal sharing             | terminal() with shared session        |

## Related resources

* [Terminal API reference](https://developers.cloudflare.com/sandbox/api/terminal/) — Method signatures and types
* [Browser terminals](https://developers.cloudflare.com/sandbox/guides/browser-terminals/) — Step-by-step setup guide
* [Session management](https://developers.cloudflare.com/sandbox/concepts/sessions/) — How sessions work
* [Architecture](https://developers.cloudflare.com/sandbox/concepts/architecture/) — Overall SDK design

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/concepts/terminal/#page","headline":"Terminal connections · Cloudflare Sandbox SDK docs","description":"Sandbox SDK terminal connections stream bidirectional data between browser UIs and container shells.","url":"https://developers.cloudflare.com/sandbox/concepts/terminal/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Deploy the sandbox bridge Worker to control Cloudflare Sandboxes over HTTP from any language or platform.
title: Sandbox bridge
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Sandbox bridge

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/bridge/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Sandbox SDK 1.0 preview

This is the supported self-deployed bridge. The 1.0 preview covers the Worker SDK on `@next`; the bridge stays on this stable template, package line, and HTTP API.

The sandbox bridge is a reference-implementation Cloudflare Worker that exposes the [Sandbox SDK](https://developers.cloudflare.com/sandbox/api/) as an HTTP API. Any HTTP client — Python script, Node.js service, CI pipeline — can create and control sandboxes without writing a Worker. You deploy the Worker in **your** account; it is not a Cloudflare-hosted shared API.

## Why use the bridge

The Sandbox SDK is designed for use within Cloudflare Workers. If your application runs outside of the Workers ecosystem, it cannot interact with sandboxes directly.

The bridge exposes the Sandbox SDK as a standard HTTP API so you can create and control sandboxes from any language or platform.

Key [Sandbox SDK methods](https://developers.cloudflare.com/sandbox/api/) map to individual HTTP endpoints. The bridge adds authentication, input validation, workspace path containment, and an optional [warm pool](https://developers.cloudflare.com/sandbox/bridge/http-api/#warm-pool) for instant container boot.

## Deploy

Deploy the bridge Worker to your Cloudflare account:

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/sandbox-sdk/tree/main/bridge/worker)

The button deploys the Worker and generates a `SANDBOX_API_KEY` secret for authentication. When deployment finishes, note your Worker URL and API key — every example on this page uses them.

Manual deployment

If you prefer to deploy step by step, scaffold the project and deploy manually.

**Prerequisites:**

* A [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages) with the Containers / Sandbox beta enabled.
* [Node.js ↗](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) and npm.
* [Docker ↗](https://www.docker.com/) running locally — `wrangler deploy` builds a container image from the bridge `Dockerfile`.

**Steps:**

1. Scaffold the bridge project:  
```sh  
npm create cloudflare -- sandbox-bridge --template=cloudflare/sandbox-sdk/bridge/worker  
cd sandbox-bridge  
```
2. Authenticate with Cloudflare:  
```sh  
npx wrangler login  
```
3. Set the API key secret. Choose any strong token value — clients must send this as a Bearer token:  
```sh  
openssl rand -hex 32 | tee /dev/stderr | npx wrangler secret put SANDBOX_API_KEY  
```  
The key is printed to your terminal and piped to Wrangler. Save it — you will need it to authenticate API requests.
4. Deploy the Worker:  
```sh  
npx wrangler deploy  
```
5. Verify the deployment:  
```sh  
curl https://cloudflare-sandbox-bridge.<your-subdomain>.workers.dev/health  
```  
You should see `{"ok":true}`.

### Container image

The bridge `Dockerfile` extends the [cloudflare/sandbox ↗](https://hub.docker.com/r/cloudflare/sandbox) base image and pre-installs common agent tooling:

* **Languages**: Python 3.13, Node.js, Bun
* **Tools**: git, ripgrep, curl, wget, jq, tar, sed, gawk, procps

Customize the `Dockerfile` to add languages, system packages, or tools your workloads need.

## Usage

All examples assume the following environment variables are set:

```sh
export SANDBOX_API_URL=https://cloudflare-sandbox-bridge.<your-subdomain>.workers.dev
export SANDBOX_API_KEY=<your-token>
```

### Create a sandbox and run a command

```sh
# Create a sandbox
SANDBOX_ID=$(curl -s -X POST "$SANDBOX_API_URL/v1/sandbox" \
  -H "Authorization: Bearer $SANDBOX_API_KEY" | jq -r '.id')

echo "Sandbox ID: $SANDBOX_ID"

# Run a command
curl -s -X POST "$SANDBOX_API_URL/v1/sandbox/$SANDBOX_ID/exec" \
  -H "Authorization: Bearer $SANDBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"argv": ["sh", "-lc", "echo hello from the sandbox"], "timeout_ms": 10000}'

# Destroy the sandbox when done
curl -s -X DELETE "$SANDBOX_API_URL/v1/sandbox/$SANDBOX_ID" \
  -H "Authorization: Bearer $SANDBOX_API_KEY"
```

```js
const API_URL = process.env.SANDBOX_API_URL;
const API_KEY = process.env.SANDBOX_API_KEY;

const headers = {
  Authorization: `Bearer ${API_KEY}`,
  "Content-Type": "application/json",
};

// Create a sandbox
const { id } = await fetch(`${API_URL}/v1/sandbox`, {
  method: "POST",
  headers,
}).then((r) => r.json());

console.log(`Sandbox ID: ${id}`);

// Run a command
// Response is a text/event-stream with the following SSE events:
//   event: stdout  — data is a base64-encoded output chunk
//   event: stderr  — data is a base64-encoded error chunk
//   event: exit    — data is JSON: {"exit_code": 0}
//   event: error   — data is JSON: {"error": "...", "code": "..."}
const execRes = await fetch(`${API_URL}/v1/sandbox/${id}/exec`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    argv: ["sh", "-lc", "echo hello from the sandbox"],
    timeout_ms: 10000,
  }),
});

console.log(await execRes.text());

// Destroy the sandbox when done
await fetch(`${API_URL}/v1/sandbox/${id}`, {
  method: "DELETE",
  headers,
});
```

```python
# /// script
# dependencies = ["httpx"]
# ///
import os
import httpx

API_URL = os.environ["SANDBOX_API_URL"]
API_KEY = os.environ["SANDBOX_API_KEY"]

headers = {"Authorization": f"Bearer {API_KEY}"}

# Create a sandbox
resp = httpx.post(f"{API_URL}/v1/sandbox", headers=headers)
sandbox_id = resp.json()["id"]
print(f"Sandbox ID: {sandbox_id}")

# Run a command
# Response is a text/event-stream with the following SSE events:
#   event: stdout  — data is a base64-encoded output chunk
#   event: stderr  — data is a base64-encoded error chunk
#   event: exit    — data is JSON: {"exit_code": 0}
#   event: error   — data is JSON: {"error": "...", "code": "..."}
exec_resp = httpx.post(
    f"{API_URL}/v1/sandbox/{sandbox_id}/exec",
    headers=headers,
    json={
        "argv": ["sh", "-lc", "echo hello from the sandbox"],
        "timeout_ms": 10000,
    },
)
print(exec_resp.text)

# Destroy the sandbox when done
httpx.delete(f"{API_URL}/v1/sandbox/{sandbox_id}", headers=headers)
```

### Write and read files

```sh
# Write a file
curl -s -X PUT "$SANDBOX_API_URL/v1/sandbox/$SANDBOX_ID/file/workspace/hello.py" \
  -H "Authorization: Bearer $SANDBOX_API_KEY" \
  --data-binary 'print("hello world")'

# Read a file
curl -s "$SANDBOX_API_URL/v1/sandbox/$SANDBOX_ID/file/workspace/hello.py" \
  -H "Authorization: Bearer $SANDBOX_API_KEY"
```

```js
// Write a file
await fetch(`${API_URL}/v1/sandbox/${id}/file/workspace/hello.py`, {
  method: "PUT",
  headers,
  body: 'print("hello world")',
});

// Read a file
const content = await fetch(
  `${API_URL}/v1/sandbox/${id}/file/workspace/hello.py`,
  { headers },
).then((r) => r.text());

console.log(content);
```

```python
# /// script
# dependencies = ["httpx"]
# ///
import os
import httpx

API_URL = os.environ["SANDBOX_API_URL"]
API_KEY = os.environ["SANDBOX_API_KEY"]
SANDBOX_ID = os.environ["SANDBOX_ID"]  # from the "Create a sandbox" step
headers = {"Authorization": f"Bearer {API_KEY}"}

# Write a file
httpx.put(
    f"{API_URL}/v1/sandbox/{SANDBOX_ID}/file/workspace/hello.py",
    headers=headers,
    content=b'print("hello world")',
)

# Read a file
content = httpx.get(
    f"{API_URL}/v1/sandbox/{SANDBOX_ID}/file/workspace/hello.py",
    headers=headers,
).text
print(content)
```

## Keep the bridge updated

The bulk of the bridge logic is in the `@cloudflare/sandbox` package. To pull in the latest improvements:

1. Update the SDK dependency:  
```sh  
npm update @cloudflare/sandbox  
```
2. Redeploy:  
```sh  
npx wrangler deploy  
```

Check the [sandbox-sdk releases ↗](https://github.com/cloudflare/sandbox-sdk/releases) for changes to the `Dockerfile` or bridge configuration that may require manual updates.

## Source code and examples

The bridge source code and examples are available on GitHub:

* [Bridge source ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/bridge) — Worker, Dockerfile, deploy script, and OpenAPI schema.
* [Workspace chat example ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/bridge/examples/workspace-chat) — Full-stack chat application with a file browser sidebar.
* [Basic example ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/bridge/examples/basic) — One-shot Python coding agent using the OpenAI Agents SDK.

## Related resources

* [HTTP API reference](https://developers.cloudflare.com/sandbox/bridge/http-api/) — Complete route reference for the bridge API.
* [Getting started](https://developers.cloudflare.com/sandbox/get-started/) — Build your first sandbox application directly on Workers.
* [Architecture](https://developers.cloudflare.com/sandbox/concepts/architecture/) — How the Sandbox SDK layers Workers, Durable Objects, and Containers.
* [API reference](https://developers.cloudflare.com/sandbox/api/) — Complete Sandbox SDK method reference.
* [OpenAI Agents SDK tutorial](https://developers.cloudflare.com/sandbox/tutorials/openai-agents/) — Build a Python coding agent with the bridge.

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/bridge/#page","headline":"Sandbox bridge · Cloudflare Sandbox SDK docs","description":"Deploy the sandbox bridge Worker to control Cloudflare Sandboxes over HTTP from any language or platform.","url":"https://developers.cloudflare.com/sandbox/bridge/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Python","Node.js","Docker"]}
```

---

---
description: Complete HTTP API reference for the sandbox bridge Worker.
title: HTTP API reference
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# HTTP API reference

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/bridge/http-api/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Sandbox SDK 1.0 preview

This is the supported bridge HTTP API. The 1.0 preview covers the Worker SDK on `@next`; the bridge stays on this stable template and route surface.

This page documents every route exposed by the [sandbox bridge](https://developers.cloudflare.com/sandbox/bridge/) on the stable template.

## Authentication

All routes under `/v1/sandbox/*` and `/v1/openapi.*` require a Bearer token:

```txt
Authorization: Bearer <SANDBOX_API_KEY>
```

When `SANDBOX_API_KEY` is not configured, authentication is skipped for local development convenience. Always set the secret before deploying to production.

## OpenAPI schema

The bridge serves its own API documentation:

| Method | Route            | Description                          |
| ------ | ---------------- | ------------------------------------ |
| GET    | /v1/openapi.json | Machine-readable OpenAPI 3.1 schema. |
| GET    | /v1/openapi      | Interactive HTML documentation.      |

Both routes accept authentication via Bearer header or `?token=` query parameter.

When running locally with `npm run dev`, open `http://localhost:8787/v1/openapi` in your browser to explore every endpoint interactively.

## Sandbox lifecycle

| Method | Route                   | Description                                                 |
| ------ | ----------------------- | ----------------------------------------------------------- |
| POST   | /v1/sandbox             | Create a new sandbox. Returns {"id": "<sandbox-id>"}.       |
| DELETE | /v1/sandbox/:id         | Destroy the sandbox container. Returns 204.                 |
| GET    | /v1/sandbox/:id/running | Check container liveness. Returns {"running": true\|false}. |

## Command execution

| Method | Route                | Description                                           |
| ------ | -------------------- | ----------------------------------------------------- |
| POST   | /v1/sandbox/:id/exec | Run a command. Response is an SSE stream (see below). |

The `/exec` endpoint accepts a JSON body:

```json
{
  "argv": ["sh", "-lc", "echo hello"],
  "timeout_ms": 10000,
  "cwd": "/workspace"
}
```

### Argv escaping

Each element of the `argv` array is escaped using ANSI-C `$'...'` quoting before being joined into a shell command. Tokens that contain only safe characters (`A-Za-z0-9@%+=:,./-`) are passed through unchanged. All other tokens are wrapped in `$'...'` with backslashes, single quotes, newlines, carriage returns, and tabs escaped. This prevents shell injection while preserving arguments that contain spaces, quotes, or special characters.

### SSE response format

The response is a `text/event-stream` with the following event types:

| Event  | Data                        | Description                        |
| ------ | --------------------------- | ---------------------------------- |
| stdout | Base64-encoded chunk        | Standard output from the command.  |
| stderr | Base64-encoded chunk        | Standard error from the command.   |
| exit   | {"exit\_code": N}           | Command completed. Terminal event. |
| error  | {"error": "…", "code": "…"} | Command failed. Terminal event.    |

## File operations

| Method | Route                   | Description                                                                |
| ------ | ----------------------- | -------------------------------------------------------------------------- |
| GET    | /v1/sandbox/:id/file/\* | Read a file. Returns raw bytes (application/octet-stream).                 |
| PUT    | /v1/sandbox/:id/file/\* | Write a file. Request body is raw bytes. Returns {"ok": true}. Max 32 MiB. |

The file path is encoded in the URL after `/file/`. All paths must resolve within `/workspace`. Path traversal attempts (for example, `../../etc/passwd`) are rejected.

## Workspace persistence

| Method | Route                   | Description                                                      |
| ------ | ----------------------- | ---------------------------------------------------------------- |
| POST   | /v1/sandbox/:id/persist | Serialize /workspace to a tar archive. Returns raw tar bytes.    |
| POST   | /v1/sandbox/:id/hydrate | Populate /workspace from a tar archive sent as the request body. |

The `/persist` endpoint accepts an optional `excludes` query parameter — a comma-separated list of relative paths to exclude from the archive.

The `/hydrate` endpoint accepts a raw tar payload up to 32 MiB.

## Bucket mounts

| Method | Route                   | Description                                         |
| ------ | ----------------------- | --------------------------------------------------- |
| POST   | /v1/sandbox/:id/mount   | Mount an S3-compatible bucket as a local directory. |
| POST   | /v1/sandbox/:id/unmount | Unmount a previously mounted bucket.                |

The `/mount` endpoint accepts a JSON body. Two flows are supported:

### R2 binding mounts

Omit `endpoint` and pass the Worker R2 binding name in `bucket`:

```json
{
  "bucket": "MY_BUCKET",
  "mountPath": "/mnt/data",
  "options": {
    "readOnly": false,
    "prefix": "/subdir"
  }
}
```

When `options.endpoint` is omitted, `bucket` means the Worker R2 binding name.

For an explicit S3-compatible endpoint mount, include `endpoint` and optionally `credentials`:

```json
{
  "bucket": "my-r2-bucket",
  "mountPath": "/mnt/data",
  "options": {
    "endpoint": "https://ACCOUNT_ID.r2.cloudflarestorage.com",
    "readOnly": false,
    "prefix": "/subdir",
    "credentials": {
      "accessKeyId": "...",
      "secretAccessKey": "..."
    }
  }
}
```

When `endpoint` is provided, `bucket` means the remote bucket name. Credentials are optional in this mode only — the bridge auto-detects from Worker secrets (`R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` or `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`) when omitted.

## Sessions

| Method | Route                        | Description                                       |
| ------ | ---------------------------- | ------------------------------------------------- |
| POST   | /v1/sandbox/:id/session      | Create a session. Returns {"id": "<session-id>"}. |
| DELETE | /v1/sandbox/:id/session/:sid | Delete a session. Returns 204.                    |

Sessions isolate working directory, environment variables, and command execution state within a sandbox. Pass the `Session-Id` header on `/exec`, `/file/*`, and `/pty` requests to scope them to a session.

When no `Session-Id` header is provided, requests use the sandbox's implicit execution mode. By default this is the default session, but SDKs configured with `enableDefaultSession: false` run those implicit operations sessionless instead.

## Terminal (PTY)

| Method | Route               | Description                         |
| ------ | ------------------- | ----------------------------------- |
| GET    | /v1/sandbox/:id/pty | Upgrade to a WebSocket PTY session. |

Query parameters:

| Parameter | Type   | Default | Description                            |
| --------- | ------ | ------- | -------------------------------------- |
| cols      | number | 80      | Terminal width in columns.             |
| rows      | number | 24      | Terminal height in rows.               |
| shell     | string | —       | Shell binary (for example, /bin/bash). |
| session   | string | —       | Session ID for session-scoped PTY.     |

The WebSocket carries binary frames for terminal I/O and JSON text frames for control messages:

| Direction        | Frame type  | Content                                                                      |
| ---------------- | ----------- | ---------------------------------------------------------------------------- |
| Client to server | Binary      | UTF-8 encoded keystrokes.                                                    |
| Server to client | Binary      | Terminal output including ANSI escape sequences.                             |
| Client to server | Text (JSON) | Control messages (for example, {"type": "resize", "cols": 120, "rows": 30}). |
| Server to client | Text (JSON) | Status messages (ready, exit, error).                                        |

## Warm pool

| Method | Route                       | Description                     |
| ------ | --------------------------- | ------------------------------- |
| GET    | /v1/pool/stats              | Current pool statistics.        |
| POST   | /v1/pool/prime              | Start the warm pool alarm loop. |
| POST   | /v1/pool/shutdown-prewarmed | Stop all idle warm containers.  |

The warm pool pre-starts sandbox containers so new sessions boot instantly. Configure it with environment variables in `wrangler.jsonc`:

```jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "vars": {
    "WARM_POOL_TARGET": "3",
    "WARM_POOL_REFRESH_INTERVAL": "10000"
  }
}
```

```toml
[vars]
WARM_POOL_TARGET = "3"           # Number of idle containers to keep warm (0 = disabled)
WARM_POOL_REFRESH_INTERVAL = "10000"  # Health-check interval in milliseconds
```

A cron trigger (`* * * * *`) primes the pool automatically after deployment. Set `WARM_POOL_TARGET` to `"0"` (the default) to disable the pool and avoid unexpected costs.

## Health check

| Method | Route   | Description                                           |
| ------ | ------- | ----------------------------------------------------- |
| GET    | /health | Unauthenticated liveness probe. Returns {"ok": true}. |

## Related resources

* [Bridge overview](https://developers.cloudflare.com/sandbox/bridge/) — What the bridge is, deployment, and usage examples.
* [Sandbox API reference](https://developers.cloudflare.com/sandbox/api/) — Complete Sandbox SDK method reference.
* [Bridge source on GitHub ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/bridge) — Worker, Dockerfile, and OpenAPI schema.

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/bridge/http-api/#page","headline":"HTTP API reference · Cloudflare Sandbox SDK docs","description":"Complete HTTP API reference for the sandbox bridge Worker.","url":"https://developers.cloudflare.com/sandbox/bridge/http-api/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Configure Sandbox SDK deployments with Wrangler, Dockerfiles, environment variables, and transport modes.
title: Configuration
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Configuration

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/configuration/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Configure your Sandbox SDK deployment with Wrangler, customize container images, and manage environment variables.

### [Wrangler configuration](https://developers.cloudflare.com/sandbox/configuration/wrangler/)

Configure Durable Objects bindings, container images, and Worker settings in wrangler.jsonc.

### [Dockerfile reference](https://developers.cloudflare.com/sandbox/configuration/dockerfile/)

Customize the sandbox container image with your own packages, tools, and configurations.

### [Environment variables](https://developers.cloudflare.com/sandbox/configuration/environment-variables/)

Pass configuration and secrets to your sandboxes using environment variables.

### [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/)

Configure HTTP or RPC transport to optimize communication and avoid subrequest limits.

### [Sandbox options](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/)

Configure sandbox behavior with options like `keepAlive` for long-running processes.

## Related resources

* [Get Started guide](https://developers.cloudflare.com/sandbox/get-started/) \- Initial setup walkthrough
* [Wrangler documentation](https://developers.cloudflare.com/workers/wrangler/) \- Complete Wrangler reference
* [Docker documentation ↗](https://docs.docker.com/engine/reference/builder/) \- Dockerfile syntax
* [Security model](https://developers.cloudflare.com/sandbox/concepts/security/) \- Understanding environment isolation

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/sandbox/configuration/#page","headline":"Configuration · Cloudflare Sandbox SDK docs","description":"Configure Sandbox SDK deployments with Wrangler, Dockerfiles, environment variables, and transport modes.","url":"https://developers.cloudflare.com/sandbox/configuration/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Customize the Sandbox SDK container image with packages, tools, and configurations.
title: Dockerfile reference
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Dockerfile reference

Last updated Aug 28, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/configuration/dockerfile/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Coming soon: Sandbox SDK 1.0

Image variant names (`-python`, `-opencode`, `-musl`) still apply on **`@next`**. Pin the container tag to the same preview line as `@cloudflare/sandbox@next` (for example `cloudflare/sandbox:next` or a matching prerelease). Do not mix a preview Worker package with a stable image tag.

Customize the sandbox container image with your own packages, tools, and configurations by extending the base runtime image.

## Base images

The Sandbox SDK provides multiple Ubuntu-based image variants. Choose the one that fits your use case:

| Image    | Tag suffix | Use case                                       |
| -------- | ---------- | ---------------------------------------------- |
| Default  | (none)     | Lean image for JavaScript/TypeScript workloads |
| Python   | \-python   | Data science, ML, Python code execution        |
| OpenCode | \-opencode | AI coding agents with OpenCode CLI             |

```dockerfile
# Default - lean, no Python
FROM docker.io/cloudflare/sandbox:0.7.0

# Python - includes Python 3.11 + data science packages
FROM docker.io/cloudflare/sandbox:0.7.0-python

# OpenCode - includes OpenCode CLI for AI coding
FROM docker.io/cloudflare/sandbox:0.7.0-opencode
```

Version synchronization required

Always match the Docker image version to your npm package version. If you're using `@cloudflare/sandbox@0.7.0`, use `docker.io/cloudflare/sandbox:0.7.0` (or variant) as your base image.

**Why this matters**: The SDK automatically checks version compatibility on startup. Mismatched versions can cause features to break or behave unexpectedly. If versions don't match, you'll see warnings in your logs.

See [Version compatibility](https://developers.cloudflare.com/sandbox/concepts/sandboxes/#version-compatibility) for troubleshooting version mismatch warnings.

### Default image

The default image is optimized for JavaScript and TypeScript workloads:

* Ubuntu 22.04 LTS base
* Node.js 20 LTS with npm
* Bun 1.x (JavaScript/TypeScript runtime)
* System utilities: curl, wget, git, jq, zip, unzip, file, procps, ca-certificates

### Python image

The `-python` variant includes everything in the default image plus:

* Python 3.11 with pip and venv
* Pre-installed packages: matplotlib, numpy, pandas, ipython

### OpenCode image

The `-opencode` variant includes everything in the default image plus:

* [OpenCode CLI ↗](https://opencode.ai) for AI-powered coding agents

## Creating a custom image

Create a `Dockerfile` in your project root:

```dockerfile
FROM docker.io/cloudflare/sandbox:0.7.0-python

# Install additional Python packages
RUN pip install --no-cache-dir \
    scikit-learn==1.3.0 \
    tensorflow==2.13.0 \
    transformers==4.30.0

# Install Node.js packages globally
RUN npm install -g typescript ts-node prettier

# Install system packages
RUN apt-get update && apt-get install -y \
    postgresql-client \
    redis-tools \
    && rm -rf /var/lib/apt/lists/*
```

Update `wrangler.jsonc` to reference your Dockerfile:

```jsonc
{
	"containers": [
		{
			"class_name": "Sandbox",
			"image": "./Dockerfile",
		},
	],
}
```

When you run `wrangler dev` or `wrangler deploy`, Wrangler automatically builds your Docker image and pushes it to Cloudflare's container registry. You don't need to manually build or publish images.

## Using arbitrary base images

You can add sandbox capabilities to any Docker image using the standalone binary. This approach lets you use your existing images without depending on the Cloudflare base images:

```dockerfile
FROM your-custom-image:tag

# Copy the sandbox binary from the official image
COPY --from=docker.io/cloudflare/sandbox:0.7.0 /container-server/sandbox /sandbox

ENTRYPOINT ["/sandbox"]
```

The `/sandbox` binary starts the HTTP API server that enables SDK communication. You can optionally run your own startup command:

```dockerfile
FROM node:20-slim

COPY --from=docker.io/cloudflare/sandbox:0.7.0 /container-server/sandbox /sandbox

# Copy your application
COPY . /app
WORKDIR /app

ENTRYPOINT ["/sandbox"]
CMD ["node", "server.js"]
```

When using `CMD`, the sandbox binary runs your command as a child process with proper signal forwarding.

## Custom startup scripts

For more complex startup sequences, create a custom startup script:

```dockerfile
FROM docker.io/cloudflare/sandbox:0.7.0-python

COPY my-app.js /workspace/my-app.js
COPY startup.sh /workspace/startup.sh
RUN chmod +x /workspace/startup.sh

CMD ["/workspace/startup.sh"]
```

The base image already sets the correct `ENTRYPOINT`, so you only need to provide a `CMD`. The sandbox binary starts the HTTP API server, then spawns your `CMD` as a child process with proper signal forwarding.

```bash
#!/bin/bash

# Start your services in the background
node /workspace/my-app.js &

# Start additional services
redis-server --daemonize yes
until redis-cli ping; do sleep 1; done

# Keep the script running (the sandbox binary handles the API server)
wait
```

Legacy startup scripts

If you have existing startup scripts that end with `exec bun /container-server/dist/index.js`, they will continue to work for backwards compatibility. However, we recommend migrating to the new approach using `CMD` for your startup script. Do not override `ENTRYPOINT` when extending the base image.

## Related resources

* [Image Management](https://developers.cloudflare.com/containers/guides/image-management/) \- Building and pushing images to Cloudflare's registry
* [Wrangler configuration](https://developers.cloudflare.com/sandbox/configuration/wrangler/) \- Using custom images in wrangler.jsonc
* [Docker documentation ↗](https://docs.docker.com/reference/dockerfile/) \- Complete Dockerfile syntax
* [Container concepts](https://developers.cloudflare.com/sandbox/concepts/containers/) \- Understanding the runtime environment

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/configuration/dockerfile/#page","headline":"Dockerfile reference · Cloudflare Sandbox SDK docs","description":"Customize the Sandbox SDK container image with packages, tools, and configurations.","url":"https://developers.cloudflare.com/sandbox/configuration/dockerfile/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-28","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Pass configuration, secrets, and runtime settings to Sandbox SDK containers using environment variables.
title: Environment variables
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Environment variables

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/configuration/environment-variables/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Coming soon: Sandbox SDK 1.0

This page documents environment configuration on today's stable `@cloudflare/sandbox` package.

For `@cloudflare/sandbox@next`, refer to [Environment variables](https://developers.cloudflare.com/sandbox/1-0-preview/environment/) in the 1.0 preview section.

Pass configuration, secrets, and runtime settings to your sandboxes using environment variables.

## SDK configuration variables

These environment variables configure how the Sandbox SDK behaves. Set these as Worker `vars` in your `wrangler.jsonc` file. The SDK reads them from the Worker's environment bindings.

### SANDBOX\_TRANSPORT

| **Type**    | "http" \| "websocket" | "rpc" |
| ----------- | --------------------- | ----- |
| **Default** | "http"                |       |

Controls the transport protocol for SDK-to-container communication. RPC transport multiplexes all operations over a single persistent connection, avoiding [subrequest limits](https://developers.cloudflare.com/workers/platform/limits/#subrequests) when performing many SDK operations per request.

```jsonc
{
	"vars": {
		"SANDBOX_TRANSPORT": "rpc"
	}
}
```

```toml
[vars]
SANDBOX_TRANSPORT = "rpc"
```

For a complete guide including valid transport modes, performance considerations, and migration instructions, refer to [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/).

### COMMAND\_TIMEOUT\_MS

| **Type**    | number (milliseconds) |
| ----------- | --------------------- |
| **Default** | None (no timeout)     |

Sets a global default timeout for every `exec()` call. When set, any command that exceeds this duration raises an error on the caller side and closes the connection.

Per-command `timeout` on `exec()` and session-level `commandTimeoutMs` on [createSession()](https://developers.cloudflare.com/sandbox/api/sessions/#createsession) both override this value. For more details on timeout precedence, refer to [Execute commands - Timeouts](https://developers.cloudflare.com/sandbox/guides/execute-commands/#timeouts).

```jsonc
{
	"vars": {
		"COMMAND_TIMEOUT_MS": "30000"
	}
}
```

```toml
[vars]
COMMAND_TIMEOUT_MS = "30000"
```

Note

A timeout does not kill the underlying process. It only terminates the connection to the caller. The process continues running until the session is deleted or the sandbox is destroyed.

## Three ways to set environment variables

The Sandbox SDK provides three methods for setting environment variables, each suited for different use cases:

### 1\. Sandbox-level with setEnvVars()

Set environment variables globally for all commands in the sandbox:

```typescript
const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Set once, available for all subsequent commands
await sandbox.setEnvVars({
	DATABASE_URL: env.DATABASE_URL,
	API_KEY: env.API_KEY,
});

await sandbox.exec("python migrate.py"); // Has DATABASE_URL and API_KEY
await sandbox.exec("python seed.py"); // Has DATABASE_URL and API_KEY

// Unset variables by passing undefined
await sandbox.setEnvVars({
	API_KEY: "new-key", // Updates API_KEY
	OLD_SECRET: undefined, // Unsets OLD_SECRET
});
```

**Use when:** You need the same environment variables for multiple commands.

**Unsetting variables**: Pass `undefined` or `null` to unset environment variables:

```typescript
await sandbox.setEnvVars({
	API_KEY: 'new-key',     // Sets API_KEY
	OLD_SECRET: undefined,  // Unsets OLD_SECRET
	DEBUG_MODE: null        // Unsets DEBUG_MODE
});
```

### 2\. Per-command with exec() options

Pass environment variables for a specific command:

```typescript
await sandbox.exec("node app.js", {
	env: {
		NODE_ENV: "production",
		PORT: "3000",
	},
});

// Also works with startProcess()
await sandbox.startProcess("python server.py", {
	env: {
		DATABASE_URL: env.DATABASE_URL,
	},
});
```

**Use when:** You need different environment variables for different commands, or want to override sandbox-level variables.

Note

Per-command environment variables with `undefined` values are skipped (treated as "not configured"), unlike `setEnvVars()` where `undefined` explicitly unsets a variable.

### 3\. Session-level with createSession()

Create an isolated session with its own environment variables:

```typescript
const session = await sandbox.createSession({
	env: {
		DATABASE_URL: env.DATABASE_URL,
		SECRET_KEY: env.SECRET_KEY,
	},
});

// All commands in this session have these vars
await session.exec("python migrate.py");
await session.exec("python seed.py");
```

**Use when:** You need isolated execution contexts with different environment variables running concurrently.

## Unsetting environment variables

The Sandbox SDK supports unsetting environment variables by passing `undefined` or `null` values. This enables idiomatic JavaScript patterns for managing configuration:

```typescript
await sandbox.setEnvVars({
	// Set new values
	API_KEY: 'new-key',
	DATABASE_URL: env.DATABASE_URL,

	// Unset variables (removes them from the environment)
	OLD_API_KEY: undefined,
	TEMP_TOKEN: null
});
```

**Before this change**: Passing `undefined` values would throw a runtime error.

**After this change**: `undefined` and `null` values run `unset VARIABLE_NAME` in the shell.

### Use cases for unsetting

**Remove sensitive data after use:**

```typescript
// Use a temporary token
await sandbox.setEnvVars({ TEMP_TOKEN: 'abc123' });
await sandbox.exec('curl -H "Authorization: $TEMP_TOKEN" api.example.com');

// Clean up the token
await sandbox.setEnvVars({ TEMP_TOKEN: undefined });
```

**Conditional environment setup:**

```typescript
await sandbox.setEnvVars({
	API_KEY: env.API_KEY,
	DEBUG_MODE: env.NODE_ENV === 'development' ? 'true' : undefined,
	PROFILING: env.ENABLE_PROFILING ? 'true' : undefined
});
```

**Reset to system defaults:**

```typescript
// Unset to fall back to container's default NODE_ENV
await sandbox.setEnvVars({ NODE_ENV: undefined });
```

## Common patterns

### Pass Worker secrets to sandbox

Securely pass secrets from your Worker to the sandbox. First, set secrets using Wrangler:

```bash
wrangler secret put OPENAI_API_KEY
wrangler secret put DATABASE_URL
```

Then pass them to your sandbox:

```typescript
import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";

interface Env {
	Sandbox: DurableObjectNamespace<Sandbox>;
	OPENAI_API_KEY: string;
	DATABASE_URL: string;
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const sandbox = getSandbox(env.Sandbox, "user-sandbox");

		// Option 1: Set globally for all commands
		await sandbox.setEnvVars({
			OPENAI_API_KEY: env.OPENAI_API_KEY,
			DATABASE_URL: env.DATABASE_URL,
		});
		await sandbox.exec("python analyze.py");

		// Option 2: Pass per-command
		await sandbox.exec("python analyze.py", {
			env: {
				OPENAI_API_KEY: env.OPENAI_API_KEY,
			},
		});

		return Response.json({ success: true });
	},
};
```

### Combine default and specific variables

```typescript
const defaults = { NODE_ENV: "production", LOG_LEVEL: "info" };

await sandbox.exec("npm start", {
	env: { ...defaults, PORT: "3000", API_KEY: env.API_KEY },
});
```

### Multiple isolated sessions

Run different tasks with different environment variables concurrently:

```typescript
// Production database session
const prodSession = await sandbox.createSession({
	env: { DATABASE_URL: env.PROD_DATABASE_URL },
});

// Staging database session
const stagingSession = await sandbox.createSession({
	env: { DATABASE_URL: env.STAGING_DATABASE_URL },
});

// Run migrations on both concurrently
await Promise.all([
	prodSession.exec("python migrate.py"),
	stagingSession.exec("python migrate.py"),
]);
```

### Configure transport mode

Set `SANDBOX_TRANSPORT` in your Worker's `vars` to switch between HTTP, WebSocket, and RPC transport. For details on when and how to configure each transport, refer to [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/).

### Bucket mounting credentials

When mounting S3-compatible object storage, the SDK uses **s3fs-fuse** under the hood, which requires AWS-style credentials. For R2, generate API tokens from the Cloudflare dashboard and provide them using AWS environment variable names:

**Get R2 API tokens:**

1. Go to [**R2** \> **Overview** ↗](https://dash.cloudflare.com/?to=/:account/r2) in the Cloudflare dashboard
2. Select **Manage R2 API Tokens**
3. Create a token with **Object Read & Write** permissions
4. Copy the **Access Key ID** and **Secret Access Key**

**Set credentials as Worker secrets:**

```bash
wrangler secret put AWS_ACCESS_KEY_ID
# Paste your R2 Access Key ID

wrangler secret put AWS_SECRET_ACCESS_KEY
# Paste your R2 Secret Access Key
```

**Mount buckets with automatic credential detection:**

```typescript
import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";

interface Env {
	Sandbox: DurableObjectNamespace<Sandbox>;
	AWS_ACCESS_KEY_ID: string;
	AWS_SECRET_ACCESS_KEY: string;
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const sandbox = getSandbox(env.Sandbox, "data-processor");

		// Credentials automatically detected from environment
		await sandbox.mountBucket("my-r2-bucket", "/data", {
			endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
		});

		// Access mounted bucket using standard file operations
		await sandbox.exec("python", { args: ["process.py", "/data/input.csv"] });

		return Response.json({ success: true });
	},
};
```

The SDK automatically detects `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` from your Worker's environment when you call `mountBucket()` without explicit credentials.

**Pass credentials explicitly** (if using custom secret names):

```typescript
await sandbox.mountBucket("my-r2-bucket", "/data", {
	endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
	credentials: {
		accessKeyId: env.R2_ACCESS_KEY_ID,
		secretAccessKey: env.R2_SECRET_ACCESS_KEY,
	},
});
```

AWS nomenclature for R2

The SDK uses AWS-style credential names (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) because bucket mounting is powered by **s3fs-fuse**, which expects S3-compatible credentials. R2's API tokens work with this format since R2 implements the S3 API.

See [Mount buckets guide](https://developers.cloudflare.com/sandbox/guides/mount-buckets/) for complete bucket mounting documentation.

## Environment variable precedence

When the same variable is set at multiple levels, the most specific level takes precedence:

1. **Command-level** (highest) - Passed to `exec()` or `startProcess()` options
2. **Sandbox or session-level** \- Set with `setEnvVars()`
3. **Container default** \- Built into the Docker image with `ENV`
4. **System default** (lowest) - Operating system defaults

Example:

```typescript
// In Dockerfile: ENV NODE_ENV=development

// Sandbox-level
await sandbox.setEnvVars({ NODE_ENV: "staging" });

// Command-level overrides all
await sandbox.exec("node app.js", {
	env: { NODE_ENV: "production" }, // This wins
});
```

## Related resources

* [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/) \- Configure HTTP, WebSocket, and RPC transport
* [Wrangler configuration](https://developers.cloudflare.com/sandbox/configuration/wrangler/) \- Setting Worker-level environment
* [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/) \- Managing sensitive data
* [Sessions API](https://developers.cloudflare.com/sandbox/api/sessions/) \- Session-level environment variables
* [Security model](https://developers.cloudflare.com/sandbox/concepts/security/) \- Understanding data isolation
* [Handle outbound traffic](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/) \- Keep credentials out of the sandbox entirely using outbound handlers

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/configuration/environment-variables/#page","headline":"Environment variables · Cloudflare Sandbox SDK docs","description":"Pass configuration, secrets, and runtime settings to Sandbox SDK containers using environment variables.","url":"https://developers.cloudflare.com/sandbox/configuration/environment-variables/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Configure Sandbox SDK behavior with sleep timeouts, resource limits, and container settings.
title: Sandbox options
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Sandbox options

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Configure sandbox behavior by passing options when creating a sandbox instance with `getSandbox()`.

Coming soon: Sandbox SDK 1.0

This page documents options on today's stable `@cloudflare/sandbox` package.

In the **1.0 preview** (`@next`), `enableDefaultSession` and transport selection are removed. `sleepAfter`, `keepAlive`, `containerTimeouts`, and `normalizeId` still apply. Refer to [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/1-0-preview/lifecycle/) and [Migrate](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/).

## Available options

```ts
import { getSandbox } from '@cloudflare/sandbox';

const sandbox = getSandbox(binding, sandboxId, options?: SandboxOptions);
```

### enableDefaultSession

**Type**: `boolean` **Default**: `true`

Controls what happens when you call sandbox methods without an explicit `sessionId`. When `true`, implicit operations use the sandbox's default session and preserve shell state between calls. When `false`, implicit operations run in isolation and do not inherit shell state from prior calls unless you explicitly target a session.

Use `enableDefaultSession: true` for interactive or stateful workflows where commands should share working directory and exported variables. Use `enableDefaultSession: false` for stateless request handling where one call should not affect the next one. It is recommended to set this to `false` — default session support will be removed in a future version of the Sandbox SDK, and using `createSession()` explicitly is the preferred pattern going forward.

```js
// Default behavior: implicit operations use the default session
const statefulSandbox = getSandbox(env.Sandbox, "user-123");

await statefulSandbox.exec("cd /workspace/app");
const statefulResult = await statefulSandbox.exec("pwd");
// statefulResult.stdout: "/workspace/app"
// The second exec inherited the working directory from the first.

// Sessionless behavior: implicit operations do not share shell state
const statelessSandbox = getSandbox(env.Sandbox, "api-worker", {
	enableDefaultSession: false,
});

await statelessSandbox.exec("cd /workspace/app");
const statelessResult = await statelessSandbox.exec("pwd");
// statelessResult.stdout: "/workspace"
// The second exec did not inherit shell state from the first.
```

```ts
// Default behavior: implicit operations use the default session
const statefulSandbox = getSandbox(env.Sandbox, 'user-123');

await statefulSandbox.exec('cd /workspace/app');
const statefulResult = await statefulSandbox.exec('pwd');
// statefulResult.stdout: "/workspace/app"
// The second exec inherited the working directory from the first.

// Sessionless behavior: implicit operations do not share shell state
const statelessSandbox = getSandbox(env.Sandbox, 'api-worker', {
  enableDefaultSession: false
});

await statelessSandbox.exec('cd /workspace/app');
const statelessResult = await statelessSandbox.exec('pwd');
// statelessResult.stdout: "/workspace"
// The second exec did not inherit shell state from the first.
```

### keepAlive

**Type**: `boolean` **Default**: `false`

Keep the container alive indefinitely by preventing automatic shutdown. When `true`, the container automatically sends heartbeat pings every 30 seconds to prevent eviction and will never auto-timeout.

**How it works**: The sandbox automatically schedules lightweight ping requests to the container every 30 seconds. This prevents the container from being evicted due to inactivity while minimizing resource overhead. You can also enable/disable keepAlive dynamically using [setKeepAlive()](https://developers.cloudflare.com/sandbox/api/lifecycle/#setkeepalive).

The `keepAlive` flag persists across Durable Object hibernation and wakeup cycles. Once enabled, you do not need to re-set it after the sandbox wakes from hibernation.

```js
// For long-running processes that need the container to stay alive
const sandbox = getSandbox(env.Sandbox, "user-123", {
	keepAlive: true,
});

// Run your long-running process
await sandbox.startProcess("python long_running_script.py");

// Important: Must explicitly destroy when done
try {
	// Your work here
} finally {
	await sandbox.destroy(); // Required to prevent containers running indefinitely
}
```

```ts
// For long-running processes that need the container to stay alive
const sandbox = getSandbox(env.Sandbox, 'user-123', {
  keepAlive: true
});

// Run your long-running process
await sandbox.startProcess('python long_running_script.py');

// Important: Must explicitly destroy when done
try {
  // Your work here
} finally {
  await sandbox.destroy(); // Required to prevent containers running indefinitely
}
```

Resource management with keepAlive

When `keepAlive: true` is set, containers automatically send heartbeat pings to prevent eviction and will not automatically timeout. They must be explicitly destroyed using `destroy()` or disabled with `setKeepAlive(false)` to prevent containers running indefinitely and counting toward your account limits.

### sleepAfter

**Type**: `string | number` **Default**: `"10m"` (10 minutes)

Duration of inactivity before the sandbox automatically sleeps. Accepts duration strings (`"30s"`, `"5m"`, `"1h"`) or numbers (seconds).

Bug fix in v0.2.17

Prior to v0.2.17, the `sleepAfter` option passed to `getSandbox()` was ignored due to a timing issue. The option is now properly applied when creating sandbox instances.

```js
// Sleep after 30 seconds of inactivity
const sandbox = getSandbox(env.Sandbox, "user-123", {
	sleepAfter: "30s",
});

// Sleep after 5 minutes (using number)
const sandbox2 = getSandbox(env.Sandbox, "user-456", {
	sleepAfter: 300, // 300 seconds = 5 minutes
});
```

```ts
// Sleep after 30 seconds of inactivity
const sandbox = getSandbox(env.Sandbox, 'user-123', {
  sleepAfter: '30s'
});

// Sleep after 5 minutes (using number)
const sandbox2 = getSandbox(env.Sandbox, 'user-456', {
  sleepAfter: 300  // 300 seconds = 5 minutes
});
```

Ignored when keepAlive is true

When `keepAlive: true` is set, `sleepAfter` is ignored and the sandbox never sleeps automatically.

### containerTimeouts

**Type**: `object`

Configure timeouts for container startup operations.

```js
// Extended startup with custom Dockerfile work
// (installing packages, starting services before SDK)
const sandbox = getSandbox(env.Sandbox, "data-processor", {
	containerTimeouts: {
		portReadyTimeoutMS: 180_000, // 3 minutes for startup work
	},
});

// Wait longer during traffic spikes
const sandbox2 = getSandbox(env.Sandbox, "user-env", {
	containerTimeouts: {
		instanceGetTimeoutMS: 60_000, // 1 minute for provisioning
	},
});
```

```ts
// Extended startup with custom Dockerfile work
// (installing packages, starting services before SDK)
const sandbox = getSandbox(env.Sandbox, 'data-processor', {
  containerTimeouts: {
    portReadyTimeoutMS: 180_000  // 3 minutes for startup work
  }
});

// Wait longer during traffic spikes
const sandbox2 = getSandbox(env.Sandbox, 'user-env', {
  containerTimeouts: {
    instanceGetTimeoutMS: 60_000   // 1 minute for provisioning
  }
});
```

**Available timeout options**:

* `instanceGetTimeoutMS` \- How long to wait for Cloudflare to provision a new container instance. Increase during traffic spikes when many containers provision simultaneously. **Default**: `30000` (30 seconds)
* `portReadyTimeoutMS` \- How long to wait for the sandbox API to become ready. Increase if you extend the base Dockerfile with custom startup work (installing packages, starting services). **Default**: `90000` (90 seconds)

**Environment variable overrides**:

* `SANDBOX_INSTANCE_TIMEOUT_MS` \- Override `instanceGetTimeoutMS`
* `SANDBOX_PORT_TIMEOUT_MS` \- Override `portReadyTimeoutMS`

Precedence: `options` \> `env vars` \> SDK defaults

### Logging

**Type**: Environment variables

Control SDK logging for debugging and monitoring. Set these in your Worker's `wrangler.jsonc` file.

**Available options**:

* `SANDBOX_LOG_LEVEL` \- Minimum log level: `debug`, `info`, `warn`, `error`. **Default**: `info`
* `SANDBOX_LOG_FORMAT` \- Output format: `json`, `pretty`. **Default**: `json`

```jsonc
{
	"vars": {
		"SANDBOX_LOG_LEVEL": "debug",
		"SANDBOX_LOG_FORMAT": "pretty"
	}
}
```

```toml
[vars]
SANDBOX_LOG_LEVEL = "debug"
SANDBOX_LOG_FORMAT = "pretty"
```

Read at startup

Logging configuration is read when your Worker starts and cannot be changed at runtime. Changes require redeploying your Worker.

Use `debug` \+ `pretty` for local development. Use `info` or `warn` \+ `json` for production (structured logging).

### normalizeId

**Type**: `boolean` **Default**: `false` (will become `true` in a future version)

Lowercase sandbox IDs when creating sandboxes. When `true`, the ID you provide is lowercased before creating the Durable Object (e.g., "MyProject-123" → "myproject-123").

**Why this matters**: Preview URLs extract the sandbox ID from the hostname, which is always lowercase due to DNS case-insensitivity. Without normalization, a sandbox created with "MyProject-123" becomes unreachable via preview URL because the URL routing looks for "myproject-123" (different Durable Object).

```js
// Without normalization (default)
const sandbox1 = getSandbox(env.Sandbox, "MyProject-123");
// Creates Durable Object with ID: "MyProject-123"
// Preview URL: 8000-myproject-123.example.com
// Problem: URL routes to "myproject-123" (different DO)

// With normalization
const sandbox2 = getSandbox(env.Sandbox, "MyProject-123", {
	normalizeId: true,
});
// Creates Durable Object with ID: "myproject-123"
// Preview URL: 8000-myproject-123.example.com
// Works: URL routes to "myproject-123" (same DO)
```

```ts
// Without normalization (default)
const sandbox1 = getSandbox(env.Sandbox, 'MyProject-123');
// Creates Durable Object with ID: "MyProject-123"
// Preview URL: 8000-myproject-123.example.com
// Problem: URL routes to "myproject-123" (different DO)

// With normalization
const sandbox2 = getSandbox(env.Sandbox, 'MyProject-123', {
  normalizeId: true
});
// Creates Durable Object with ID: "myproject-123"
// Preview URL: 8000-myproject-123.example.com
// Works: URL routes to "myproject-123" (same DO)
```

Different normalizeId values = different sandboxes

`getSandbox(ns, 'MyProject-123')` and `getSandbox(ns, 'MyProject-123', { normalizeId: true })` create two separate Durable Objects. If you have existing sandboxes with uppercase IDs, enabling normalization creates new sandboxes—you won't access the old ones.

Future default

In a future SDK version, `normalizeId` will default to `true`. All sandbox IDs will be lowercase regardless of input casing. Use lowercase IDs now or explicitly set `normalizeId: true` to prepare for this change.

## When to use normalizeId

Use `normalizeId: true` when:

* **Using preview URLs** \- Required for port exposure if your IDs contain uppercase letters
* **New projects** \- Either enable this option OR use lowercase IDs from the start (both work)
* **Migrating existing code** \- Create new sandboxes with this enabled; old uppercase sandboxes will eventually be destroyed (explicitly or after timeout)

**Best practice**: Use lowercase IDs from the start (`'my-project-123'` instead of `'MyProject-123'`).

## When to use sleepAfter

Use custom `sleepAfter` values to:

* **Reduce costs** \- Shorter timeouts (e.g., `"1m"`) for infrequent workloads
* **Extend availability** \- Longer timeouts (e.g., `"30m"`) for interactive workflows
* **Balance performance** \- Fine-tune based on your application's usage patterns

The default 10-minute timeout works well for most applications. Adjust based on your needs.

## When to use keepAlive

Use `keepAlive: true` for:

* **Long-running builds** \- CI/CD pipelines that may have idle periods between steps
* **Batch processing** \- Jobs that process data in waves with gaps between batches
* **Monitoring tasks** \- Processes that periodically check external services
* **Interactive sessions** \- User-driven workflows where the container should remain available

With `keepAlive`, containers send automatic heartbeat pings every 30 seconds to prevent eviction and never sleep automatically. Use for scenarios where you control the lifecycle explicitly.

## Related resources

* [Expose services guide](https://developers.cloudflare.com/sandbox/guides/expose-services/) \- Using `normalizeId` with preview URLs
* [Preview URLs concept](https://developers.cloudflare.com/sandbox/concepts/preview-urls/) \- Understanding DNS case-insensitivity
* [Background processes guide](https://developers.cloudflare.com/sandbox/guides/background-processes/) \- Using `keepAlive` with long-running processes
* [Lifecycle API](https://developers.cloudflare.com/sandbox/api/lifecycle/) \- Create and manage sandboxes with `setKeepAlive()`
* [Sandboxes concept](https://developers.cloudflare.com/sandbox/concepts/sandboxes/) \- Understanding sandbox lifecycle

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/configuration/sandbox-options/#page","headline":"Sandbox options · Cloudflare Sandbox SDK docs","description":"Configure Sandbox SDK behavior with sleep timeouts, resource limits, and container settings.","url":"https://developers.cloudflare.com/sandbox/configuration/sandbox-options/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Configure how Sandbox SDK communicates between Durable Objects and containers.
title: Transport modes
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Transport modes

Last updated Aug 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/configuration/transport/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Configure how the Sandbox SDK communicates with containers using transport modes.

Coming soon: Sandbox SDK 1.0

This page documents transport selection on today's stable `@cloudflare/sandbox` package.

**Sandbox SDK 1.0** (preview on `@next`) uses a single RPC control channel. Remove `SANDBOX_TRANSPORT`, the `transport` option on `getSandbox()`, and `setTransport()`. Refer to [Migrate to the 1.0 preview](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/#remove-transport-selection).

## Overview

The Sandbox SDK supports three transport modes for communication between the Durable Object and the container:

* **HTTP transport** (default) - Each SDK operation makes a separate HTTP request to the container.
* **NEW: RPC transport** \- All SDK operations are multiplexed over a single persistent WebSocket connection. Will replace HTTP as the default transport in future. Available since 0.9.1.
* **Deprecated: WebSocket transport** \- All SDK operations are multiplexed over a single persistent WebSocket. Superseded by RPC transport which uses an improved protocol.

## When to use RPC transport

Use the RPC transport when your Worker or Durable Object makes many SDK operations per request. This avoids hitting [subrequest limits](https://developers.cloudflare.com/workers/platform/limits/#subrequests).

### Subrequest limits

Cloudflare Workers have subrequest limits that apply when making requests to external services, including container API calls:

* **Workers Free**: 50 subrequests per request
* **Workers Paid**: 1,000 subrequests per request

With HTTP transport (default), each SDK operation (`exec()`, `readFile()`, `writeFile()`, etc.) consumes one subrequest. Applications that perform many sandbox operations in a single request can hit these limits.

### How RPC transport helps

RPC transport establishes a single persistent connection to the container and multiplexes all SDK operations over it. The WebSocket upgrade counts as **one subrequest** regardless of how many operations you perform afterwards.

**Example with HTTP transport (4 subrequests):**

```typescript
await sandbox.exec("python setup.py");
await sandbox.writeFile("/app/config.json", config);
await sandbox.exec("python process.py");
const result = await sandbox.readFile("/app/output.txt");
```

**Same code with RPC transport (1 subrequest):**

```typescript
// Identical code - transport is configured via environment variable
await sandbox.exec("python setup.py");
await sandbox.writeFile("/app/config.json", config);
await sandbox.exec("python process.py");
const result = await sandbox.readFile("/app/output.txt");
```

RPC transport also removes the [32 MiB limitation](https://developers.cloudflare.com/workers/runtime-apis/rpc/#limitations) that the HTTP transport has. Pass a `ReadableStream` instance to the `writeFile()` method.

```js
const req = await fetch("https://example.com/archive.tar.gz");
await sandbox.writeFile("/archive.tar.gz", req.body);
```

## Configuration

Set the `SANDBOX_TRANSPORT` environment variable in your Worker's configuration. The SDK reads this from the Worker environment bindings (not from inside the container).

### HTTP transport (default)

HTTP transport is the default and requires no additional configuration.

### RPC transport

Enable RPC transport by adding `SANDBOX_TRANSPORT` to your Worker's `vars`:

```jsonc
{
	"name": "my-sandbox-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-09-12",
	"vars": {
		"SANDBOX_TRANSPORT": "rpc"
	},
	"containers": [
		{
			"class_name": "Sandbox",
			"image": "./Dockerfile",
		},
	],
	"durable_objects": {
		"bindings": [
			{
				"class_name": "Sandbox",
				"name": "Sandbox",
			},
		],
	},
}
```

```toml
name = "my-sandbox-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-12"

[vars]
SANDBOX_TRANSPORT = "rpc"

[[containers]]
class_name = "Sandbox"
image = "./Dockerfile"

[[durable_objects.bindings]]
class_name = "Sandbox"
name = "Sandbox"
```

No application code changes are needed. The SDK automatically uses the configured transport for all operations.

## Transport behavior

### Connection lifecycle

**HTTP transport:**

* Creates a new HTTP request for each SDK operation
* No persistent connection
* Each request is independent and stateless

**RPC transport:**

* Establishes a WebSocket connection on the first SDK operation
* Maintains the persistent connection for all subsequent operations
* Connection is closed when the sandbox sleeps or is evicted
* Automatically reconnects if the connection drops

### Streaming support

All transports support streaming operations (like `exec()` with real-time output):

* **HTTP transport** \- Uses Server-Sent Events (SSE)
* **RPC transport** \- Uses WebSocket streaming messages

Your code remains identical regardless of transport mode.

### Error handling

All transports provide identical error handling behavior. The SDK automatically retries on transient errors (like 503 responses) with exponential backoff.

WebSocket-specific behavior:

* Connection failures trigger automatic reconnection
* The SDK transparently handles WebSocket disconnections
* In-flight operations are not lost during reconnection

## Choosing a transport

We expect the RPC transport to replace the default HTTP transport in a future release. New functionality may support only the RPC transport. Switching to use it now will avoid migrations in the future.

## Migration guide

Switching between transports requires no code changes.

### Switch from HTTP to RPC

Requires staged deployment

Using the `rpc` transport requires version 0.9.1 or newer. If you are using an older version of the Sandbox SDK upgrade and deploy your application with the newer `@cloudflare/sandbox` and image first. Otherwise there will be issues with newer SDK clients attempting to connect to older sandboxes that do not support the new transport.

Add `SANDBOX_TRANSPORT` to your `wrangler.jsonc`:

```jsonc
{
	"vars": {
		"SANDBOX_TRANSPORT": "rpc"
	},
}
```

```toml
[vars]
SANDBOX_TRANSPORT = "rpc"
```

Then deploy:

```bash
npx wrangler deploy
```

### Switch from RPC to HTTP

Remove the `SANDBOX_TRANSPORT` variable (or set it to `"http"`):

```jsonc
{
	"vars": {
		// Remove SANDBOX_TRANSPORT or set to "http"
	},
}
```

```toml
vars = { }
```

### Switch from deprecated WebSocket to RPC

Requires staged deployment

Using the `rpc` transport requires version 0.9.1 or newer. If you are using an older version of the Sandbox SDK upgrade and deploy your application with the newer `@cloudflare/sandbox` and image first. Otherwise there will be issues with newer SDK clients attempting to connect to older sandboxes that do not support the new transport.

Set the `SANDBOX_TRANSPORT` variable to `"rpc"`:

```jsonc
{
	"vars": {
		"SANDBOX_TRANSPORT": "rpc"
	},
}
```

```toml
[vars]
SANDBOX_TRANSPORT = "rpc"
```

## Related resources

* [Wrangler configuration](https://developers.cloudflare.com/sandbox/configuration/wrangler/) \- Complete Worker configuration
* [Environment variables](https://developers.cloudflare.com/sandbox/configuration/environment-variables/) \- Passing configuration to sandboxes
* [Workers subrequest limits](https://developers.cloudflare.com/workers/platform/limits/#subrequests) \- Understanding subrequest limits
* [Architecture](https://developers.cloudflare.com/sandbox/concepts/architecture/) \- How Sandbox SDK components communicate

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/configuration/transport/#page","headline":"Transport modes · Cloudflare Sandbox SDK docs","description":"Configure how Sandbox SDK communicates between Durable Objects and containers.","url":"https://developers.cloudflare.com/sandbox/configuration/transport/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-07","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Set up Wrangler bindings, Durable Objects, and container settings for Sandbox SDK.
title: Wrangler configuration
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Wrangler configuration

Last updated Aug 28, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/configuration/wrangler/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

## Minimal configuration

The minimum required configuration for using Sandbox SDK:

```jsonc
{
	"name": "my-sandbox-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-09-12",
	"compatibility_flags": ["nodejs_compat"],
	"containers": [
		{
			"class_name": "Sandbox",
			"image": "./Dockerfile",
		},
	],
	"durable_objects": {
		"bindings": [
			{
				"class_name": "Sandbox",
				"name": "Sandbox",
			},
		],
	},
	"migrations": [
		{
			"new_sqlite_classes": ["Sandbox"],
			"tag": "v1",
		},
	],
}
```

```toml
name = "my-sandbox-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-12"
compatibility_flags = [ "nodejs_compat" ]

[[containers]]
class_name = "Sandbox"
image = "./Dockerfile"

[[durable_objects.bindings]]
class_name = "Sandbox"
name = "Sandbox"

[[migrations]]
new_sqlite_classes = [ "Sandbox" ]
tag = "v1"
```

## Required settings

The Sandbox SDK is built on Cloudflare Containers. Your configuration requires three sections:

1. **containers** \- Define the container image (your runtime environment)
2. **durable\_objects.bindings** \- Bind the Sandbox Durable Object to your Worker
3. **migrations** \- Initialize the Durable Object class

The minimal configuration shown above includes all required settings. For detailed configuration options, refer to the [Containers configuration documentation](https://developers.cloudflare.com/workers/wrangler/configuration/#containers).

## Backup storage

To use the [backup and restore API](https://developers.cloudflare.com/sandbox/api/backups/), you need an R2 bucket binding and presigned URL credentials. The container uploads and downloads backup archives directly to/from R2 using presigned URLs, which requires R2 API token credentials.

### 1\. Create the R2 bucket

```sh
npx wrangler r2 bucket create my-backup-bucket
```

### 2\. Add the binding and environment variables

```jsonc
{
	"vars": {
		"BACKUP_BUCKET_NAME": "my-backup-bucket",
		"CLOUDFLARE_ACCOUNT_ID": "<YOUR_ACCOUNT_ID>",
	},
	"r2_buckets": [
		{
			"binding": "BACKUP_BUCKET",
			"bucket_name": "my-backup-bucket",
		},
	],
}
```

```toml
[vars]
BACKUP_BUCKET_NAME = "my-backup-bucket"
CLOUDFLARE_ACCOUNT_ID = "<YOUR_ACCOUNT_ID>"

[[r2_buckets]]
binding = "BACKUP_BUCKET"
bucket_name = "my-backup-bucket"
```

### 3\. Set R2 API credentials as secrets

```sh
npx wrangler secret put R2_ACCESS_KEY_ID
npx wrangler secret put R2_SECRET_ACCESS_KEY
```

Create an R2 API token in the [Cloudflare dashboard ↗](https://dash.cloudflare.com/) under **R2** \> **Overview** \> **Manage R2 API Tokens**. The token needs **Object Read & Write** permissions for your backup bucket.

The SDK uses these credentials to generate presigned URLs that allow the container to transfer backup archives directly to and from R2\. For a complete setup walkthrough, refer to the [backup and restore guide](https://developers.cloudflare.com/sandbox/guides/backup-restore/).

## Troubleshooting

### Binding not found

**Error**: `TypeError: env.Sandbox is undefined`

**Solution**: Ensure your `wrangler.jsonc` includes the Durable Objects binding:

```jsonc
{
	"durable_objects": {
		"bindings": [
			{
				"class_name": "Sandbox",
				"name": "Sandbox",
			},
		],
	},
}
```

```toml
[[durable_objects.bindings]]
class_name = "Sandbox"
name = "Sandbox"
```

### Missing migrations

**Error**: Durable Object not initialized

**Solution**: Add migrations for the Sandbox class:

```jsonc
{
	"migrations": [
		{
			"new_sqlite_classes": ["Sandbox"],
			"tag": "v1",
		},
	],
}
```

```toml
[[migrations]]
new_sqlite_classes = [ "Sandbox" ]
tag = "v1"
```

## Related resources

* [Deploy a Sandbox application](https://developers.cloudflare.com/sandbox/guides/deploy/) \- Deploy and keep package and image aligned
* [Deploy Containers](https://developers.cloudflare.com/containers/guides/deploy/) \- Containers deploy path
* [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/) \- Configure HTTP, WebSocket, and RPC transport
* [Wrangler documentation](https://developers.cloudflare.com/workers/wrangler/) \- Complete Wrangler reference
* [Durable Objects setup](https://developers.cloudflare.com/durable-objects/get-started/) \- DO-specific configuration
* [Dockerfile reference](https://developers.cloudflare.com/sandbox/configuration/dockerfile/) \- Custom container images
* [Environment variables](https://developers.cloudflare.com/sandbox/configuration/environment-variables/) \- Passing configuration to sandboxes
* [Get Started guide](https://developers.cloudflare.com/sandbox/get-started/) \- Initial setup walkthrough

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/configuration/wrangler/#page","headline":"Wrangler configuration · Cloudflare Sandbox SDK docs","description":"Set up Wrangler bindings, Durable Objects, and container settings for Sandbox SDK.","url":"https://developers.cloudflare.com/sandbox/configuration/wrangler/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-28","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Sandbox SDK platform information including pricing and resource limits.
title: Platform
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Platform

Last updated Apr 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/platform/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Information about the Sandbox SDK platform, including pricing and limits.

## Available resources

* [Pricing](https://developers.cloudflare.com/sandbox/platform/pricing/) \- Understand costs based on the Containers platform
* [Limits](https://developers.cloudflare.com/sandbox/platform/limits/) \- Resource limits and best practices

Since Sandbox SDK is built on [Containers](https://developers.cloudflare.com/containers/), it shares the same underlying platform characteristics. Refer to these pages to understand how pricing and limits work for your sandbox deployments.

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/sandbox/platform/#page","headline":"Platform · Cloudflare Sandbox SDK docs","description":"Sandbox SDK platform information including pricing and resource limits.","url":"https://developers.cloudflare.com/sandbox/platform/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Resource limits for Sandbox SDK including vCPU, memory, disk, and container constraints.
title: Limits
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Limits

Last updated Aug 28, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/platform/limits/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Since the Sandbox SDK is built on top of the [Containers](https://developers.cloudflare.com/containers/) platform, it shares the same underlying platform characteristics. Refer to these pages to understand how pricing and limits work for your sandbox deployments.

Sandbox also inherits current Containers lifecycle, placement, and routing behavior. For more detail, refer to [Lifecycle of a Container](https://developers.cloudflare.com/containers/concepts/architecture/) and [Scaling and Routing](https://developers.cloudflare.com/containers/configuration/scaling-and-routing/).

## Container limits

Refer to [Containers limits](https://developers.cloudflare.com/containers/platform/limits/) for complete details on:

* Memory, vCPU, and disk limits for concurrent container instances
* Instance types and their resource allocations
* Image size and storage limits

## Workers and Durable Objects limits

When using the Sandbox SDK from Workers or Durable Objects, you are subject to [Workers subrequest limits](https://developers.cloudflare.com/workers/platform/limits/#subrequests). By default, the SDK uses HTTP transport where each operation (`exec()`, `readFile()`, `writeFile()`, etc.) counts as one subrequest.

### Subrequest limits

* **Workers Free**: 50 subrequests per request
* **Workers Paid**: 1,000 subrequests per request

### Avoid subrequest limits with RPC transport

Enable RPC transport to multiplex all SDK calls over a single persistent connection:

```jsonc
{
	"vars": {
		"SANDBOX_TRANSPORT": "rpc"
	},
}
```

```toml
[vars]
SANDBOX_TRANSPORT = "rpc"
```

With RPC transport enabled:

* The persistent connection counts as one subrequest
* All subsequent SDK operations use the existing connection (no additional subrequests)
* Ideal for workflows with many SDK operations per request

See [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/) for a complete guide.

## Best practices

To work within these limits:

* **Right-size your instances** \- Choose the appropriate [instance type](https://developers.cloudflare.com/containers/platform/limits/#instance-types) based on your workload requirements
* **Clean up unused sandboxes** \- Terminate sandbox sessions when they are no longer needed to free up resources
* **Optimize images** \- Keep your [custom Dockerfiles](https://developers.cloudflare.com/sandbox/configuration/dockerfile/) lean to reduce image size
* **Use RPC transport for high-frequency operations** \- Enable `SANDBOX_TRANSPORT=rpc` to avoid subrequest limits when making many SDK calls per request

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/platform/limits/#page","headline":"Limits · Cloudflare Sandbox SDK docs","description":"Resource limits for Sandbox SDK including vCPU, memory, disk, and container constraints.","url":"https://developers.cloudflare.com/sandbox/platform/limits/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-28","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```

---

---
description: Sandbox SDK pricing is based on the underlying Containers platform usage rates.
title: Pricing
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Pricing

Last updated Aug 28, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/sandbox/platform/pricing/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Sandbox SDK pricing is determined by the underlying [Containers](https://developers.cloudflare.com/containers/) platform it's built on.

## Containers Pricing

Refer to [Containers pricing](https://developers.cloudflare.com/containers/platform/pricing/) for complete details on:

* vCPU, memory, and disk usage rates
* Network egress pricing
* Instance types and their costs

## Related Pricing

When using Sandbox, you'll also be billed for:

* [Workers](https://developers.cloudflare.com/workers/platform/pricing/) \- Handles incoming requests to your sandbox
* [Durable Objects](https://developers.cloudflare.com/durable-objects/platform/pricing/) \- Powers each sandbox instance
* [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/#pricing) \- Optional observability (if enabled)

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/platform/pricing/#page","headline":"Pricing · Cloudflare Sandbox SDK docs","description":"Sandbox SDK pricing is based on the underlying Containers platform usage rates.","url":"https://developers.cloudflare.com/sandbox/platform/pricing/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-28","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```
