---
title: Sandbox SDK (Beta)
description: The Sandbox SDK enables you to run untrusted code securely in isolated environments. Built on Containers, Sandbox SDK provides a simple API for executing commands, managing files, running background processes, and exposing services — all from your Workers applications.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Sandbox SDK (Beta)

Build secure, isolated code execution environments

 Available on Workers Paid plan 

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.

* [ Execute Commands ](#tab-panel-6085)
* [ Code Interpreter ](#tab-panel-6086)
* [ File Operations ](#tab-panel-6087)
* [ File Watching ](#tab-panel-6088)
* [ Terminal Access ](#tab-panel-6089)
* [ WebSocket Connections ](#tab-panel-6090)

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

### Execute commands securely

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

[ Learn about command execution ](https://developers.cloudflare.com/sandbox/guides/execute-commands/) 

### Manage files and processes

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

[ Learn about file operations ](https://developers.cloudflare.com/sandbox/guides/manage-files/) 

### Expose services with preview URLs

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

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

### Execute code directly

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 ](https://developers.cloudflare.com/sandbox/guides/code-execution/) 

### Build interactive 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 ](https://developers.cloudflare.com/sandbox/guides/browser-terminals/) 

### Persistent storage with object storage

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 ](https://developers.cloudflare.com/sandbox/guides/mount-buckets/) 

### Watch files for real-time changes

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 ](https://developers.cloudflare.com/sandbox/guides/file-watching/) 

### Proxy external API requests securely

Keep credentials in your Worker while allowing sandboxes to access external APIs. A Worker proxy validates short-lived JWT tokens from the sandbox and injects real credentials at request time.

[ Learn about request proxying ](https://developers.cloudflare.com/sandbox/guides/proxy-requests/) 

---

## 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

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

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

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

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

[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.

[Beta Information](https://developers.cloudflare.com/sandbox/platform/beta-info/) 

Learn about the Sandbox Beta, current status, and upcoming features.

[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.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}}]}
```

---

---
title: Getting started
description: 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.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/get-started.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Getting started

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.

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:

 npm  yarn  pnpm 

```
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

Terminal window

```

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

    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 the same ID to reuse the same sandbox instance across requests.
* `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:

Terminal window

```

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:

Terminal window

```

# 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:

Terminal window

```

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 first deployment, wait 2-3 minutes before making requests. The Worker deploys immediately, but the container needs time to provision.

Check deployment status:

Terminal window

```

npx wrangler containers list


```

## 5\. Test your deployment

Visit your Worker URL (shown in deploy output):

Terminal window

```

# 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.

Preview URLs require custom domain

If you plan to expose ports from sandboxes (using `exposePort()` for preview URLs), you will need to set up a custom domain with wildcard DNS routing. The `.workers.dev` domain does not support the subdomain patterns required for preview URLs. See [Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/) when you are ready to expose services.

## Understanding the configuration

Your `wrangler.jsonc` connects three pieces together:

* [  wrangler.jsonc ](#tab-panel-6239)
* [  wrangler.toml ](#tab-panel-6240)

```

{

  "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]]

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
* [Expose services](https://developers.cloudflare.com/sandbox/guides/expose-services/) \- Get public URLs for services running in your sandbox
* [Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/) \- Set up custom domains for preview URLs
* [API reference](https://developers.cloudflare.com/sandbox/api/) \- Complete API documentation

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/get-started/","name":"Getting started"}}]}
```

---

---
title: Tutorials
description: Learn how to build applications with Sandbox SDK through step-by-step tutorials. Each tutorial takes 20-30 minutes.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/tutorials/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Tutorials

Learn how to build applications with Sandbox SDK through step-by-step tutorials. Each tutorial takes 20-30 minutes.

[Code interpreter with Workers AIBuild 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 R2Mount 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 SandboxUse Claude Code to implement a task in your GitHub repository.](https://developers.cloudflare.com/sandbox/tutorials/claude-code/)[Build an AI code executorUse 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 AIUpload CSV files, generate analysis code with Claude, and return visualizations.](https://developers.cloudflare.com/sandbox/tutorials/analyze-data-with-ai/)[Automated testing pipelineBuild 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 botClone repositories, analyze code with Claude, and post review comments to GitHub PRs.](https://developers.cloudflare.com/sandbox/tutorials/code-review-bot/)

## 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/tutorials/","name":"Tutorials"}}]}
```

---

---
title: Build an AI code executor
description: Use Claude to generate Python code from natural language and execute it securely in sandboxes.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/tutorials/ai-code-executor.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Build an AI code executor

**Last reviewed:**  6 months ago 

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:

 npm  yarn  pnpm 

```
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
```

Terminal window

```

cd ai-code-executor


```

## 2\. Install dependencies

Install the Anthropic SDK:

 npm  yarn  pnpm  bun 

```
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:

Terminal window

```

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:

Terminal window

```

npm run dev


```

Note

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

Test with curl:

Terminal window

```

curl -X POST http://localhost:8787/execute \

  -H "Content-Type: application/json" \

  -d '{"question": "What is the 10th Fibonacci number?"}'


```

Response:

```

{

  "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:

Terminal window

```

npx wrangler deploy


```

Then set your Anthropic API key as a production secret:

Terminal window

```

npx wrangler secret put ANTHROPIC_API_KEY


```

Paste your API key from the [Anthropic Console ↗](https://console.anthropic.com/) when prompted.

Warning

After first deployment, wait 2-3 minutes for container provisioning. Check status with `npx wrangler containers list`.

## 7\. Test your deployment

Try different questions:

Terminal window

```

# 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/tutorials/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/tutorials/ai-code-executor/","name":"Build an AI code executor"}}]}
```

---

---
title: Analyze data with AI
description: Upload CSV files, generate analysis code with Claude, and return visualizations.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Analyze data with AI

**Last reviewed:**  6 months ago 

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:

 npm  yarn  pnpm 

```
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
```

Terminal window

```

cd analyze-data


```

## 2\. Install dependencies

 npm  yarn  pnpm  bun 

```
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 },

        );

      }


      // Check for generated chart

      let chart = null;

      try {

        const chartFile = await sandbox.readFile("/workspace/chart.png");

        const buffer = new Uint8Array(chartFile.content);

        chart = `data:image/png;base64,${btoa(String.fromCharCode(...buffer))}`;

      } 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:

Terminal window

```

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

Download a sample CSV:

Terminal window

```

# 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:

Terminal window

```

npm run dev


```

Test with curl:

Terminal window

```

curl -X POST http://localhost:8787 \

  -F "file=@test.csv" \

  -F "question=What is the average rating by year?"


```

Response:

```

{

  "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:

Terminal window

```

npx wrangler deploy


```

Then set your Anthropic API key as a production secret:

Terminal window

```

npx wrangler secret put ANTHROPIC_API_KEY


```

Paste your API key from the [Anthropic Console ↗](https://console.anthropic.com/) when prompted.

Warning

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/tutorials/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/tutorials/analyze-data-with-ai/","name":"Analyze data with AI"}}]}
```

---

---
title: Automated testing pipeline
description: Build a testing pipeline that clones Git repositories, installs dependencies, runs tests, and reports results.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Automated testing pipeline

**Last reviewed:**  6 months ago 

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

 npm  yarn  pnpm 

```
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
```

Terminal window

```

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:

Terminal window

```

npm run dev


```

Test with a repository:

Terminal window

```

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:

```

{

  "success": true,

  "exitCode": 0,

  "projectType": "nodejs",

  "message": "All tests passed"

}


```

## 4\. Deploy

Terminal window

```

npx wrangler deploy


```

For private repositories, set your GitHub token:

Terminal window

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/tutorials/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/tutorials/automated-testing-pipeline/","name":"Automated testing pipeline"}}]}
```

---

---
title: Run Claude Code on a Sandbox
description: Use Claude Code to implement a task in your GitHub repository.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/tutorials/claude-code.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Run Claude Code on a Sandbox

**Last reviewed:**  5 months ago 

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:

 npm  yarn  pnpm 

```
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
```

Terminal window

```

cd claude-code-sandbox


```

## 2\. Set up local environment variables

Create a `.dev.vars` file in your project root for local development:

Terminal window

```

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:

Terminal window

```

npm run dev


```

Note

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

Test with curl:

Terminal window

```

curl -X POST http://localhost:8787/ \

  -d '{

    "repo": "https://github.com/cloudflare/agents",

    "task": "remove the emojis from the readme"

  }'


```

Response:

```

{

  "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:

Terminal window

```

npx wrangler deploy


```

Then set your Anthropic API key as a production secret:

Terminal window

```

npx wrangler secret put ANTHROPIC_API_KEY


```

Paste your API key from the [Anthropic Console ↗](https://console.anthropic.com/) when prompted.

Warning

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/tutorials/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/tutorials/claude-code/","name":"Run Claude Code on a Sandbox"}}]}
```

---

---
title: Build a code review bot
description: Clone repositories, analyze code with Claude, and post review comments to GitHub PRs.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/tutorials/code-review-bot.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Build a code review bot

**Last reviewed:**  6 months ago 

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

 npm  yarn  pnpm 

```
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
```

Terminal window

```

cd code-review-bot


```

## 2\. Install dependencies

 npm  yarn  pnpm  bun 

```
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:

Terminal window

```

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:

Terminal window

```

npm run dev


```

In a separate terminal, create a tunnel to your local server:

Terminal window

```

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:

Terminal window

```

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:

Terminal window

```

npx wrangler deploy


```

Then set your production secrets:

Terminal window

```

# 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/tutorials/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/tutorials/code-review-bot/","name":"Build a code review bot"}}]}
```

---

---
title: Data persistence with R2
description: Mount R2 buckets as local filesystem paths to persist data across sandbox lifecycles.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/tutorials/persistent-storage.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Data persistence with R2

**Last reviewed:**  5 months ago 

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.

**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

 npm  yarn  pnpm 

```
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
```

Terminal window

```

cd data-pipeline


```

## 2\. Configure R2 binding

Add an R2 bucket binding to your `wrangler.json`:

wrangler.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:

* [  JavaScript ](#tab-panel-6493)
* [  TypeScript ](#tab-panel-6494)

JavaScript

```

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:**

Terminal window

```

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:**

Terminal window

```

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:

Terminal window

```

# 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
* **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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/tutorials/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/tutorials/persistent-storage/","name":"Data persistence with R2"}}]}
```

---

---
title: Code interpreter with Workers AI
description: Build a code interpreter using Workers AI GPT-OSS model with the official workers-ai-provider package.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/tutorials/workers-ai-code-interpreter.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Code interpreter with Workers AI

**Last reviewed:**  2 months ago 

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:

 npm  yarn  pnpm 

```
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
```

Terminal window

```

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:

* [  wrangler.jsonc ](#tab-panel-6495)
* [  wrangler.toml ](#tab-panel-6496)

```

{

  "name": "sandbox-code-interpreter-example",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "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"

      }

    ]

  }

}


```

```

name = "sandbox-code-interpreter-example"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-03"


[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:

Terminal window

```

npm run dev


```

Note

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

Test with curl:

Terminal window

```

# 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:

Terminal window

```

npx wrangler deploy


```

Warning

After first deployment, wait 2-3 minutes for container provisioning before making requests.

## 6\. Test your deployment

Try more complex queries:

Terminal window

```

# 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/tutorials/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/tutorials/workers-ai-code-interpreter/","name":"Code interpreter with Workers AI"}}]}
```

---

---
title: API Reference
description: The Sandbox SDK provides a comprehensive API for executing code, managing files, running processes, and exposing services in isolated sandboxes.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/api/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# API Reference

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.

[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 with copy-on-write overlays. Store backups in 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.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/api/","name":"API Reference"}}]}
```

---

---
title: Backups
description: Create point-in-time snapshots of sandbox directories and restore them with copy-on-write overlays.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/api/backups.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Backups

Create point-in-time snapshots of sandbox directories and restore them with copy-on-write overlays.

## Methods

### `createBackup()`

Create a point-in-time snapshot of a directory and upload it to R2 storage.

TypeScript

```

await sandbox.createBackup(options: BackupOptions): Promise<DirectoryBackup>


```

**Parameters**:

* `options` \- Backup configuration (see [BackupOptions](#backupoptions)):  
   * `dir` (required) - Absolute path to the directory to back up (for example, `"/workspace"`)  
   * `name` (optional) - Human-readable name for the backup. Maximum 256 characters, no control characters.  
   * `ttl` (optional) - Time-to-live in seconds until the backup expires. Default: `259200` (3 days). Must be a positive number.  
   * `useGitignore` (optional) - When `true`, excludes files matching `.gitignore` rules from the backup. Default: `false`. If the directory is not inside a git repository, no exclusions are applied. Requires `git` to be available in the container.

**Returns**: `Promise<DirectoryBackup>` containing:

* `id` \- Unique backup identifier (UUID)
* `dir` \- Directory that was backed up

* [  JavaScript ](#tab-panel-6091)
* [  TypeScript ](#tab-panel-6092)

JavaScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Create a backup of /workspace

const backup = await sandbox.createBackup({ dir: "/workspace" });


// Later, restore the backup

await sandbox.restoreBackup(backup);


```

TypeScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Create a backup of /workspace

const backup = await sandbox.createBackup({ dir: "/workspace" });


// Later, restore the backup

await sandbox.restoreBackup(backup);


```

**How it works**:

1. The container creates a compressed squashfs archive from the directory.
2. The container uploads the archive directly to R2 using a presigned URL.
3. Metadata is stored alongside the archive in R2.
4. The local archive is cleaned up.

**Throws**:

* `InvalidBackupConfigError` \- If `dir` is not absolute, contains `..`, the `BACKUP_BUCKET` binding is missing, or the R2 presigned URL credentials are not configured
* `BackupCreateError` \- If the container fails to create the archive, the upload to R2 fails, or `useGitignore` is `true` but `git` is not available in the container

R2 binding and credentials required

You must configure a `BACKUP_BUCKET` R2 binding and R2 presigned URL credentials (`R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `CLOUDFLARE_ACCOUNT_ID`, `BACKUP_BUCKET_NAME`) in your `wrangler.jsonc` before using backup methods. Refer to the [Wrangler configuration](https://developers.cloudflare.com/sandbox/configuration/wrangler/) for binding setup.

Path permissions

The backup process uses `mksquashfs`, which must have read access to every file and subdirectory in the target path. If any file has restrictive permissions (for example, directories owned by a different user), the backup fails with a `BackupCreateError: mksquashfs failed: Could not create destination file: Permission denied` error. Run `chmod -R a+rX` on the target directory before backing up, or refer to the [path permissions guide](https://developers.cloudflare.com/sandbox/guides/backup-restore/#path-permissions) for other options.

Partial writes

Partially-written files may not be captured consistently. Only completed writes are guaranteed to be included in the backup.

---

### `restoreBackup()`

Restore a previously created backup into a directory using FUSE overlayfs (copy-on-write).

TypeScript

```

await sandbox.restoreBackup(backup: DirectoryBackup): Promise<RestoreBackupResult>


```

**Parameters**:

* `backup` \- The backup handle returned by `createBackup()`. Contains `id` and `dir`. (see [DirectoryBackup](#directorybackup))

**Returns**: `Promise<RestoreBackupResult>` containing:

* `success` \- Whether the restore succeeded
* `dir` \- Directory that was restored
* `id` \- Backup ID that was restored

* [  JavaScript ](#tab-panel-6093)
* [  TypeScript ](#tab-panel-6094)

JavaScript

```

// Create a named backup with 24-hour TTL

const backup = await sandbox.createBackup({

  dir: "/workspace",

  name: "before-refactor",

  ttl: 86400,

});


// Store the handle for later use

await env.KV.put(`backup:${userId}`, JSON.stringify(backup));


```

TypeScript

```

// Create a named backup with 24-hour TTL

const backup = await sandbox.createBackup({

  dir: "/workspace",

  name: "before-refactor",

  ttl: 86400,

});


// Store the handle for later use

await env.KV.put(`backup:${userId}`, JSON.stringify(backup));


```

**How it works**:

1. Metadata is downloaded from R2 and the TTL is checked. If expired, an error is thrown (with a 60-second buffer).
2. The container downloads the archive directly from R2 using a presigned URL.
3. The container mounts the squashfs archive with FUSE overlayfs.

**Throws**:

* `InvalidBackupConfigError` \- If `backup.id` is missing or not a valid UUID, or `backup.dir` is invalid
* `BackupNotFoundError` \- If the backup metadata or archive is not found in R2
* `BackupExpiredError` \- If the backup TTL has elapsed
* `BackupRestoreError` \- If the container fails to restore

Copy-on-write

Restore uses copy-on-write semantics. The backup is mounted as a read-only lower layer, and new writes go to a writable upper layer. The backup can be restored into a different directory than the original.

Ephemeral mount

The FUSE mount is lost when the sandbox sleeps or restarts. Re-restore from the backup handle to recover. Stop processes writing to the target directory before restoring.

## Usage patterns

### Exclude gitignored files

Use `useGitignore` to exclude files matching `.gitignore` rules (such as `node_modules/` or `dist/`) from the backup. This reduces backup size for git repositories.

* [  JavaScript ](#tab-panel-6095)
* [  TypeScript ](#tab-panel-6096)

JavaScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Exclude gitignored files from the backup

const backup = await sandbox.createBackup({

  dir: "/workspace",

  useGitignore: true,

});


// Without useGitignore (default), all files are included

const fullBackup = await sandbox.createBackup({

  dir: "/workspace",

});


```

TypeScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Exclude gitignored files from the backup

const backup = await sandbox.createBackup({

  dir: "/workspace",

  useGitignore: true,

});


// Without useGitignore (default), all files are included

const fullBackup = await sandbox.createBackup({

  dir: "/workspace",

});


```

If the directory is not inside a git repository, `useGitignore` has no effect and all files are included. If `useGitignore` is `true` but `git` is not installed in the container, a `BackupCreateError` is thrown.

### Checkpoint and restore

Use backups as checkpoints before risky operations.

* [  JavaScript ](#tab-panel-6097)
* [  TypeScript ](#tab-panel-6098)

JavaScript

```

// Save checkpoint before risky operation

const checkpoint = await sandbox.createBackup({ dir: "/workspace" });


try {

  await sandbox.exec("npm install some-experimental-package");

  await sandbox.exec("npm run build");

} catch (error) {

  // Restore to the checkpoint if something goes wrong

  await sandbox.restoreBackup(checkpoint);

}


```

TypeScript

```

// Save checkpoint before risky operation

const checkpoint = await sandbox.createBackup({ dir: "/workspace" });


try {

  await sandbox.exec("npm install some-experimental-package");

  await sandbox.exec("npm run build");

} catch (error) {

  // Restore to the checkpoint if something goes wrong

  await sandbox.restoreBackup(checkpoint);

}


```

### Error handling

* [  JavaScript ](#tab-panel-6099)
* [  TypeScript ](#tab-panel-6100)

JavaScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


try {

  const backup = await sandbox.createBackup({ dir: "/workspace" });

  console.log(`Backup created: ${backup.id}`);

} 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);

  }

}


```

TypeScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


try {

  const backup = await sandbox.createBackup({ dir: "/workspace" });

  console.log(`Backup created: ${backup.id}`);

} 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);

  }

}


```

## Behavior

* Concurrent backup and restore operations on the same sandbox are automatically serialized.
* The returned `DirectoryBackup` handle is serializable — store it in KV, D1, or Durable Object storage.
* Overlapping backups are independent. Restoring a parent directory overwrites subdirectory mounts.

### TTL enforcement

The `ttl` value controls when a backup is considered expired. The SDK enforces this at **restore time only** — when you call `restoreBackup()`, the SDK reads the backup metadata from R2 and checks whether the TTL has elapsed. If it has, the restore is rejected with a `BACKUP_EXPIRED` error.

The TTL does **not** automatically delete objects from R2\. Expired backup archives and metadata remain in your R2 bucket until you delete them. To automatically clean up expired objects, configure an [R2 object lifecycle rule](https://developers.cloudflare.com/r2/buckets/object-lifecycles/) on your backup bucket. Without a lifecycle rule, expired backups continue to consume R2 storage.

## Types

### `BackupOptions`

TypeScript

```

interface BackupOptions {

  dir: string;

  name?: string;

  ttl?: number;

  useGitignore?: boolean;

}


```

**Fields**:

* `dir` (required) - Absolute path to the directory to back up
* `name` (optional) - Human-readable backup name. Maximum 256 characters, no control characters.
* `ttl` (optional) - Time-to-live in seconds. Default: `259200` (3 days). Must be a positive number.
* `useGitignore` (optional) - When `true`, excludes files matching `.gitignore` rules if the directory is inside a git repository. Default: `false`. If the directory is not inside a git repository, no git-based exclusions are applied.

### `DirectoryBackup`

TypeScript

```

interface DirectoryBackup {

  readonly id: string;

  readonly dir: string;

}


```

**Fields**:

* `id` \- Unique backup identifier (UUID)
* `dir` \- Directory that was backed up

### `RestoreBackupResult`

TypeScript

```

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

* [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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/api/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/api/backups/","name":"Backups"}}]}
```

---

---
title: Commands
description: Execute commands and manage background processes in the sandbox's isolated container environment.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/api/commands.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Commands

Execute commands and manage background processes in the sandbox's isolated container environment.

## Methods

### `exec()`

Execute a command and return the complete result.

TypeScript

```

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`

* [  JavaScript ](#tab-panel-6125)
* [  TypeScript ](#tab-panel-6126)

JavaScript

```

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,

});


```

TypeScript

```

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.

TypeScript

```

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`)

* [  JavaScript ](#tab-panel-6121)
* [  TypeScript ](#tab-panel-6122)

JavaScript

```

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);

  }

}


```

TypeScript

```

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.

TypeScript

```

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

* [  JavaScript ](#tab-panel-6105)
* [  TypeScript ](#tab-panel-6106)

JavaScript

```

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",

});


```

TypeScript

```

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.

TypeScript

```

const processes = await sandbox.listProcesses(): Promise<ProcessInfo[]>


```

* [  JavaScript ](#tab-panel-6101)
* [  TypeScript ](#tab-panel-6102)

JavaScript

```

const processes = await sandbox.listProcesses();


for (const proc of processes) {

  console.log(`${proc.id}: ${proc.command} (PID ${proc.pid})`);

}


```

TypeScript

```

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.

TypeScript

```

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.

* [  JavaScript ](#tab-panel-6107)
* [  TypeScript ](#tab-panel-6108)

JavaScript

```

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);


```

TypeScript

```

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.

TypeScript

```

await sandbox.killAllProcesses(): Promise<void>


```

* [  JavaScript ](#tab-panel-6103)
* [  TypeScript ](#tab-panel-6104)

JavaScript

```

await sandbox.killAllProcesses();


```

TypeScript

```

await sandbox.killAllProcesses();


```

### `streamProcessLogs()`

Stream logs from a running process in real-time.

TypeScript

```

const stream = await sandbox.streamProcessLogs(processId: string): Promise<ReadableStream>


```

**Parameters**:

* `processId` \- The process ID

**Returns**: `Promise<ReadableStream>` emitting `LogEvent` objects

* [  JavaScript ](#tab-panel-6111)
* [  TypeScript ](#tab-panel-6112)

JavaScript

```

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;

}


```

TypeScript

```

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.

TypeScript

```

const logs = await sandbox.getProcessLogs(processId: string): Promise<string>


```

**Parameters**:

* `processId` \- The process ID

**Returns**: `Promise<string>` with all accumulated output

* [  JavaScript ](#tab-panel-6109)
* [  TypeScript ](#tab-panel-6110)

JavaScript

```

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);


```

TypeScript

```

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.

* [  JavaScript ](#tab-panel-6113)
* [  TypeScript ](#tab-panel-6114)

JavaScript

```

// 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


```

TypeScript

```

// 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:**

* [  JavaScript ](#tab-panel-6117)
* [  TypeScript ](#tab-panel-6118)

JavaScript

```

const formData = JSON.stringify({

  username: "john_doe",

  email: "john@example.com",

});


const result = await sandbox.exec("python process_form.py", {

  stdin: formData,

});


```

TypeScript

```

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:**

* [  JavaScript ](#tab-panel-6115)
* [  TypeScript ](#tab-panel-6116)

JavaScript

```

// Simulate user responses to prompts

const responses = "yes\nmy-app\n1.0.0\n";

const result = await sandbox.exec("npm init", {

  stdin: responses,

});


```

TypeScript

```

// 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:**

* [  JavaScript ](#tab-panel-6119)
* [  TypeScript ](#tab-panel-6120)

JavaScript

```

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);


```

TypeScript

```

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.

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6127)
* [  TypeScript ](#tab-panel-6128)

JavaScript

```

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,

});


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6123)
* [  TypeScript ](#tab-panel-6124)

JavaScript

```

const db = await sandbox.startProcess("redis-server");


// Wait for database to accept connections

await db.waitForPort(6379, {

  mode: "tcp",

  timeout: 10000,

});


```

TypeScript

```

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.

TypeScript

```

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)

* [  JavaScript ](#tab-panel-6131)
* [  TypeScript ](#tab-panel-6132)

JavaScript

```

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);


```

TypeScript

```

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.

TypeScript

```

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

* [  JavaScript ](#tab-panel-6129)
* [  TypeScript ](#tab-panel-6130)

JavaScript

```

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


```

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/api/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/api/commands/","name":"Commands"}}]}
```

---

---
title: File Watching
description: 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().
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/api/file-watching.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# File Watching

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.

TypeScript

```

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 omitted, the default session is used)

**Returns**: `Promise<ReadableStream<Uint8Array>>` — an SSE stream of `FileWatchSSEEvent` objects

* [  JavaScript ](#tab-panel-6133)
* [  TypeScript ](#tab-panel-6134)

JavaScript

```

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();


```

TypeScript

```

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:

TypeScript

```

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.

TypeScript

```

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.

TypeScript

```

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.

TypeScript

```

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 default session 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.

TypeScript

```

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:

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/api/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/api/file-watching/","name":"File Watching"}}]}
```

---

---
title: Files
description: Read, write, and manage files in the sandbox filesystem. All paths are absolute (e.g., /workspace/app.js).
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/api/files.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Files

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.

TypeScript

```

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"`)

* [  JavaScript ](#tab-panel-6135)
* [  TypeScript ](#tab-panel-6136)

JavaScript

```

await sandbox.writeFile("/workspace/app.js", `console.log('Hello!');`);


// Binary data

await sandbox.writeFile("/tmp/image.png", base64Data, { encoding: "base64" });


```

TypeScript

```

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.

### `readFile()`

Read a file from the sandbox.

TypeScript

```

const file = await sandbox.readFile(path: string, options?: ReadFileOptions): Promise<FileInfo>


```

**Parameters**:

* `path` \- Absolute path to the file
* `options` (optional):  
   * `encoding` \- File encoding (`"utf-8"` or `"base64"`, default: auto-detected from MIME type)

**Returns**: `Promise<FileInfo>` with `content` and `encoding`

* [  JavaScript ](#tab-panel-6143)
* [  TypeScript ](#tab-panel-6144)

JavaScript

```

const file = await sandbox.readFile("/workspace/package.json");

const pkg = JSON.parse(file.content);


// Binary data (auto-detected or forced)

const image = await sandbox.readFile("/tmp/image.png", { encoding: "base64" });


// Force encoding (override MIME detection)

const textAsBase64 = await sandbox.readFile("/workspace/data.txt", {

  encoding: "base64",

});


```

TypeScript

```

const file = await sandbox.readFile('/workspace/package.json');

const pkg = JSON.parse(file.content);


// Binary data (auto-detected or forced)

const image = await sandbox.readFile('/tmp/image.png', { encoding: 'base64' });


// Force encoding (override MIME detection)

const textAsBase64 = await sandbox.readFile('/workspace/data.txt', { encoding: 'base64' });


```

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.

TypeScript

```

const result = await sandbox.exists(path: string): Promise<FileExistsResult>


```

**Parameters**:

* `path` \- Absolute path to check

**Returns**: `Promise<FileExistsResult>` with `exists` boolean

* [  JavaScript ](#tab-panel-6147)
* [  TypeScript ](#tab-panel-6148)

JavaScript

```

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");

}


```

TypeScript

```

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.

TypeScript

```

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`)

* [  JavaScript ](#tab-panel-6139)
* [  TypeScript ](#tab-panel-6140)

JavaScript

```

await sandbox.mkdir("/workspace/src");


// Nested directories

await sandbox.mkdir("/workspace/src/components/ui", { recursive: true });


```

TypeScript

```

await sandbox.mkdir('/workspace/src');


// Nested directories

await sandbox.mkdir('/workspace/src/components/ui', { recursive: true });


```

### `deleteFile()`

Delete a file.

TypeScript

```

await sandbox.deleteFile(path: string): Promise<void>


```

**Parameters**:

* `path` \- Absolute path to the file

* [  JavaScript ](#tab-panel-6137)
* [  TypeScript ](#tab-panel-6138)

JavaScript

```

await sandbox.deleteFile("/workspace/temp.txt");


```

TypeScript

```

await sandbox.deleteFile('/workspace/temp.txt');


```

### `renameFile()`

Rename a file.

TypeScript

```

await sandbox.renameFile(oldPath: string, newPath: string): Promise<void>


```

**Parameters**:

* `oldPath` \- Current file path
* `newPath` \- New file path

* [  JavaScript ](#tab-panel-6141)
* [  TypeScript ](#tab-panel-6142)

JavaScript

```

await sandbox.renameFile("/workspace/draft.txt", "/workspace/final.txt");


```

TypeScript

```

await sandbox.renameFile('/workspace/draft.txt', '/workspace/final.txt');


```

### `moveFile()`

Move a file to a different directory.

TypeScript

```

await sandbox.moveFile(sourcePath: string, destinationPath: string): Promise<void>


```

**Parameters**:

* `sourcePath` \- Current file path
* `destinationPath` \- Destination path

* [  JavaScript ](#tab-panel-6145)
* [  TypeScript ](#tab-panel-6146)

JavaScript

```

await sandbox.moveFile("/tmp/download.txt", "/workspace/data.txt");


```

TypeScript

```

await sandbox.moveFile('/tmp/download.txt', '/workspace/data.txt');


```

### `gitCheckout()`

Clone a git repository.

TypeScript

```

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)

* [  JavaScript ](#tab-panel-6149)
* [  TypeScript ](#tab-panel-6150)

JavaScript

```

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,

});


```

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/api/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/api/files/","name":"Files"}}]}
```

---

---
title: Code Interpreter
description: Execute Python, JavaScript, and TypeScript code with support for data visualizations, tables, and rich output formats. Contexts maintain state (variables, imports, functions) across executions.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/api/interpreter.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Code Interpreter

Execute Python, JavaScript, and TypeScript code with support for data visualizations, tables, and rich output formats. Contexts maintain state (variables, imports, functions) across executions.

## Methods

### `createCodeContext()`

Create a persistent execution context for running code.

TypeScript

```

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`

* [  JavaScript ](#tab-panel-6151)
* [  TypeScript ](#tab-panel-6152)

JavaScript

```

const ctx = await sandbox.createCodeContext({

  language: "python",

  envVars: { API_KEY: env.API_KEY },

});


```

TypeScript

```

const ctx = await sandbox.createCodeContext({

  language: 'python',

  envVars: { API_KEY: env.API_KEY }

});


```

### `runCode()`

Execute code in a context and return the complete result.

TypeScript

```

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**:

* [  JavaScript ](#tab-panel-6153)
* [  TypeScript ](#tab-panel-6154)

JavaScript

```

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"


```

TypeScript

```

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.

* [  JavaScript ](#tab-panel-6159)
* [  TypeScript ](#tab-panel-6160)

JavaScript

```

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"


```

TypeScript

```

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**:

* [  JavaScript ](#tab-panel-6155)
* [  TypeScript ](#tab-panel-6156)

JavaScript

```

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

}


```

TypeScript

```

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.

* [  JavaScript ](#tab-panel-6165)
* [  TypeScript ](#tab-panel-6166)

JavaScript

```

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


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6161)
* [  TypeScript ](#tab-panel-6162)

JavaScript

```

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"


```

TypeScript

```

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.

TypeScript

```

const contexts = await sandbox.listCodeContexts(): Promise<CodeContext[]>


```

* [  JavaScript ](#tab-panel-6157)
* [  TypeScript ](#tab-panel-6158)

JavaScript

```

const contexts = await sandbox.listCodeContexts();

console.log(`Found ${contexts.length} contexts`);


```

TypeScript

```

const contexts = await sandbox.listCodeContexts();

console.log(`Found ${contexts.length} contexts`);


```

### `deleteCodeContext()`

Delete a code execution context and free its resources.

TypeScript

```

await sandbox.deleteCodeContext(contextId: string): Promise<void>


```

* [  JavaScript ](#tab-panel-6163)
* [  TypeScript ](#tab-panel-6164)

JavaScript

```

const ctx = await sandbox.createCodeContext({ language: "python" });

await sandbox.runCode('print("Hello")', { context: ctx });

await sandbox.deleteCodeContext(ctx.id);


```

TypeScript

```

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)**:

* [  JavaScript ](#tab-panel-6169)
* [  TypeScript ](#tab-panel-6170)

JavaScript

```

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" },

  });

}


```

TypeScript

```

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)**:

* [  JavaScript ](#tab-panel-6167)
* [  TypeScript ](#tab-panel-6168)

JavaScript

```

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" },

  });

}


```

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/api/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/api/interpreter/","name":"Code Interpreter"}}]}
```

---

---
title: Lifecycle
description: Create and manage sandbox containers. Get sandbox instances, configure options, and clean up resources.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/api/lifecycle.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Lifecycle

Create and manage sandbox containers. Get sandbox instances, configure options, and clean up resources.

## Methods

### `getSandbox()`

Get or create a sandbox instance by ID.

TypeScript

```

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
* `options` (optional) - See [SandboxOptions](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/) for all available options:  
   * `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.

* [  JavaScript ](#tab-panel-6171)
* [  TypeScript ](#tab-panel-6172)

JavaScript

```

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);

  },

};


```

TypeScript

```

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);

  }

};


```

Warning

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.

TypeScript

```

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.

* [  JavaScript ](#tab-panel-6173)
* [  TypeScript ](#tab-panel-6174)

JavaScript

```

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);


```

TypeScript

```

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.

TypeScript

```

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

* [  JavaScript ](#tab-panel-6175)
* [  TypeScript ](#tab-panel-6176)

JavaScript

```

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();

  }

}


```

TypeScript

```

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 isolated execution contexts within a sandbox

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/api/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/api/lifecycle/","name":"Lifecycle"}}]}
```

---

---
title: Ports
description: Expose services running in your sandbox via public preview URLs. See Preview URLs concept for details.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/api/ports.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Ports

Production requires custom domain

Preview URLs require a custom domain with wildcard DNS routing in production. See [Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/).

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.

TypeScript

```

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.

* [  JavaScript ](#tab-panel-6181)
* [  TypeScript ](#tab-panel-6182)

JavaScript

```

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 });

  },

};


```

TypeScript

```

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.

TypeScript

```

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`

* [  JavaScript ](#tab-panel-6189)
* [  TypeScript ](#tab-panel-6190)

JavaScript

```

// 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",

});


```

TypeScript

```

// 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)

* [  JavaScript ](#tab-panel-6183)
* [  TypeScript ](#tab-panel-6184)

JavaScript

```

// 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


```

TypeScript

```

// 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.

TypeScript

```

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

* [  JavaScript ](#tab-panel-6187)
* [  TypeScript ](#tab-panel-6188)

JavaScript

```

// 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 });

  },

};


```

TypeScript

```

// 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.

TypeScript

```

await sandbox.unexposePort(port: number): Promise<void>


```

**Parameters**:

* `port` \- Port number to unexpose

* [  JavaScript ](#tab-panel-6177)
* [  TypeScript ](#tab-panel-6178)

JavaScript

```

await sandbox.unexposePort(8000);


```

TypeScript

```

await sandbox.unexposePort(8000);


```

### `getExposedPorts()`

Get information about all currently exposed ports.

TypeScript

```

const response = await sandbox.getExposedPorts(): Promise<GetExposedPortsResponse>


```

**Returns**: `Promise<GetExposedPortsResponse>` with `ports` array (containing `port`, `url`, `name`)

* [  JavaScript ](#tab-panel-6179)
* [  TypeScript ](#tab-panel-6180)

JavaScript

```

const { ports } = await sandbox.getExposedPorts();


for (const port of ports) {

  console.log(`${port.name || port.port}: ${port.url}`);

}


```

TypeScript

```

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.

TypeScript

```

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

* [  JavaScript ](#tab-panel-6185)
* [  TypeScript ](#tab-panel-6186)

JavaScript

```

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 });

  },

};


```

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/api/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/api/ports/","name":"Ports"}}]}
```

---

---
title: Sessions
description: Create isolated execution contexts within a sandbox. Each session maintains its own shell state, environment variables, and working directory. See Session management concept for details.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/api/sessions.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Sessions

Create isolated execution contexts within a sandbox. Each session maintains its own shell state, environment variables, and working directory. See [Session management concept](https://developers.cloudflare.com/sandbox/concepts/sessions/) for details.

Note

Every sandbox has a default session that automatically maintains shell state. Create additional sessions when you need isolated shell contexts for different environments or parallel workflows. For sandbox-level operations like creating containers or destroying the entire sandbox, see the [Lifecycle API](https://developers.cloudflare.com/sandbox/api/lifecycle/).

## Methods

### `createSession()`

Create a new isolated execution session.

TypeScript

```

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

* [  JavaScript ](#tab-panel-6197)
* [  TypeScript ](#tab-panel-6198)

JavaScript

```

// Multiple isolated 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


```

TypeScript

```

// Multiple isolated 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.

TypeScript

```

const session = await sandbox.getSession(sessionId: string): Promise<ExecutionSession>


```

**Parameters**:

* `sessionId` \- ID of an existing session

**Returns**: `Promise<ExecutionSession>` bound to the specified session

* [  JavaScript ](#tab-panel-6191)
* [  TypeScript ](#tab-panel-6192)

JavaScript

```

// First request - create session

const session = await sandbox.createSession({ id: "user-123" });

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("user-123");

const result = await session.exec("cd repo && npm run build");


```

TypeScript

```

// First request - create session

const session = await sandbox.createSession({ id: 'user-123' });

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('user-123');

const result = await session.exec('cd repo && npm run build');


```

---

### `deleteSession()`

Delete a session and clean up its resources.

TypeScript

```

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

* [  JavaScript ](#tab-panel-6193)
* [  TypeScript ](#tab-panel-6194)

JavaScript

```

// 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");

}


```

TypeScript

```

// 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');

}


```

Warning

Deleting a session immediately terminates all running commands. The default session cannot be deleted.

---

### `setEnvVars()`

Set environment variables in the sandbox.

TypeScript

```

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

Warning

Call `setEnvVars()` **before** any other sandbox operations to ensure environment variables are available from the start.

* [  JavaScript ](#tab-panel-6195)
* [  TypeScript ](#tab-panel-6196)

JavaScript

```

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");


```

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/api/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/api/sessions/","name":"Sessions"}}]}
```

---

---
title: Storage
description: Mount S3-compatible storage buckets (R2, S3, GCS) into the sandbox filesystem for persistent data access.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/api/storage.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Storage

Mount S3-compatible storage buckets (R2, S3, GCS) into the sandbox filesystem for persistent data access.

## Methods

### `mountBucket()`

Mount an S3-compatible bucket to a local path in the sandbox.

TypeScript

```

await sandbox.mountBucket(

  bucket: string,

  mountPath: string,

  options: MountBucketOptions

): Promise<void>


```

**Parameters**:

* `bucket` \- Bucket name (e.g., `"my-r2-bucket"`)
* `mountPath` \- Local filesystem path to mount at (e.g., `"/data"`)
* `options` \- Mount configuration (see [MountBucketOptions](#mountbucketoptions))

* [  JavaScript ](#tab-panel-6201)
* [  TypeScript ](#tab-panel-6202)

JavaScript

```

// Mount R2 bucket to /data

await sandbox.mountBucket("my-bucket", "/data", {

  endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",

  provider: "r2",

});


// Read/write files directly

const data = await sandbox.readFile("/data/config.json");

await sandbox.writeFile("/data/results.json", JSON.stringify(data));


// Mount with explicit credentials

await sandbox.mountBucket("my-bucket", "/storage", {

  endpoint: "https://s3.amazonaws.com",

  credentials: {

    accessKeyId: env.AWS_ACCESS_KEY_ID,

    secretAccessKey: env.AWS_SECRET_ACCESS_KEY,

  },

});


// Read-only mount

await sandbox.mountBucket("datasets", "/datasets", {

  endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",

  readOnly: true,

});


// Mount a subdirectory within the bucket

await sandbox.mountBucket("shared-bucket", "/user-data", {

  endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",

  prefix: "/users/user-123/",

});


```

TypeScript

```

// Mount R2 bucket to /data

await sandbox.mountBucket('my-bucket', '/data', {

  endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',

  provider: 'r2'

});


// Read/write files directly

const data = await sandbox.readFile('/data/config.json');

await sandbox.writeFile('/data/results.json', JSON.stringify(data));


// Mount with explicit credentials

await sandbox.mountBucket('my-bucket', '/storage', {

  endpoint: 'https://s3.amazonaws.com',

  credentials: {

    accessKeyId: env.AWS_ACCESS_KEY_ID,

    secretAccessKey: env.AWS_SECRET_ACCESS_KEY

  }

});


// Read-only mount

await sandbox.mountBucket('datasets', '/datasets', {

  endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',

  readOnly: true

});


// Mount a subdirectory within the bucket

await sandbox.mountBucket('shared-bucket', '/user-data', {

  endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',

  prefix: '/users/user-123/'

});


```

**Throws**:

* `InvalidMountPointError` \- Invalid mount path or conflicts with existing mounts
* `BucketAccessError` \- Bucket does not exist or insufficient permissions

Authentication

Credentials can be provided via:

1. Explicit `credentials` in options
2. Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
3. Automatic detection from bound R2 buckets

See the [Mount Buckets guide](https://developers.cloudflare.com/sandbox/guides/mount-buckets/) for detailed authentication options.

### `unmountBucket()`

Unmount a previously mounted bucket.

TypeScript

```

await sandbox.unmountBucket(mountPath: string): Promise<void>


```

**Parameters**:

* `mountPath` \- Path where the bucket is mounted (e.g., `"/data"`)

* [  JavaScript ](#tab-panel-6199)
* [  TypeScript ](#tab-panel-6200)

JavaScript

```

// Mount, process, unmount

await sandbox.mountBucket("data", "/data", { endpoint: "..." });

await sandbox.exec("python process.py");


// Unmount

await sandbox.unmountBucket("/data");


```

TypeScript

```

// Mount, process, unmount

await sandbox.mountBucket('data', '/data', { endpoint: '...' });

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`

TypeScript

```

interface MountBucketOptions {

  endpoint?: string;

  localBucket?: boolean;

  provider?: BucketProvider;

  credentials?: BucketCredentials;

  readOnly?: boolean;

  prefix?: string;

  s3fsOptions?: Record<string, string>;

}


```

**Fields**:

* `endpoint` (required when `localBucket` is `false` or unset) - S3-compatible endpoint URL  
   * R2: `'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com'`  
   * S3: `'https://s3.amazonaws.com'`  
   * GCS: `'https://storage.googleapis.com'`
* `localBucket` (optional) - 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  
   * `endpoint` and `credentials` are not required when this is `true`  
         * `provider` and `s3fsOptions` are not used when this is `true`  
   * Default: `false`
* `provider` (optional) - Storage provider hint  
   * Enables provider-specific optimizations  
   * Values: `'r2'`, `'s3'`, `'gcs'`
* `credentials` (optional) - API credentials  
   * Contains `accessKeyId` and `secretAccessKey`  
   * If not provided, uses environment variables
* `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 and end with `/` (e.g., `/data/uploads/`)  
   * Default: Mount entire bucket
* `s3fsOptions` (optional) - Advanced s3fs mount flags  
   * Example: `{ 'use_cache': '/tmp/cache' }`

### `BucketProvider`

Storage provider hint for automatic s3fs flag optimization.

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/api/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/api/storage/","name":"Storage"}}]}
```

---

---
title: Terminal
description: Connect browser-based terminal UIs to sandbox shells via WebSocket.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/api/terminal.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Terminal

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.

## Server-side methods

### `terminal()`

Proxy a WebSocket upgrade request to create a terminal connection.

TypeScript

```

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

* [  JavaScript ](#tab-panel-6203)
* [  TypeScript ](#tab-panel-6204)

JavaScript

```

// In your Worker's fetch handler

return await sandbox.terminal(request, { cols: 120, rows: 30 });


```

TypeScript

```

// 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/):

* [  JavaScript ](#tab-panel-6205)
* [  TypeScript ](#tab-panel-6206)

JavaScript

```

// Default session

return await sandbox.terminal(request);


// Specific session

const session = await sandbox.getSession("dev");

return await session.terminal(request);


```

TypeScript

```

// 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`

TypeScript

```

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

* [  JavaScript ](#tab-panel-6207)
* [  TypeScript ](#tab-panel-6208)

JavaScript

```

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" });


```

TypeScript

```

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.

TypeScript

```

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.

TypeScript

```

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):

```

{ "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:

```

{ "type": "ready" }


```

**Exit** — the shell process has terminated:

```

{ "type": "exit", "code": 0, "signal": "SIGTERM" }


```

**Error** — an error occurred (for example, invalid control message or session not found):

```

{ "type": "error", "message": "Session not found" }


```

## Types

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/api/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/api/terminal/","name":"Terminal"}}]}
```

---

---
title: How-to guides
description: 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.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# How-to guides

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.

[Run background processesStart and manage long-running services and applications.](https://developers.cloudflare.com/sandbox/guides/background-processes/)[Backup and restoreCreate point-in-time backups and restore sandbox directories.](https://developers.cloudflare.com/sandbox/guides/backup-restore/)[Browser terminalsConnect browser-based terminals to sandbox shells using xterm.js or raw WebSockets.](https://developers.cloudflare.com/sandbox/guides/browser-terminals/)[Use code interpreterExecute Python and JavaScript code with rich outputs.](https://developers.cloudflare.com/sandbox/guides/code-execution/)[Run Docker-in-DockerRun Docker commands inside a sandbox container.](https://developers.cloudflare.com/sandbox/guides/docker-in-docker/)[Execute commandsRun commands with streaming output, error handling, and shell access.](https://developers.cloudflare.com/sandbox/guides/execute-commands/)[Expose servicesCreate preview URLs and expose ports for web services.](https://developers.cloudflare.com/sandbox/guides/expose-services/)[Watch filesystem changesMonitor files and directories in real-time to build responsive development tools and automation workflows.](https://developers.cloudflare.com/sandbox/guides/file-watching/)[Work with GitClone repositories, manage branches, and automate Git operations.](https://developers.cloudflare.com/sandbox/guides/git-workflows/)[Manage filesRead, write, organize, and synchronize files in the sandbox.](https://developers.cloudflare.com/sandbox/guides/manage-files/)[Mount bucketsMount S3-compatible object storage as local filesystems for persistent data storage.](https://developers.cloudflare.com/sandbox/guides/mount-buckets/)[Handle outbound trafficIntercept and handle outbound HTTP from sandboxes using Workers.](https://developers.cloudflare.com/sandbox/guides/outbound-traffic/)[Deploy to ProductionSet up custom domains for preview URLs in production.](https://developers.cloudflare.com/sandbox/guides/production-deployment/)[Proxy requests to external APIsKeep credentials secure by routing sandbox requests through a Worker proxy that injects authentication at request time.](https://developers.cloudflare.com/sandbox/guides/proxy-requests/)[Stream outputHandle real-time output from commands and processes.](https://developers.cloudflare.com/sandbox/guides/streaming-output/)[WebSocket ConnectionsConnect to WebSocket servers running in sandboxes.](https://developers.cloudflare.com/sandbox/guides/websocket-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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}}]}
```

---

---
title: Run background processes
description: Start and manage long-running services and applications.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/background-processes.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Run background processes

This guide shows you how to start, monitor, and manage long-running background processes in the sandbox.

## 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

* [  JavaScript ](#tab-panel-6243)
* [  TypeScript ](#tab-panel-6244)

JavaScript

```

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


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6245)
* [  TypeScript ](#tab-panel-6246)

JavaScript

```

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");


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6251)
* [  TypeScript ](#tab-panel-6252)

JavaScript

```

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",

);


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6241)
* [  TypeScript ](#tab-panel-6242)

JavaScript

```

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");


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6247)
* [  TypeScript ](#tab-panel-6248)

JavaScript

```

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);


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6253)
* [  TypeScript ](#tab-panel-6254)

JavaScript

```

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);

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6249)
* [  TypeScript ](#tab-panel-6250)

JavaScript

```

const logs = await sandbox.getProcessLogs(server.id);

console.log("Logs:", logs);


```

TypeScript

```

const logs = await sandbox.getProcessLogs(server.id);

console.log('Logs:', logs);


```

## Stop processes

Stop background processes and their children:

* [  JavaScript ](#tab-panel-6255)
* [  TypeScript ](#tab-panel-6256)

JavaScript

```

// 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();


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6257)
* [  TypeScript ](#tab-panel-6258)

JavaScript

```

// 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);


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6263)
* [  TypeScript ](#tab-panel-6264)

JavaScript

```

// 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");


```

TypeScript

```

// 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):

* [  JavaScript ](#tab-panel-6265)
* [  TypeScript ](#tab-panel-6266)

JavaScript

```

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();

    }

  },

};


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6261)
* [  TypeScript ](#tab-panel-6262)

JavaScript

```

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);

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6259)
* [  TypeScript ](#tab-panel-6260)

JavaScript

```

await sandbox.killAllProcesses();

const server = await sandbox.startProcess("node server.js");


```

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/background-processes/","name":"Run background processes"}}]}
```

---

---
title: Backup and restore
description: Create point-in-time backups and restore sandbox directories.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/backup-restore.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Backup and restore

Create point-in-time snapshots of sandbox directories and restore them using copy-on-write overlays. Backups are stored in an R2 bucket and use squashfs compression.

Production only

Backup and restore does not work with `wrangler dev` because it requires FUSE support that wrangler does not currently provide. Deploy your Worker with `wrangler deploy` to use this feature. All other Sandbox SDK features work in local development.

## Prerequisites

1. Create an R2 bucket for storing backups:  
Terminal window  
```  
npx wrangler r2 bucket create my-backup-bucket  
```
2. Add the `BACKUP_BUCKET` R2 binding and presigned URL credentials to your Wrangler configuration:  
   * [  wrangler.jsonc ](#tab-panel-6267)  
   * [  wrangler.toml ](#tab-panel-6268)  
```  
{  
  "name": "my-sandbox-worker",  
  "main": "src/index.ts",  
  // Set this to today's date  
  "compatibility_date": "2026-04-03",  
  "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",  
    },  
  ],  
}  
```  
```  
name = "my-sandbox-worker"  
main = "src/index.ts"  
# Set this to today's date  
compatibility_date = "2026-04-03"  
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"  
```
3. Set your R2 API credentials as secrets:  
Terminal window  
```  
npx wrangler secret put R2_ACCESS_KEY_ID  
npx wrangler secret put R2_SECRET_ACCESS_KEY  
```  
You can create R2 API tokens 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.

## Create a backup

Use `createBackup()` to snapshot a directory and upload it to R2:

* [  JavaScript ](#tab-panel-6269)
* [  TypeScript ](#tab-panel-6270)

JavaScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Create a backup of /workspace

const backup = await sandbox.createBackup({ dir: "/workspace" });

console.log(`Backup created: ${backup.id}`);


```

TypeScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Create a backup of /workspace

const backup = await sandbox.createBackup({ dir: "/workspace" });

console.log(`Backup created: ${backup.id}`);


```

The SDK creates a compressed squashfs archive of the directory and uploads it directly to your R2 bucket using a presigned URL.

## Restore a backup

Use `restoreBackup()` to restore a directory from a backup:

* [  JavaScript ](#tab-panel-6271)
* [  TypeScript ](#tab-panel-6272)

JavaScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Create a backup

const backup = await sandbox.createBackup({ dir: "/workspace" });


// Restore the backup

const result = await sandbox.restoreBackup(backup);

console.log(`Restored: ${result.success}`);


```

TypeScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Create a backup

const backup = await sandbox.createBackup({ dir: "/workspace" });


// Restore the backup

const result = await sandbox.restoreBackup(backup);

console.log(`Restored: ${result.success}`);


```

Ephemeral mount

The FUSE mount is lost when the sandbox sleeps or the container restarts. Re-restore from the backup handle to recover.

## Exclude gitignored files

When backing up a directory inside a git repository, set `useGitignore: true` to exclude files matching `.gitignore` rules. This is useful for skipping large generated directories like `node_modules/`, `dist/`, or `build/` that can be recreated.

* [  JavaScript ](#tab-panel-6273)
* [  TypeScript ](#tab-panel-6274)

JavaScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Back up only tracked and untracked non-ignored files

const backup = await sandbox.createBackup({

  dir: "/workspace",

  useGitignore: true,

});


```

TypeScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Back up only tracked and untracked non-ignored files

const backup = await sandbox.createBackup({

  dir: "/workspace",

  useGitignore: true,

});


```

The SDK uses `git ls-files` to resolve which files are ignored. Both root-level and nested `.gitignore` files are respected.

By default, `useGitignore` is `false` and all files in the directory are included in the backup.

Requirements

`useGitignore` requires `git` to be installed in the container. If `useGitignore` is `true` and `git` is not available, `createBackup()` throws a `BackupCreateError`. If the backup directory is not inside a git repository, the option has no effect and all files are included.

## Checkpoint and rollback

Save state before risky operations and restore if something fails:

* [  JavaScript ](#tab-panel-6277)
* [  TypeScript ](#tab-panel-6278)

JavaScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Save checkpoint before risky operation

const checkpoint = await sandbox.createBackup({ dir: "/workspace" });


try {

  await sandbox.exec("npm install some-experimental-package");

  await sandbox.exec("npm run build");

} catch (error) {

  // Restore to checkpoint if something goes wrong

  await sandbox.restoreBackup(checkpoint);

  console.log("Rolled back to checkpoint");

}


```

TypeScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Save checkpoint before risky operation

const checkpoint = await sandbox.createBackup({ dir: "/workspace" });


try {

  await sandbox.exec("npm install some-experimental-package");

  await sandbox.exec("npm run build");

} catch (error) {

  // Restore to checkpoint if something goes wrong

  await sandbox.restoreBackup(checkpoint);

  console.log("Rolled back to checkpoint");

}


```

## Store backup handles

The `DirectoryBackup` handle is serializable. Persist it to KV, D1, or Durable Object storage for later use:

* [  JavaScript ](#tab-panel-6281)
* [  TypeScript ](#tab-panel-6282)

JavaScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Create a backup and store the handle in KV

const backup = await sandbox.createBackup({

  dir: "/workspace",

  name: "deploy-v2",

  ttl: 604800, // 7 days

});


await env.KV.put(`backup:${userId}`, JSON.stringify(backup));


// Later, retrieve and restore

const stored = await env.KV.get(`backup:${userId}`);

if (stored) {

  const backupHandle = JSON.parse(stored);

  await sandbox.restoreBackup(backupHandle);

}


```

TypeScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Create a backup and store the handle in KV

const backup = await sandbox.createBackup({

  dir: "/workspace",

  name: "deploy-v2",

  ttl: 604800, // 7 days

});


await env.KV.put(`backup:${userId}`, JSON.stringify(backup));


// Later, retrieve and restore

const stored = await env.KV.get(`backup:${userId}`);

if (stored) {

  const backupHandle = JSON.parse(stored);

  await sandbox.restoreBackup(backupHandle);

}


```

## Use named backups

Add a `name` option to identify backups. Names can be up to 256 characters:

* [  JavaScript ](#tab-panel-6275)
* [  TypeScript ](#tab-panel-6276)

JavaScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


const backup = await sandbox.createBackup({

  dir: "/workspace",

  name: "before-migration",

});


console.log(`Backup ID: ${backup.id}`);


```

TypeScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


const backup = await sandbox.createBackup({

  dir: "/workspace",

  name: "before-migration",

});


console.log(`Backup ID: ${backup.id}`);


```

## Configure TTL

Set a custom time-to-live for backups. The default TTL is 3 days (259200 seconds). The `ttl` value must be a positive number of seconds:

* [  JavaScript ](#tab-panel-6283)
* [  TypeScript ](#tab-panel-6284)

JavaScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Short-lived backup for a quick operation

const shortBackup = await sandbox.createBackup({

  dir: "/workspace",

  ttl: 600, // 10 minutes

});


// Long-lived backup for extended workflows

const longBackup = await sandbox.createBackup({

  dir: "/workspace",

  name: "daily-snapshot",

  ttl: 604800, // 7 days

});


```

TypeScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Short-lived backup for a quick operation

const shortBackup = await sandbox.createBackup({

  dir: "/workspace",

  ttl: 600, // 10 minutes

});


// Long-lived backup for extended workflows

const longBackup = await sandbox.createBackup({

  dir: "/workspace",

  name: "daily-snapshot",

  ttl: 604800, // 7 days

});


```

### How TTL is enforced

The TTL is enforced at **restore time**, not at creation time. When you call `restoreBackup()`, the SDK reads the backup metadata from R2 and compares the creation timestamp plus TTL against the current time (with a 60-second buffer to prevent race conditions). If the TTL has elapsed, the restore is rejected with a `BACKUP_EXPIRED` error.

The TTL does **not** automatically delete backup objects from R2\. Expired backups remain in your bucket and continue to consume storage until you explicitly delete them or configure an automatic cleanup rule.

### Configure R2 lifecycle rules for automatic cleanup

To automatically remove expired backup objects from R2, set up an [R2 object lifecycle rule](https://developers.cloudflare.com/r2/buckets/object-lifecycles/) on your backup bucket. This is the recommended way to prevent expired backups from accumulating indefinitely.

For example, if your longest TTL is 7 days, configure a lifecycle rule to delete objects older than 7 days from the `backups/` prefix. This ensures R2 storage does not grow unbounded while giving you a buffer to restore any non-expired backup.

## Clean up backup objects in R2

Backup archives are stored in your R2 bucket under the `backups/` prefix with the structure `backups/{backupId}/data.sqsh` and `backups/{backupId}/meta.json`. You can use the `BACKUP_BUCKET` R2 binding to manage these objects directly.

### Replace the latest backup (delete-then-write)

If you only need the most recent backup, delete the previous one before creating a new one:

* [  JavaScript ](#tab-panel-6285)
* [  TypeScript ](#tab-panel-6286)

JavaScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Delete the previous backup's R2 objects before creating a new one

if (previousBackup) {

  await env.BACKUP_BUCKET.delete(`backups/${previousBackup.id}/data.sqsh`);

  await env.BACKUP_BUCKET.delete(`backups/${previousBackup.id}/meta.json`);

}


// Create a fresh backup

const backup = await sandbox.createBackup({

  dir: "/workspace",

  name: "latest",

});


// Store the handle so you can delete it next time

await env.KV.put("latest-backup", JSON.stringify(backup));


```

TypeScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Delete the previous backup's R2 objects before creating a new one

if (previousBackup) {

  await env.BACKUP_BUCKET.delete(`backups/${previousBackup.id}/data.sqsh`);

  await env.BACKUP_BUCKET.delete(`backups/${previousBackup.id}/meta.json`);

}


// Create a fresh backup

const backup = await sandbox.createBackup({

  dir: "/workspace",

  name: "latest",

});


// Store the handle so you can delete it next time

await env.KV.put("latest-backup", JSON.stringify(backup));


```

### List and delete old backups by prefix

To clean up multiple old backups, list objects under the `backups/` prefix and delete them by key:

* [  JavaScript ](#tab-panel-6287)
* [  TypeScript ](#tab-panel-6288)

JavaScript

```

// List all backup objects in the bucket

const listed = await env.BACKUP_BUCKET.list({ prefix: "backups/" });


for (const object of listed.objects) {

  // Parse the backup ID from the key (backups/{id}/data.sqsh or backups/{id}/meta.json)

  const parts = object.key.split("/");

  const backupId = parts[1];


  // Delete objects older than 7 days

  const ageMs = Date.now() - object.uploaded.getTime();

  const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;

  if (ageMs > sevenDaysMs) {

    await env.BACKUP_BUCKET.delete(object.key);

    console.log(`Deleted expired object: ${object.key}`);

  }

}


```

TypeScript

```

// List all backup objects in the bucket

const listed = await env.BACKUP_BUCKET.list({ prefix: "backups/" });


for (const object of listed.objects) {

  // Parse the backup ID from the key (backups/{id}/data.sqsh or backups/{id}/meta.json)

  const parts = object.key.split("/");

  const backupId = parts[1];


  // Delete objects older than 7 days

  const ageMs = Date.now() - object.uploaded.getTime();

  const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;

  if (ageMs > sevenDaysMs) {

    await env.BACKUP_BUCKET.delete(object.key);

    console.log(`Deleted expired object: ${object.key}`);

  }

}


```

### Delete a specific backup by ID

If you have the backup ID, delete both its archive and metadata directly:

* [  JavaScript ](#tab-panel-6279)
* [  TypeScript ](#tab-panel-6280)

JavaScript

```

const backupId = backup.id;


await env.BACKUP_BUCKET.delete(`backups/${backupId}/data.sqsh`);

await env.BACKUP_BUCKET.delete(`backups/${backupId}/meta.json`);


```

TypeScript

```

const backupId = backup.id;


await env.BACKUP_BUCKET.delete(`backups/${backupId}/data.sqsh`);

await env.BACKUP_BUCKET.delete(`backups/${backupId}/meta.json`);


```

## Copy-on-write behavior

Restore uses FUSE overlayfs to mount the backup as a read-only lower layer. New writes go to a writable upper layer and do not affect the original backup:

* [  JavaScript ](#tab-panel-6289)
* [  TypeScript ](#tab-panel-6290)

JavaScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Create a backup

const backup = await sandbox.createBackup({ dir: "/workspace" });


// Restore the backup

await sandbox.restoreBackup(backup);


// New writes go to the upper layer — the backup is unchanged

await sandbox.writeFile(

  "/workspace/new-file.txt",

  "This does not modify the backup",

);


// Restore the same backup again to discard changes

await sandbox.restoreBackup(backup);


```

TypeScript

```

const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Create a backup

const backup = await sandbox.createBackup({ dir: "/workspace" });


// Restore the backup

await sandbox.restoreBackup(backup);


// New writes go to the upper layer — the backup is unchanged

await sandbox.writeFile(

  "/workspace/new-file.txt",

  "This does not modify the backup",

);


// Restore the same backup again to discard changes

await sandbox.restoreBackup(backup);


```

## Handle errors

Backup and restore operations can throw specific errors. Wrap calls in [try...catch ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch) blocks:

* [  JavaScript ](#tab-panel-6291)
* [  TypeScript ](#tab-panel-6292)

JavaScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Handle backup errors

try {

  const backup = await sandbox.createBackup({ dir: "/workspace" });

} catch (error) {

  if (error.code === "INVALID_BACKUP_CONFIG") {

    // Missing BACKUP_BUCKET binding or invalid directory path

    console.error("Configuration error:", error.message);

  } else if (error.code === "BACKUP_CREATE_FAILED") {

    // Archive creation or upload to R2 failed

    console.error("Backup failed:", error.message);

  }

}


// Handle restore errors

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);

  }

}


```

TypeScript

```

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


const sandbox = getSandbox(env.Sandbox, "my-sandbox");


// Handle backup errors

try {

  const backup = await sandbox.createBackup({ dir: "/workspace" });

} catch (error) {

  if (error.code === "INVALID_BACKUP_CONFIG") {

    // Missing BACKUP_BUCKET binding or invalid directory path

    console.error("Configuration error:", error.message);

  } else if (error.code === "BACKUP_CREATE_FAILED") {

    // Archive creation or upload to R2 failed

    console.error("Backup failed:", error.message);

  }

}


// Handle restore errors

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);

  }

}


```

## Path permissions

The `createBackup()` method uses `mksquashfs` to create a compressed archive of the target directory. This process must be able to read every file and subdirectory within the path you are backing up. If any file or directory has restrictive permissions that prevent the archiver from reading it, the backup fails with a `BackupCreateError` and a "Permission denied" message.

### Common causes

* **Directories owned by other users** — If the target directory contains subdirectories created by a different user or process (for example, `/home/sandbox/.claude`), the archiver may not have read access.
* **Restrictive file modes** — Files with modes like `0600` or directories with `0700` that belong to a different user than the one running the backup process.
* **Runtime-generated config directories** — Tools and applications often create configuration directories (such as `.cache`, `.config`, or tool-specific dotfiles) with restrictive permissions.

### Fix permissions at build time

The recommended approach is to set permissions in your Dockerfile so that every container starts with the correct access. This avoids running `chmod` at runtime before every backup:

```

# Ensure the backup target directory is readable

RUN mkdir -p /home/sandbox && chmod -R a+rX /home/sandbox


```

The `a+rX` flag grants read access to all files and execute (traverse) access to all directories, without changing write permissions.

### Fix permissions at runtime

If the restrictive permissions come from files created at runtime (for example, a tool that generates config files with `0600` mode), fix them before calling `createBackup()`:

TypeScript

```

await sandbox.exec("chmod -R a+rX /home/sandbox/.claude");

const backup = await sandbox.createBackup({ dir: "/home/sandbox" });


```

### Example error

If the backup encounters a permission issue, you will see an error like:

```

BackupCreateError: mksquashfs failed: Could not create destination file: Permission denied


```

This means `mksquashfs` could not read one or more files inside the directory you passed to `createBackup()`. Check the permissions of all files and subdirectories within that path.

## Best practices

* **Stop writes before restoring** \- Stop processes writing to the target directory before calling `restoreBackup()`
* **Use checkpoints** \- Create backups before risky operations like package installations or migrations
* **Exclude gitignored files** \- Set `useGitignore: true` when backing up git repositories to skip generated files like `node_modules/` and reduce backup size
* **Set appropriate TTLs** \- Use short TTLs for temporary checkpoints and longer TTLs for persistent snapshots
* **Store handles externally** \- Persist `DirectoryBackup` handles to KV, D1, or Durable Object storage for cross-request access
* **Configure R2 lifecycle rules** \- Set up [object lifecycle rules](https://developers.cloudflare.com/r2/buckets/object-lifecycles/) to automatically delete expired backups from R2, since TTL is only enforced at restore time
* **Clean up old backups** \- Delete previous backup objects from R2 when you no longer need them, or use the delete-then-write pattern for rolling backups
* **Handle errors** \- Wrap backup and restore calls in `try...catch` blocks
* **Re-restore after restart** \- The FUSE mount is ephemeral, so re-restore from the backup handle after container restarts

## Related resources

* [Backups API reference](https://developers.cloudflare.com/sandbox/api/backups/) \- Full method documentation
* [Storage API reference](https://developers.cloudflare.com/sandbox/api/storage/) \- Mount S3-compatible buckets
* [R2 documentation](https://developers.cloudflare.com/r2/) \- Learn about Cloudflare R2
* [R2 lifecycle rules](https://developers.cloudflare.com/r2/buckets/object-lifecycles/) \- Configure automatic object cleanup

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/backup-restore/","name":"Backup and restore"}}]}
```

---

---
title: Browser terminals
description: Connect browser-based terminals to sandbox shells using xterm.js or raw WebSockets.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/browser-terminals.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Browser terminals

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.

## 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:

 npm  yarn  pnpm  bun 

```
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:

* [  JavaScript ](#tab-panel-6293)
* [  TypeScript ](#tab-panel-6294)

JavaScript

```

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 });

  },

};


```

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);


    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.

* [  JavaScript ](#tab-panel-6295)
* [  TypeScript ](#tab-panel-6296)

JavaScript

```

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());


```

TypeScript

```

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.

* [  JavaScript ](#tab-panel-6297)
* [  TypeScript ](#tab-panel-6298)

JavaScript

```

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));

  }

}


```

TypeScript

```

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.
* **Use sessions for isolation** — If users need separate shell environments, create sessions with different working directories and environment variables.

## 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/browser-terminals/","name":"Browser terminals"}}]}
```

---

---
title: Use code interpreter
description: Execute Python and JavaScript code with rich outputs.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/code-execution.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Use code interpreter

This guide shows you how to execute Python and JavaScript code with rich outputs using the Code Interpreter API.

## 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:

* [  JavaScript ](#tab-panel-6299)
* [  TypeScript ](#tab-panel-6300)

JavaScript

```

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",

});


```

TypeScript

```

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

* [  JavaScript ](#tab-panel-6301)
* [  TypeScript ](#tab-panel-6302)

JavaScript

```

// 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);


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6307)
* [  TypeScript ](#tab-panel-6308)

JavaScript

```

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"


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6311)
* [  TypeScript ](#tab-panel-6312)

JavaScript

```

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);

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6309)
* [  TypeScript ](#tab-panel-6310)

JavaScript

```

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);

    },

  },

);


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6313)
* [  TypeScript ](#tab-panel-6314)

JavaScript

```

// 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);


```

TypeScript

```

// 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

* [  JavaScript ](#tab-panel-6303)
* [  TypeScript ](#tab-panel-6304)

JavaScript

```

const contexts = await sandbox.listCodeContexts();


console.log(`${contexts.length} active contexts:`);


for (const ctx of contexts) {

  console.log(`  ${ctx.id} (${ctx.language})`);

}


```

TypeScript

```

const contexts = await sandbox.listCodeContexts();


console.log(`${contexts.length} active contexts:`);


for (const ctx of contexts) {

  console.log(`  ${ctx.id} (${ctx.language})`);

}


```

### Delete contexts

* [  JavaScript ](#tab-panel-6305)
* [  TypeScript ](#tab-panel-6306)

JavaScript

```

// 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");


```

TypeScript

```

// 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/code-execution/","name":"Use code interpreter"}}]}
```

---

---
title: Run Docker-in-Docker
description: Run Docker commands inside a sandbox container.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/docker-in-docker.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Run Docker-in-Docker

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:

* [  JavaScript ](#tab-panel-6315)
* [  TypeScript ](#tab-panel-6316)

JavaScript

```

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!"


```

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/docker-in-docker/","name":"Run Docker-in-Docker"}}]}
```

---

---
title: Execute commands
description: Run commands with streaming output, error handling, and shell access.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/execute-commands.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Execute commands

This guide shows you how to execute commands in the sandbox, handle output, and manage errors effectively.

## 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:

* [  JavaScript ](#tab-panel-6317)
* [  TypeScript ](#tab-panel-6318)

JavaScript

```

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


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6319)
* [  TypeScript ](#tab-panel-6320)

JavaScript

```

// 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");


```

TypeScript

```

// 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)

* [  JavaScript ](#tab-panel-6333)
* [  TypeScript ](#tab-panel-6334)

JavaScript

```

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;

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6323)
* [  TypeScript ](#tab-panel-6324)

JavaScript

```

// 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");


```

TypeScript

```

// 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

* [  JavaScript ](#tab-panel-6331)
* [  TypeScript ](#tab-panel-6332)

JavaScript

```

// 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");


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6321)
* [  TypeScript ](#tab-panel-6322)

JavaScript

```

const result = await sandbox.exec("npm run build", {

  timeout: 30000, // 30 seconds

});


```

TypeScript

```

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`:

* [  JavaScript ](#tab-panel-6327)
* [  TypeScript ](#tab-panel-6328)

JavaScript

```

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


```

TypeScript

```

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

Warning

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:

* [  JavaScript ](#tab-panel-6325)
* [  TypeScript ](#tab-panel-6326)

JavaScript

```

const check = await sandbox.exec("which python3");

if (!check.success) {

  console.error("python3 not found");

}


```

TypeScript

```

const check = await sandbox.exec('which python3');

if (!check.success) {

  console.error('python3 not found');

}


```

### Working directory issues

Use absolute paths or change directory:

* [  JavaScript ](#tab-panel-6329)
* [  TypeScript ](#tab-panel-6330)

JavaScript

```

// 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");


```

TypeScript

```

// 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/execute-commands/","name":"Execute commands"}}]}
```

---

---
title: Expose services
description: Create preview URLs and expose ports for web services.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/expose-services.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Expose services

Production requires custom domain

Preview URLs require a custom domain with wildcard DNS routing in production. See [Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/) for setup instructions.

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`.

* [  JavaScript ](#tab-panel-6355)
* [  TypeScript ](#tab-panel-6356)

JavaScript

```

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 });

  },

};


```

TypeScript

```

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 });

  }

};


```

Warning

**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:

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6337)
* [  TypeScript ](#tab-panel-6338)

JavaScript

```

// 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,

});


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6353)
* [  TypeScript ](#tab-panel-6354)

JavaScript

```

// 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);


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6335)
* [  TypeScript ](#tab-panel-6336)

JavaScript

```

// 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 });


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6349)
* [  TypeScript ](#tab-panel-6350)

JavaScript

```

// 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 });


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6357)
* [  TypeScript ](#tab-panel-6358)

JavaScript

```

// 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,

});


```

TypeScript

```

// 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

* [  JavaScript ](#tab-panel-6341)
* [  TypeScript ](#tab-panel-6342)

JavaScript

```

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}`);

  }

}


```

TypeScript

```

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

* [  JavaScript ](#tab-panel-6339)
* [  TypeScript ](#tab-panel-6340)

JavaScript

```

// Unexpose a single port

await sandbox.unexposePort(8000);


// Unexpose multiple ports

for (const port of [3000, 5173, 8080]) {

  await sandbox.unexposePort(port);

}


```

TypeScript

```

// 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:

wrangler.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:

* [  JavaScript ](#tab-panel-6345)
* [  TypeScript ](#tab-panel-6346)

JavaScript

```

// 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 });


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6343)
* [  TypeScript ](#tab-panel-6344)

JavaScript

```

// 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 });


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6351)
* [  TypeScript ](#tab-panel-6352)

JavaScript

```

// 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 });

}


```

TypeScript

```

// 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**:

* [  JavaScript ](#tab-panel-6347)
* [  TypeScript ](#tab-panel-6348)

JavaScript

```

// Create sandbox with normalization

const sandbox = getSandbox(env.Sandbox, "MyProject-123", { normalizeId: true });

await sandbox.exposePort(8080, { hostname });


```

TypeScript

```

// 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/expose-services/","name":"Expose services"}}]}
```

---

---
title: Watch filesystem changes
description: Monitor files and directories in real-time to build responsive development tools and automation workflows.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/file-watching.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Watch filesystem changes

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:

* [  JavaScript ](#tab-panel-6359)
* [  TypeScript ](#tab-panel-6360)

JavaScript

```

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}`);

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6361)
* [  TypeScript ](#tab-panel-6362)

JavaScript

```

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}`);

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6363)
* [  TypeScript ](#tab-panel-6364)

JavaScript

```

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}`);

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6375)
* [  TypeScript ](#tab-panel-6376)

JavaScript

```

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;

    }

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6365)
* [  TypeScript ](#tab-panel-6366)

JavaScript

```

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");

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6369)
* [  TypeScript ](#tab-panel-6370)

JavaScript

```

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;

    }

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6385)
* [  TypeScript ](#tab-panel-6386)

JavaScript

```

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}`);

  },

);


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6379)
* [  TypeScript ](#tab-panel-6380)

JavaScript

```

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);

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6367)
* [  TypeScript ](#tab-panel-6368)

JavaScript

```

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");

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6373)
* [  TypeScript ](#tab-panel-6374)

JavaScript

```

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");


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6381)
* [  TypeScript ](#tab-panel-6382)

JavaScript

```

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");


```

TypeScript

```

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.

* [  JavaScript ](#tab-panel-6377)
* [  TypeScript ](#tab-panel-6378)

JavaScript

```

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

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6383)
* [  TypeScript ](#tab-panel-6384)

JavaScript

```

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);

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6371)
* [  TypeScript ](#tab-panel-6372)

JavaScript

```

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"],

});


```

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/file-watching/","name":"Watch filesystem changes"}}]}
```

---

---
title: Work with Git
description: Clone repositories, manage branches, and automate Git operations.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/git-workflows.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Work with Git

This guide shows you how to clone repositories, manage branches, and automate Git operations in the sandbox.

## Clone repositories

* [  JavaScript ](#tab-panel-6397)
* [  TypeScript ](#tab-panel-6398)

JavaScript

```

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",

});


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6387)
* [  TypeScript ](#tab-panel-6388)

JavaScript

```

const token = env.GITHUB_TOKEN;

const repoUrl = `https://${token}@github.com/user/private-repo.git`;


await sandbox.gitCheckout(repoUrl);


```

TypeScript

```

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 a Worker proxy that validates a short-lived JWT and injects the real token at request time — the sandbox never holds the credential. Refer to [Proxy requests to external APIs](https://developers.cloudflare.com/sandbox/guides/proxy-requests/).

## Clone and build

Clone a repository and run build steps:

* [  JavaScript ](#tab-panel-6389)
* [  TypeScript ](#tab-panel-6390)

JavaScript

```

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");


```

TypeScript

```

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

* [  JavaScript ](#tab-panel-6391)
* [  TypeScript ](#tab-panel-6392)

JavaScript

```

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");


```

TypeScript

```

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

* [  JavaScript ](#tab-panel-6399)
* [  TypeScript ](#tab-panel-6400)

JavaScript

```

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"');


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6395)
* [  TypeScript ](#tab-panel-6396)

JavaScript

```

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);


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6393)
* [  TypeScript ](#tab-panel-6394)

JavaScript

```

await sandbox.gitCheckout("https://github.com/user/large-repo", {

  depth: 1,

});


```

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/git-workflows/","name":"Work with Git"}}]}
```

---

---
title: Manage files
description: Read, write, organize, and synchronize files in the sandbox.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/manage-files.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Manage files

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

* [  JavaScript ](#tab-panel-6401)
* [  TypeScript ](#tab-panel-6402)

JavaScript

```

// 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


```

TypeScript

```

// 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

* [  JavaScript ](#tab-panel-6409)
* [  TypeScript ](#tab-panel-6410)

JavaScript

```

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" });


```

TypeScript

```

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

* [  JavaScript ](#tab-panel-6415)
* [  TypeScript ](#tab-panel-6416)

JavaScript

```

// 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

const imageFile = await sandbox.readFile("/workspace/image.png", {

  encoding: "base64",

});

return new Response(atob(imageFile.content), {

  headers: { "Content-Type": "image/png" },

});


// Force encoding for transmission (text → base64)

const textAsBase64 = await sandbox.readFile("/workspace/data.txt", {

  encoding: "base64",

});

// Useful for transmitting text files without encoding issues


```

TypeScript

```

// 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

const imageFile = await sandbox.readFile('/workspace/image.png', { encoding: 'base64' });

return new Response(atob(imageFile.content), {

  headers: { 'Content-Type': 'image/png' }

});


// Force encoding for transmission (text → base64)

const textAsBase64 = await sandbox.readFile('/workspace/data.txt', { encoding: 'base64' });

// Useful for transmitting text files without encoding issues


```

## Organize files

* [  JavaScript ](#tab-panel-6405)
* [  TypeScript ](#tab-panel-6406)

JavaScript

```

// 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");


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6407)
* [  TypeScript ](#tab-panel-6408)

JavaScript

```

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),

  ),

);


```

TypeScript

```

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

* [  JavaScript ](#tab-panel-6413)
* [  TypeScript ](#tab-panel-6414)

JavaScript

```

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");


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6403)
* [  TypeScript ](#tab-panel-6404)

JavaScript

```

// Create directory, then write file

await sandbox.mkdir("/workspace/data", { recursive: true });

await sandbox.writeFile("/workspace/data/file.txt", content);


```

TypeScript

```

// Create directory, then write file

await sandbox.mkdir('/workspace/data', { recursive: true });

await sandbox.writeFile('/workspace/data/file.txt', content);


```

### Binary file encoding

Use base64 for binary files:

* [  JavaScript ](#tab-panel-6411)
* [  TypeScript ](#tab-panel-6412)

JavaScript

```

// Write binary

await sandbox.writeFile("/workspace/image.png", base64Data, {

  encoding: "base64",

});


// Read binary

const file = await sandbox.readFile("/workspace/image.png", {

  encoding: "base64",

});


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6417)
* [  TypeScript ](#tab-panel-6418)

JavaScript

```

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");

  }

}


```

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/manage-files/","name":"Manage files"}}]}
```

---

---
title: Mount buckets
description: Mount S3-compatible object storage as local filesystems for persistent data storage.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/mount-buckets.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Mount buckets

Mount S3-compatible object storage buckets as local filesystem paths. Access object storage using standard file operations.

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.

## 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

* [  JavaScript ](#tab-panel-6435)
* [  TypeScript ](#tab-panel-6436)

JavaScript

```

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


const sandbox = getSandbox(env.Sandbox, "data-processor");


// Mount R2 bucket

await sandbox.mountBucket("my-r2-bucket", "/data", {

  endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",

});


// 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

await sandbox.mountBucket('my-r2-bucket', '/data', {

endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com'

});


// 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')

`] });


```

Mounting affects entire sandbox

Mounted buckets are visible across all sessions since they share the filesystem. Mount once per sandbox.

## Credentials

### Automatic detection

Set credentials as Worker secrets and the SDK automatically detects them:

Terminal window

```

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.

* [  JavaScript ](#tab-panel-6421)
* [  TypeScript ](#tab-panel-6422)

JavaScript

```

// Credentials automatically detected from environment

await sandbox.mountBucket("my-r2-bucket", "/data", {

  endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",

});


```

TypeScript

```

// Credentials automatically detected from environment

await sandbox.mountBucket('my-r2-bucket', '/data', {

  endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com'

});


```

### Explicit credentials

Pass credentials directly when needed:

* [  JavaScript ](#tab-panel-6423)
* [  TypeScript ](#tab-panel-6424)

JavaScript

```

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

  }

});


```

## 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:

* [  JavaScript ](#tab-panel-6445)
* [  TypeScript ](#tab-panel-6446)

JavaScript

```

// Mount only the /uploads/images/ subdirectory

await sandbox.mountBucket("my-bucket", "/images", {

  endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",

  prefix: "/uploads/images/",

});


// Files appear at mount point without the prefix

// Bucket: my-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-bucket:/uploads/images/photo.jpg


// Mount different prefixes to different paths

await sandbox.mountBucket("datasets", "/training-data", {

  endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",

  prefix: "/ml/training/",

});


await sandbox.mountBucket("datasets", "/test-data", {

  endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",

  prefix: "/ml/testing/",

});


```

TypeScript

```

// Mount only the /uploads/images/ subdirectory

await sandbox.mountBucket('my-bucket', '/images', {

  endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',

  prefix: '/uploads/images/'

});


// Files appear at mount point without the prefix

// Bucket: my-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-bucket:/uploads/images/photo.jpg


// Mount different prefixes to different paths

await sandbox.mountBucket('datasets', '/training-data', {

endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',

prefix: '/ml/training/'

});


await sandbox.mountBucket('datasets', '/test-data', {

endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',

prefix: '/ml/testing/'

});


```

Prefix format

The `prefix` must start and end with `/` (e.g., `/data/`, `/logs/2024/`). This is required by the underlying s3fs tool.

## Read-only mounts

Protect data by mounting buckets in read-only mode:

* [  JavaScript ](#tab-panel-6427)
* [  TypeScript ](#tab-panel-6428)

JavaScript

```

await sandbox.mountBucket("dataset-bucket", "/data", {

  endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",

  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('dataset-bucket', '/data', {

  endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',

  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 mount R2 buckets during local development with `wrangler dev` by passing the `localBucket` option. This uses the R2 binding from your Worker environment directly, so no S3-compatible endpoint or credentials are required.

### Configure R2 bindings

Add an R2 bucket binding to your Wrangler configuration:

* [  wrangler.jsonc ](#tab-panel-6419)
* [  wrangler.toml ](#tab-panel-6420)

```

{

  "r2_buckets": [

    {

      "binding": "MY_BUCKET",

      "bucket_name": "my-test-bucket"

    }

  ]

}


```

```

[[r2_buckets]]

binding = "MY_BUCKET"

bucket_name = "my-test-bucket"


```

### Mount with `localBucket`

Pass `localBucket: true` in the options to mount the bucket locally:

* [  JavaScript ](#tab-panel-6425)
* [  TypeScript ](#tab-panel-6426)

JavaScript

```

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

```

await sandbox.mountBucket('MY_BUCKET', '/data', {

  localBucket: Boolean(env.LOCAL_DEV),

  endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com'

});


```

When `localBucket` is `true`, the `endpoint` is ignored and the SDK uses the R2 binding directly. 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:

* [  JavaScript ](#tab-panel-6431)
* [  TypeScript ](#tab-panel-6432)

JavaScript

```

// 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.

## Unmount buckets

* [  JavaScript ](#tab-panel-6429)
* [  TypeScript ](#tab-panel-6430)

JavaScript

```

// Mount for processing

await sandbox.mountBucket("my-bucket", "/data", { endpoint: "..." });


// 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', { endpoint: '...' });


// 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

* [  JavaScript ](#tab-panel-6433)
* [  TypeScript ](#tab-panel-6434)

JavaScript

```

await sandbox.mountBucket("my-s3-bucket", "/data", {

  endpoint: "https://s3.us-west-2.amazonaws.com", // Regional endpoint

  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',  // Regional endpoint

  credentials: {

    accessKeyId: env.AWS_ACCESS_KEY_ID,

    secretAccessKey: env.AWS_SECRET_ACCESS_KEY

  }

});


```

### Google Cloud Storage

* [  JavaScript ](#tab-panel-6437)
* [  TypeScript ](#tab-panel-6438)

JavaScript

```

await sandbox.mountBucket("my-gcs-bucket", "/data", {

  endpoint: "https://storage.googleapis.com",

  credentials: {

    accessKeyId: env.GCS_ACCESS_KEY_ID, // HMAC key

    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,  // HMAC key

    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:

* [  JavaScript ](#tab-panel-6439)
* [  TypeScript ](#tab-panel-6440)

JavaScript

```

await sandbox.mountBucket("my-bucket", "/data", {

  endpoint: "https://s3.us-west-000.backblazeb2.com", // Provider-specific endpoint

  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',  // Provider-specific endpoint

  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) which documents supported providers and their recommended flags.

## Troubleshooting

### Missing credentials error

**Error**: `MissingCredentialsError: No credentials found`

**Solution**: Set credentials as Worker secrets:

Terminal window

```

npx wrangler secret put R2_ACCESS_KEY_ID

npx wrangler secret put R2_SECRET_ACCESS_KEY


```

or

Terminal window

```

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
* Bucket doesn't exist
* Network connectivity issues

Verify your endpoint format and credentials:

* [  JavaScript ](#tab-panel-6443)
* [  TypeScript ](#tab-panel-6444)

JavaScript

```

try {

  await sandbox.mountBucket("my-bucket", "/data", {

    endpoint: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",

  });

} catch (error) {

  console.error("Mount failed:", error.message);

  // Check endpoint format, credentials, bucket existence

}


```

TypeScript

```

try {

  await sandbox.mountBucket('my-bucket', '/data', {

    endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com'

  });

} catch (error) {

  console.error('Mount failed:', error.message);

  // Check endpoint format, credentials, bucket existence

}


```

### Path already mounted error

**Error**: `InvalidMountConfigError: Mount path already in use`

**Solution**: Unmount first or use a different path:

* [  JavaScript ](#tab-panel-6441)
* [  TypeScript ](#tab-panel-6442)

JavaScript

```

// 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:

* [  JavaScript ](#tab-panel-6447)
* [  TypeScript ](#tab-panel-6448)

JavaScript

```

// 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
* **Use R2 for Cloudflare** \- Zero egress fees and optimized configuration
* **Secure credentials** \- Always use Worker secrets, never hardcode
* **Read-only when possible** \- Protect data with read-only mounts
* **Use prefixes for isolation** \- Mount subdirectories when working with specific datasets
* **Mount paths** \- Use `/data`, `/storage`, or `/mnt/*` (avoid `/workspace`, `/tmp`)
* **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
* [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
* [R2 documentation](https://developers.cloudflare.com/r2/) \- Learn about Cloudflare R2
* [Proxy requests to external APIs](https://developers.cloudflare.com/sandbox/guides/proxy-requests/) \- Keep R2 credentials out of the sandbox by proxying requests through a Worker

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/mount-buckets/","name":"Mount buckets"}}]}
```

---

---
title: Handle outbound traffic
description: Intercept and handle outbound HTTP from sandboxes using Workers.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/outbound-traffic.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Handle outbound traffic

Outbound Workers are Workers that handle HTTP requests made by your sandbox. They act as programmable egress proxies, running on the same machine as the sandbox with access to all Workers bindings.

Use outbound Workers to route requests to Workers functions and their bindings (KV, R2, Durable Objects, etc.)

## Defining outbound handlers

Use `outbound` to intercept outbound HTTP traffic regardless of destination:

* [  JavaScript ](#tab-panel-6449)
* [  TypeScript ](#tab-panel-6450)

JavaScript

```

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);

};


```

TypeScript

```

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);

};


```

TLS support coming soon

Sandboxes currently only intercept HTTP traffic. HTTPS interception is coming soon. This will enable using Workers as a transparent proxy for credential injection.

Even though this is just using HTTP, traffic to Workers is secure and runs on the same machine as the Sandbox. If needed, you can also upgrade requests to TLS from the Worker itself.

Use `outboundByHost` to map specific domain names or IP addresses to handler functions:

* [  JavaScript ](#tab-panel-6451)
* [  TypeScript ](#tab-panel-6452)

JavaScript

```

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);

  },

};


```

TypeScript

```

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);

  },

};


```

The sandbox calls `http://my.worker` and the handler runs entirely inside the Workers runtime, outside of the sandbox.

If you define both, `outboundByHost` handlers take precedence over the catch-all `outbound` handler.

## Use Workers bindings in handlers

Outbound handlers have access to your Worker's bindings. Route sandbox traffic to internal platform resources without changing application code.

* [  JavaScript ](#tab-panel-6457)
* [  TypeScript ](#tab-panel-6458)

JavaScript

```

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 });

  },

};


```

TypeScript

```

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 outbound handler resolves it using the KV binding.

## Access Durable Object state

The `ctx` argument exposes `containerId`, which lets you interact with the sandbox's own Durable Object from an outbound handler.

* [  JavaScript ](#tab-panel-6453)
* [  TypeScript ](#tab-panel-6454)

JavaScript

```

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);

  },

};


```

TypeScript

```

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);

  },

};


```

Note

You can also use `containerId` to apply different rules per sandbox instance — for example, to look up per-instance configuration from KV.

## 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()`.

* [  JavaScript ](#tab-panel-6455)
* [  TypeScript ](#tab-panel-6456)

JavaScript

```

import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";

export { ContainerProxy };


export class MySandbox extends Sandbox {}


MySandbox.outboundHandlers = {

  kvAccess: async (request, env, ctx) => {

    const key = new URL(request.url).pathname.slice(1);

    const value = await env.KV.get(key);

    return new Response(value ?? "", { status: value ? 200 : 404 });

  },

};


```

TypeScript

```

import { Sandbox, ContainerProxy } from "@cloudflare/sandbox";

export { ContainerProxy }


export class MySandbox extends Sandbox {}


MySandbox.outboundHandlers = {

kvAccess: async (request: Request, env: Env, ctx: OutboundHandlerContext) => {

const key = new URL(request.url).pathname.slice(1);

const value = await env.KV.get(key);

return new Response(value ?? "", { status: value ? 200 : 404 });

},

};


```

Apply handlers to hosts programmatically from your Worker:

* [  JavaScript ](#tab-panel-6459)
* [  TypeScript ](#tab-panel-6460)

JavaScript

```

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 KV on a specific host during setup

    await sandbox.setOutboundByHost("my.kv", "kvAccess");

    await sandbox.exec("node setup.js");


    // Remove access once setup is complete

    await sandbox.removeOutboundByHost("my.kv");

  },

};


```

TypeScript

```

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 KV on a specific host during setup

    await sandbox.setOutboundByHost("my.kv", "kvAccess");

    await sandbox.exec("node setup.js");


    // Remove access once setup is complete

    await sandbox.removeOutboundByHost("my.kv");


},

};


```

## 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.

Warning

Hostnames that do not resolve via DNS do not work in local development yet. These hostnames do work in production. This limitation will be corrected in a future update.

## Related resources

* [Handle outbound traffic (Containers)](https://developers.cloudflare.com/containers/platform-details/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

## Related resources

* [Handle outbound traffic (Containers)](https://developers.cloudflare.com/containers/platform-details/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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/outbound-traffic/","name":"Handle outbound traffic"}}]}
```

---

---
title: Deploy to Production
description: Set up custom domains for preview URLs in production.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/production-deployment.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Deploy to Production

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 expose ports, you can deploy to `.workers.dev` without this configuration.

Deploy your Sandbox SDK application to production with preview URL support. Preview URLs require wildcard DNS routing because they generate unique subdomains for each exposed port: `https://8080-abc123.yourdomain.com`.

The `.workers.dev` domain does not support wildcard subdomains, so production deployments that use preview URLs 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:

* **Deploy 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

## Setup

### Create 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:

* [  wrangler.jsonc ](#tab-panel-6461)
* [  wrangler.toml ](#tab-panel-6462)

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "name": "my-sandbox-app",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "routes": [

    {

      "pattern": "*.yourdomain.com/*",

      "zone_name": "yourdomain.com"

    }

  ]

}


```

```

"$schema" = "./node_modules/wrangler/config-schema.json"

name = "my-sandbox-app"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-03"


[[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.

### Deploy

Deploy your Worker:

Terminal window

```

npx wrangler deploy


```

## 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 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 won't cover the second-level wildcard `*.sandbox.yourdomain.com` — see 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 Resources

* [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
* [Workers Routing](https://developers.cloudflare.com/workers/configuration/routing/) \- Advanced routing configuration
* [Cloudflare DNS](https://developers.cloudflare.com/dns/) \- DNS management

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/production-deployment/","name":"Deploy to Production"}}]}
```

---

---
title: Proxy requests to external APIs
description: Keep credentials secure by routing sandbox requests through a Worker proxy that injects authentication at request time.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/proxy-requests.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Proxy requests to external APIs

When a sandbox needs to call an external API, you might pass credentials directly into the sandbox process. That approach works, but it means the sandbox holds a live credential that any code running inside it can read, copy, or misuse.

The proxy pattern removes that risk. Your Worker issues a short-lived JWT token to the sandbox. The sandbox uses that token for all API requests, which go to your Worker first. The Worker validates the JWT and injects the real credential before forwarding the request. Real credentials never enter the sandbox.

For a complete multi-service implementation covering GitHub, Anthropic, and R2, refer to the [authentication example ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/examples/authentication).

## How it works

```

Sandbox (short-lived JWT) → Worker proxy (validates JWT, injects real credential) → External API


```

The proxy framework routes requests to named services. Each service is a `ServiceConfig` object with three fields:

* **`target`** — Base URL of the external API to proxy to
* **`validate`** — Extracts the JWT from the incoming request (returns `null` to reject)
* **`transform`** — Injects the real credential into the forwarded request (or returns a `Response` to short-circuit)

You define only the service-specific logic. The framework handles JWT verification, routing, and error responses.

## When to use this pattern

Use the proxy pattern when you need to:

* Call external APIs from sandboxes without exposing credentials
* Rotate credentials without reconfiguring sandboxes
* Restrict what a sandbox can do (for example, limit to specific paths or methods)
* Share one credential across many sandboxes without each holding a copy

For short-lived or low-risk credentials, [environment variables](https://developers.cloudflare.com/sandbox/configuration/environment-variables/) may be simpler.

## Prerequisites

* A Worker with a Sandbox binding (refer to [Get started](https://developers.cloudflare.com/sandbox/get-started/))
* The [jose ↗](https://github.com/panva/jose) package installed in your Worker project for JWT signing

## 1\. Set up secrets

Store the API credential and a secret for signing JWT tokens in your Worker:

Terminal window

```

wrangler secret put MY_API_KEY

wrangler secret put PROXY_JWT_SECRET


```

Generate a strong random value for `PROXY_JWT_SECRET`:

Terminal window

```

openssl rand -hex 32


```

## 2\. Install dependencies

 npm  yarn  pnpm  bun 

```
npm i jose
```

```
yarn add jose
```

```
pnpm add jose
```

```
bun add jose
```

## 3\. Copy the proxy framework

The proxy framework is a self-contained module you copy into your project. Download the [src/proxy/ ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/examples/authentication/src/proxy) directory from the authentication example and place it at `src/proxy/` in your Worker project.

The framework exports:

* **`createProxyHandler`** — Creates the Worker request handler that routes and validates proxy requests
* **`createProxyToken`** — Issues a signed JWT for a sandbox
* **`ServiceConfig`** — The interface your service definitions implement

## 4\. Define a service

Create a `ServiceConfig` for each external API you want to proxy. This example proxies a generic HTTP API that expects a Bearer token:

* [  JavaScript ](#tab-panel-6469)
* [  TypeScript ](#tab-panel-6470)

JavaScript

```

export const myApi = {

  // All requests to /proxy/myapi/* are forwarded to this base URL

  target: "https://api.example.com",


  // Extract the JWT from the Authorization header

  validate: (req) =>

    req.headers.get("Authorization")?.replace("Bearer ", "") ?? null,


  // Replace the JWT with the real API key before forwarding

  transform: async (req, ctx) => {

    req.headers.set("Authorization", `Bearer ${ctx.env.MY_API_KEY}`);

    return req;

  },

};


```

TypeScript

```

import type { ServiceConfig } from '../proxy';


interface Env {

  MY_API_KEY: string;

  PROXY_JWT_SECRET: string;

}


export const myApi: ServiceConfig<Env> = {

  // All requests to /proxy/myapi/* are forwarded to this base URL

  target: 'https://api.example.com',


  // Extract the JWT from the Authorization header

  validate: (req) =>

    req.headers.get('Authorization')?.replace('Bearer ', '') ?? null,


  // Replace the JWT with the real API key before forwarding

  transform: async (req, ctx) => {

    req.headers.set('Authorization', `Bearer ${ctx.env.MY_API_KEY}`);

    return req;

  }

};


```

The `transform` function receives the outgoing request and a context object containing `ctx.env` (your Worker environment) and `ctx.jwt` (the verified token payload, including `sandboxId`). Return the modified request to forward it, or return a `Response` to short-circuit with an error.

## 5\. Wire up the Worker

Register your services with `createProxyHandler` and issue tokens to sandboxes using `createProxyToken`:

* [  JavaScript ](#tab-panel-6471)
* [  TypeScript ](#tab-panel-6472)

JavaScript

```

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

import { createProxyHandler, createProxyToken } from "./proxy";

import { myApi } from "./services/myapi";


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


const proxyHandler = createProxyHandler({

  mountPath: "/proxy",

  jwtSecret: (env) => env.PROXY_JWT_SECRET,

  services: { myapi: myApi },

});


export default {

  async fetch(request, env) {

    const url = new URL(request.url);


    // Route all /proxy/* requests through the proxy handler

    if (url.pathname.startsWith("/proxy/")) {

      return proxyHandler(request, env);

    }


    // Create a sandbox and issue it a short-lived token

    const sandboxId = "my-sandbox";

    const sandbox = getSandbox(env.Sandbox, sandboxId);

    const token = await createProxyToken({

      secret: env.PROXY_JWT_SECRET,

      sandboxId,

      expiresIn: "15m",

    });


    const proxyBase = `https://${url.hostname}`;


    // Pass the token and proxy base URL to the sandbox

    await sandbox.setEnvVars({

      PROXY_TOKEN: token,

      PROXY_BASE: proxyBase,

    });


    return Response.json({ message: "Sandbox ready" });

  },

};


```

TypeScript

```

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

import { createProxyHandler, createProxyToken } from './proxy';

import { myApi } from './services/myapi';


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


interface Env {

  Sandbox: DurableObjectNamespace;

  MY_API_KEY: string;

  PROXY_JWT_SECRET: string;

}


const proxyHandler = createProxyHandler<Env>({

  mountPath: '/proxy',

  jwtSecret: (env) => env.PROXY_JWT_SECRET,

  services: { myapi: myApi }

});


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const url = new URL(request.url);


    // Route all /proxy/* requests through the proxy handler

    if (url.pathname.startsWith('/proxy/')) {

      return proxyHandler(request, env);

    }


    // Create a sandbox and issue it a short-lived token

    const sandboxId = 'my-sandbox';

    const sandbox = getSandbox(env.Sandbox, sandboxId);

    const token = await createProxyToken({

      secret: env.PROXY_JWT_SECRET,

      sandboxId,

      expiresIn: '15m'

    });


    const proxyBase = `https://${url.hostname}`;


    // Pass the token and proxy base URL to the sandbox

    await sandbox.setEnvVars({

      PROXY_TOKEN: token,

      PROXY_BASE: proxyBase

    });


    return Response.json({ message: 'Sandbox ready' });

  }

};


```

The `mountPath` (`/proxy`) and service name (`myapi`) together form the proxy route. A request to `/proxy/myapi/some/path` is validated and forwarded to `https://api.example.com/some/path`.

## 6\. Call the proxy from the sandbox

Inside the sandbox, use the `PROXY_TOKEN` and `PROXY_BASE` environment variables to call the proxy. The JWT takes the place of the real credential:

Terminal window

```

curl "$PROXY_BASE/proxy/myapi/v1/endpoint" \

  -H "Authorization: Bearer $PROXY_TOKEN" \

  -H "Content-Type: application/json" \

  -d '{"input": "hello"}'


```

Or from Python running inside the sandbox:

Python

```

import os

import requests


response = requests.post(

    f"{os.environ['PROXY_BASE']}/proxy/myapi/v1/endpoint",

    headers={"Authorization": f"Bearer {os.environ['PROXY_TOKEN']}"},

    json={"input": "hello"}

)


```

The real `MY_API_KEY` is never present in the sandbox. The Worker substitutes it transparently.

Pointing SDKs at the proxy URL

Many API clients and SDKs default to the official API base URL. After setting up the proxy, you need to tell the library to send requests to your Worker instead. Most SDKs support an environment variable or constructor option to override the base URL.

For example, the Anthropic SDK reads `ANTHROPIC_BASE_URL`. Pass your proxy URL as that value and the JWT token as the API key:

Terminal window

```

export ANTHROPIC_BASE_URL="$PROXY_BASE/proxy/anthropic"

export ANTHROPIC_API_KEY="$PROXY_TOKEN"


```

The SDK then sends all requests to your Worker proxy, which validates the token and forwards them to `api.anthropic.com` with the real key injected. Check the documentation for your API client to find the equivalent base URL setting.

## Adding more services

To proxy additional APIs, define another `ServiceConfig` and add it to `createProxyHandler`:

* [  JavaScript ](#tab-panel-6467)
* [  TypeScript ](#tab-panel-6468)

JavaScript

```

export const anotherApi = {

  target: "https://api.another-service.com",

  validate: (req) =>

    req.headers.get("Authorization")?.replace("Bearer ", "") ?? null,

  transform: async (req, ctx) => {

    req.headers.set("Authorization", `Bearer ${ctx.env.ANOTHER_API_KEY}`);

    return req;

  },

};


// In your Worker:

const proxyHandler = createProxyHandler({

  mountPath: "/proxy",

  jwtSecret: (env) => env.PROXY_JWT_SECRET,

  services: { myapi: myApi, another: anotherApi },

});


```

TypeScript

```

export const anotherApi: ServiceConfig<Env> = {

  target: 'https://api.another-service.com',

  validate: (req) => req.headers.get('Authorization')?.replace('Bearer ', '') ?? null,

  transform: async (req, ctx) => {

    req.headers.set('Authorization', `Bearer ${ctx.env.ANOTHER_API_KEY}`);

    return req;

  }

};


// In your Worker:

const proxyHandler = createProxyHandler<Env>({

  mountPath: '/proxy',

  jwtSecret: (env) => env.PROXY_JWT_SECRET,

  services: { myapi: myApi, another: anotherApi }

});


```

Each service is reachable at `/proxy/<service-name>/*`. The sandbox uses the same JWT token for all of them.

## Troubleshooting

### Proxy returns 401

The JWT is missing, expired, or signed with the wrong secret. Verify that:

* The sandbox is using the token returned by `createProxyToken`, not a hardcoded value
* The same `PROXY_JWT_SECRET` value is used to create and verify tokens
* The token has not expired — the default is 15 minutes

To issue a fresh token and pass it to the sandbox:

* [  JavaScript ](#tab-panel-6463)
* [  TypeScript ](#tab-panel-6464)

JavaScript

```

const freshToken = await createProxyToken({

  secret: env.PROXY_JWT_SECRET,

  sandboxId,

  expiresIn: "15m",

});

await sandbox.setEnvVars({ PROXY_TOKEN: freshToken });


```

TypeScript

```

const freshToken = await createProxyToken({

  secret: env.PROXY_JWT_SECRET,

  sandboxId,

  expiresIn: '15m'

});

await sandbox.setEnvVars({ PROXY_TOKEN: freshToken });


```

### Proxy returns 404 for the service

The service name in the URL must match the key in the `services` object. A request to `/proxy/myapi/...` requires `services: { myapi: ... }`.

### transform returns unexpected results

Log the request URL in `transform` to confirm the path is being rewritten correctly:

* [  JavaScript ](#tab-panel-6465)
* [  TypeScript ](#tab-panel-6466)

JavaScript

```

transform: async (req, ctx) => {

  console.log("Proxying to:", req.url);

  req.headers.set("Authorization", `Bearer ${ctx.env.MY_API_KEY}`);

  return req;

};


```

TypeScript

```

transform: async (req, ctx) => {

  console.log('Proxying to:', req.url);

  req.headers.set('Authorization', `Bearer ${ctx.env.MY_API_KEY}`);

  return req;

}


```

## Related resources

* [Authentication example ↗](https://github.com/cloudflare/sandbox-sdk/tree/main/examples/authentication) — complete multi-service implementation with GitHub, Anthropic, and R2
* [Security model](https://developers.cloudflare.com/sandbox/concepts/security/) — how the Sandbox SDK approaches security
* [Work with Git](https://developers.cloudflare.com/sandbox/guides/git-workflows/) — Git operations in sandboxes
* [Environment variables](https://developers.cloudflare.com/sandbox/configuration/environment-variables/) — simpler alternative for lower-risk credentials

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/proxy-requests/","name":"Proxy requests to external APIs"}}]}
```

---

---
title: Stream output
description: Handle real-time output from commands and processes.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/streaming-output.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Stream output

This guide shows you how to handle real-time output from commands, processes, and code execution.

## 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:

* [  JavaScript ](#tab-panel-6479)
* [  TypeScript ](#tab-panel-6480)

JavaScript

```

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;

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6475)
* [  TypeScript ](#tab-panel-6476)

JavaScript

```

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",

      },

    });

  },

};


```

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, '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:

* [  JavaScript ](#tab-panel-6473)
* [  TypeScript ](#tab-panel-6474)

JavaScript

```

// 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();

});


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-6477)
* [  TypeScript ](#tab-panel-6478)

JavaScript

```

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;

  }

}


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6481)
* [  TypeScript ](#tab-panel-6482)

JavaScript

```

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;

  }

}


```

TypeScript

```

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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/streaming-output/","name":"Stream output"}}]}
```

---

---
title: WebSocket Connections
description: Connect to WebSocket servers running in sandboxes.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/guides/websocket-connections.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# WebSocket Connections

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:**

echo-server.ts

```

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:**

startup.sh

```

#!/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:**

* [  JavaScript ](#tab-panel-6485)
* [  TypeScript ](#tab-panel-6486)

JavaScript

```

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");

  },

};


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6487)
* [  TypeScript ](#tab-panel-6488)

JavaScript

```

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 });

  },

};


```

TypeScript

```

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 });


}

};


```

**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:

* [  JavaScript ](#tab-panel-6489)
* [  TypeScript ](#tab-panel-6490)

JavaScript

```

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");

  },

};


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-6483)
* [  TypeScript ](#tab-panel-6484)

JavaScript

```

console.log(request.headers.get("Upgrade")); // 'websocket'

console.log(request.headers.get("Connection")); // 'Upgrade'


```

TypeScript

```

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
* [Background processes guide](https://developers.cloudflare.com/sandbox/guides/background-processes/) \- Managing long-running services

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/guides/","name":"How-to guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/guides/websocket-connections/","name":"WebSocket Connections"}}]}
```

---

---
title: Concepts
description: 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.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/concepts/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Concepts

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 service exposure works
* [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

## 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/concepts/","name":"Concepts"}}]}
```

---

---
title: Architecture
description: Sandbox SDK lets you execute untrusted code safely from your Workers. It combines three Cloudflare technologies to provide secure, stateful, and isolated execution:
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/concepts/architecture.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Architecture

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 two 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");


```

### WebSocket transport

Multiplexes all SDK calls over a single persistent WebSocket connection. Avoids [subrequest limits](https://developers.cloudflare.com/workers/platform/limits/#subrequests) when making many concurrent operations.

Enable WebSocket transport by setting the `SANDBOX_TRANSPORT` variable in your Worker's configuration:

* [  wrangler.jsonc ](#tab-panel-6209)
* [  wrangler.toml ](#tab-panel-6210)

```

{

  "vars": {

    "SANDBOX_TRANSPORT": "websocket"

  },

}


```

```

[vars]

SANDBOX_TRANSPORT = "websocket"


```

The transport layer is transparent to your application code - all SDK methods work identically regardless of transport. See [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/) for details on when to use each transport and configuration examples.

## 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

**WebSocket transport flow**:

1. **Client SDK** validates parameters and sends request over persistent WebSocket connection
2. **Durable Object** maintains WebSocket connection, multiplexes concurrent requests
3. **Container Runtime** adapts WebSocket messages to HTTP-style request/response
4. **Response flows back** over same WebSocket connection with proper error transformation

The WebSocket connection is established on first SDK call and reused 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/concepts/architecture/","name":"Architecture"}}]}
```

---

---
title: Container runtime
description: 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.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/concepts/containers.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Container runtime

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:

Terminal window

```

# 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:

Terminal window

```

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:

Terminal window

```

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

* [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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/concepts/containers/","name":"Container runtime"}}]}
```

---

---
title: Preview URLs
description: 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.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/concepts/preview-urls.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Preview URLs

Production requires custom domain

Preview URLs work in local development without configuration. For production, you need a custom domain with wildcard DNS routing. See [Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/).

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

Warning

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, see the [TLS note in Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/).
* **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, see [Production Deployment troubleshooting](https://developers.cloudflare.com/sandbox/guides/production-deployment/#troubleshooting).

### Local Development

Local development limitation

When using `wrangler dev`, you must expose ports in your 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

* [Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/) \- Set up custom domains for production
* [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
* [Security Model](https://developers.cloudflare.com/sandbox/concepts/security/) \- Security best practices

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/concepts/preview-urls/","name":"Preview URLs"}}]}
```

---

---
title: Sandbox lifecycle
description: A sandbox is an isolated execution environment where your code runs. Each sandbox:
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/concepts/sandboxes.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Sandbox lifecycle

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

## 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}`);


```

User's work persists while actively using the sandbox. Good for interactive environments, playgrounds, and notebooks where users work continuously.

### Per-session sandboxes

TypeScript

```

const sessionId = `session-${Date.now()}-${Math.random()}`;

const sandbox = getSandbox(env.Sandbox, sessionId);

// Later:

await sandbox.destroy();


```

Fresh environment each time. Good for one-time execution, CI/CD, and isolated tests.

### 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

**Don't 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 don't 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`:

```

# 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 long-lived sandboxes** \- One 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
* [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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/concepts/sandboxes/","name":"Sandbox lifecycle"}}]}
```

---

---
title: Security model
description: The Sandbox SDK is built on Containers, which run each sandbox in its own VM for strong isolation.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/concepts/security.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Security model

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/platform-details/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);


```

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:

TypeScript

```

// Bad - hardcoded in file

await sandbox.writeFile('/workspace/config.js', `

  const API_KEY = 'sk_live_abc123';

`);


// Good - use environment variables

await sandbox.startProcess('node app.js', {

  env: {

    API_KEY: env.API_KEY,  // From Worker environment binding

  }

});


```

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');

}


```

## Proxying external API requests

Passing 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. A Worker proxy removes that exposure by keeping credentials exclusively in the Worker and giving the sandbox a short-lived JWT instead.

The flow works as follows:

```

Sandbox (short-lived JWT) → Worker proxy (validates JWT, injects real credentials) → External API


```

The sandbox never sees the real credential. If the JWT is compromised, it expires after a short window and cannot be reused.

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. Refer to [Proxy requests to external APIs](https://developers.cloudflare.com/sandbox/guides/proxy-requests/) for a complete implementation.

## 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/platform-details/architecture/) \- Underlying platform security
* [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/concepts/sandboxes/) \- Resource management

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/concepts/security/","name":"Security model"}}]}
```

---

---
title: Session management
description: Sessions are bash shell execution contexts within a sandbox. Think of them like terminal tabs or panes in the same container.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/concepts/sessions.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Session management

Sessions are bash shell execution contexts within a sandbox. Think of them like terminal tabs or panes in the same container.

* **Sandbox** \= A computer (container)
* **Session** \= A terminal shell session in that computer

## Default session

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.

### 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 doesn't 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 you can't "break" commands by referencing non-existent sessions. However, custom configuration (environment variables, working directory) is lost after deletion.

## Creating sessions

Create additional sessions for isolated shell contexts:

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's isolated per 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's shared

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 isolated shell state for different 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 (untrusted code)
* Different users require fully separated environments
* 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 isolation

**Sessions share filesystem** \- file operations affect all sessions:

TypeScript

```

// Bad - affects all sessions

await session.exec('rm -rf /workspace/*');


// For untrusted code isolation, use separate sandboxes

const userSandbox = getSandbox(env.Sandbox, 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/concepts/sessions/","name":"Session management"}}]}
```

---

---
title: Terminal connections
description: 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.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/concepts/terminal.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Terminal connections

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.

## 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.

```

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 isolation

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 all send input. This enables collaborative terminal use cases.

## 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/concepts/terminal/","name":"Terminal connections"}}]}
```

---

---
title: Configuration
description: Configure your Sandbox SDK deployment with Wrangler, customize container images, and manage environment variables.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/configuration/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Configuration

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 WebSocket 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/configuration/","name":"Configuration"}}]}
```

---

---
title: Dockerfile reference
description: Customize the sandbox container image with your own packages, tools, and configurations by extending the base runtime image.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/configuration/dockerfile.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Dockerfile reference

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             |

```

# 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:

wrangler.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.

startup.sh

```

#!/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/platform-details/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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/configuration/","name":"Configuration"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/configuration/dockerfile/","name":"Dockerfile reference"}}]}
```

---

---
title: Environment variables
description: Pass configuration, secrets, and runtime settings to your sandboxes using environment variables.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/configuration/environment-variables.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Environment variables

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" |
| ----------- | --------------------- |
| **Default** | "http"                |

Controls the transport protocol for SDK-to-container communication. WebSocket 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.

* [  wrangler.jsonc ](#tab-panel-6211)
* [  wrangler.toml ](#tab-panel-6212)

```

{

  "vars": {

    "SANDBOX_TRANSPORT": "websocket"

  }

}


```

```

[vars]

SANDBOX_TRANSPORT = "websocket"


```

See [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/) for a complete guide including when to use each transport, performance considerations, and migration instructions.

### 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).

* [  wrangler.jsonc ](#tab-panel-6213)
* [  wrangler.toml ](#tab-panel-6214)

```

{

  "vars": {

    "COMMAND_TIMEOUT_MS": "30000"

  }

}


```

```

[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:

Terminal window

```

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 and WebSocket transport. See [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/) for details on when and how to configure each 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:**

Terminal window

```

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 vs WebSocket 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
* [Proxy requests to external APIs](https://developers.cloudflare.com/sandbox/guides/proxy-requests/) \- Keep credentials out of the sandbox entirely using a Worker proxy

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/configuration/","name":"Configuration"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/configuration/environment-variables/","name":"Environment variables"}}]}
```

---

---
title: Sandbox options
description: Configure sandbox behavior by passing options when creating a sandbox instance with getSandbox().
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/configuration/sandbox-options.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Sandbox options

Configure sandbox behavior by passing options when creating a sandbox instance with `getSandbox()`.

## Available options

TypeScript

```

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


const sandbox = getSandbox(binding, sandboxId, options?: SandboxOptions);


```

### 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.

* [  JavaScript ](#tab-panel-6219)
* [  TypeScript ](#tab-panel-6220)

JavaScript

```

// 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

}


```

TypeScript

```

// 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.

* [  JavaScript ](#tab-panel-6217)
* [  TypeScript ](#tab-panel-6218)

JavaScript

```

// 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

});


```

TypeScript

```

// 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.

* [  JavaScript ](#tab-panel-6221)
* [  TypeScript ](#tab-panel-6222)

JavaScript

```

// 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

  },

});


```

TypeScript

```

// 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`

* [  wrangler.jsonc ](#tab-panel-6215)
* [  wrangler.toml ](#tab-panel-6216)

```

{

  "vars": {

    "SANDBOX_LOG_LEVEL": "debug",

    "SANDBOX_LOG_FORMAT": "pretty"

  }

}


```

```

[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).

* [  JavaScript ](#tab-panel-6223)
* [  TypeScript ](#tab-panel-6224)

JavaScript

```

// 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)


```

TypeScript

```

// 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/configuration/","name":"Configuration"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/configuration/sandbox-options/","name":"Sandbox options"}}]}
```

---

---
title: Transport modes
description: Configure how the Sandbox SDK communicates with containers using transport modes.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/configuration/transport.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Transport modes

Configure how the Sandbox SDK communicates with containers using transport modes.

## Overview

The Sandbox SDK supports two 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.
* **WebSocket transport** \- All SDK operations are multiplexed over a single persistent WebSocket connection.

## When to use WebSocket transport

Use WebSocket 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 WebSocket transport helps

WebSocket 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 WebSocket 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");


```

## 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.

### WebSocket transport

Enable WebSocket transport by adding `SANDBOX_TRANSPORT` to your Worker's `vars`:

* [  wrangler.jsonc ](#tab-panel-6229)
* [  wrangler.toml ](#tab-panel-6230)

```

{

  "name": "my-sandbox-worker",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "vars": {

    "SANDBOX_TRANSPORT": "websocket"

  },

  "containers": [

    {

      "class_name": "Sandbox",

      "image": "./Dockerfile",

    },

  ],

  "durable_objects": {

    "bindings": [

      {

        "class_name": "Sandbox",

        "name": "Sandbox",

      },

    ],

  },

}


```

```

name = "my-sandbox-worker"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-03"


[vars]

SANDBOX_TRANSPORT = "websocket"


[[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

**WebSocket 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

Both transports support streaming operations (like `exec()` with real-time output):

* **HTTP transport** \- Uses Server-Sent Events (SSE)
* **WebSocket transport** \- Uses WebSocket streaming messages

Your code remains identical regardless of transport mode.

### Error handling

Both 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

| Scenario                                    | Recommended transport |
| ------------------------------------------- | --------------------- |
| Many SDK operations per request             | WebSocket             |
| Running inside Workers or Durable Objects   | WebSocket             |
| Approaching subrequest limits               | WebSocket             |
| Simple, infrequent sandbox usage            | HTTP (default)        |
| Debugging or inspecting individual requests | HTTP (default)        |

Default is sufficient for most use cases

HTTP transport works well for most applications. Only switch to WebSocket transport if you are hitting subrequest limits or performing many rapid sandbox operations per request.

## Migration guide

Switching between transports requires no code changes.

### Switch from HTTP to WebSocket

Add `SANDBOX_TRANSPORT` to your `wrangler.jsonc`:

* [  wrangler.jsonc ](#tab-panel-6225)
* [  wrangler.toml ](#tab-panel-6226)

```

{

  "vars": {

    "SANDBOX_TRANSPORT": "websocket"

  },

}


```

```

[vars]

SANDBOX_TRANSPORT = "websocket"


```

Then deploy:

Terminal window

```

npx wrangler deploy


```

### Switch from WebSocket to HTTP

Remove the `SANDBOX_TRANSPORT` variable (or set it to `"http"`):

* [  wrangler.jsonc ](#tab-panel-6227)
* [  wrangler.toml ](#tab-panel-6228)

```

{

  "vars": {

    // Remove SANDBOX_TRANSPORT or set to "http"

  },

}


```

```

vars = { }


```

## 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/configuration/","name":"Configuration"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/configuration/transport/","name":"Transport modes"}}]}
```

---

---
title: Wrangler configuration
description: The minimum required configuration for using Sandbox SDK:
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/configuration/wrangler.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Wrangler configuration

## Minimal configuration

The minimum required configuration for using Sandbox SDK:

* [  wrangler.jsonc ](#tab-panel-6237)
* [  wrangler.toml ](#tab-panel-6238)

```

{

  "name": "my-sandbox-worker",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "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",

    },

  ],

}


```

```

name = "my-sandbox-worker"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-03"

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

Terminal window

```

npx wrangler r2 bucket create my-backup-bucket


```

### 2\. Add the binding and environment variables

* [  wrangler.jsonc ](#tab-panel-6231)
* [  wrangler.toml ](#tab-panel-6232)

```

{

  "vars": {

    "BACKUP_BUCKET_NAME": "my-backup-bucket",

    "CLOUDFLARE_ACCOUNT_ID": "<YOUR_ACCOUNT_ID>",

  },

  "r2_buckets": [

    {

      "binding": "BACKUP_BUCKET",

      "bucket_name": "my-backup-bucket",

    },

  ],

}


```

```

[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

Terminal window

```

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:

* [  wrangler.jsonc ](#tab-panel-6233)
* [  wrangler.toml ](#tab-panel-6234)

```

{

  "durable_objects": {

    "bindings": [

      {

        "class_name": "Sandbox",

        "name": "Sandbox",

      },

    ],

  },

}


```

```

[[durable_objects.bindings]]

class_name = "Sandbox"

name = "Sandbox"


```

### Missing migrations

**Error**: Durable Object not initialized

**Solution**: Add migrations for the Sandbox class:

* [  wrangler.jsonc ](#tab-panel-6235)
* [  wrangler.toml ](#tab-panel-6236)

```

{

  "migrations": [

    {

      "new_sqlite_classes": ["Sandbox"],

      "tag": "v1",

    },

  ],

}


```

```

[[migrations]]

new_sqlite_classes = [ "Sandbox" ]

tag = "v1"


```

## Related resources

* [Transport modes](https://developers.cloudflare.com/sandbox/configuration/transport/) \- Configure HTTP vs WebSocket 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

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/configuration/","name":"Configuration"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/configuration/wrangler/","name":"Wrangler configuration"}}]}
```

---

---
title: Platform
description: Information about the Sandbox SDK platform, including pricing, limits, and beta status.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/platform/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Platform

Information about the Sandbox SDK platform, including pricing, limits, and beta status.

## 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
* [Beta Information](https://developers.cloudflare.com/sandbox/platform/beta-info/) \- Current status and roadmap

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.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/platform/","name":"Platform"}}]}
```

---

---
title: Beta Information
description: Sandbox SDK is currently in open beta. This means the product is publicly available and ready to use, but we're actively gathering feedback and may make changes based on what we learn.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/platform/beta-info.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Beta Information

Sandbox SDK is currently in open beta. This means the product is publicly available and ready to use, but we're actively gathering feedback and may make changes based on what we learn.

## What to Expect

During the beta period:

* **API stability** \- The core API is stable, but we may introduce new features or adjust existing ones based on feedback
* **Production use** \- You can use Sandbox SDK in production, but be aware of potential changes
* **Active development** \- We're continuously improving performance, adding features, and fixing bugs
* **Documentation updates** \- Guides and examples will be refined as we learn from real-world usage

## Known Limitations

See [Containers Beta Information](https://developers.cloudflare.com/containers/beta-info/) for current limitations and known issues, as Sandbox SDK inherits the same constraints.

## Feedback Wanted

We'd love to hear about your experience with Sandbox SDK:

* What are you building?
* What features would be most valuable?
* What challenges have you encountered?
* What instance sizes do you need?

Share your feedback:

* [GitHub Issues ↗](https://github.com/cloudflare/sandbox-sdk/issues) \- Report bugs or request features
* [Developer Discord ↗](https://discord.cloudflare.com) \- Chat with the team and community
* [Community Forum ↗](https://community.cloudflare.com) \- Discuss use cases and best practices

Check the [GitHub repository ↗](https://github.com/cloudflare/sandbox-sdk) for the latest updates and upcoming features.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/platform/","name":"Platform"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/platform/beta-info/","name":"Beta Information"}}]}
```

---

---
title: Limits
description: Since the Sandbox SDK is built on top of the Containers platform, it shares the same underlying platform characteristics. Refer to these pages to understand how pricing and limits work for your sandbox deployments.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/platform/limits.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Limits

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.

## Container limits

Refer to [Containers limits](https://developers.cloudflare.com/containers/platform-details/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 WebSocket transport

Enable WebSocket transport to multiplex all SDK calls over a single persistent connection:

* [  wrangler.jsonc ](#tab-panel-6491)
* [  wrangler.toml ](#tab-panel-6492)

```

{

  "vars": {

    "SANDBOX_TRANSPORT": "websocket"

  },

}


```

```

[vars]

SANDBOX_TRANSPORT = "websocket"


```

With WebSocket transport enabled:

* The WebSocket upgrade 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-details/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 WebSocket transport for high-frequency operations** \- Enable `SANDBOX_TRANSPORT=websocket` to avoid subrequest limits when making many SDK calls per request

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/platform/","name":"Platform"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/platform/limits/","name":"Limits"}}]}
```

---

---
title: Pricing
description: Sandbox SDK pricing is determined by the underlying Containers platform it's built on.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/sandbox/platform/pricing.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Pricing

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/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)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/platform/","name":"Platform"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/platform/pricing/","name":"Pricing"}}]}
```
