# Render Sandboxes — Run untrusted code in an isolated, on-demand environment.


> *Render Sandboxes is in early access.*
>
> APIs, defaults, and limits might change during the early access period. Before using sandboxes in production workloads, discuss your use case with your Render contact.

*Render Sandboxes* provides managed, ephemeral environments for executing untrusted code. Spin up a sandbox, install what you need, and run your workload. Render handles all of the provisioning and cleanup.

- *Enable agents* to install packages, write files, and run generated code without touching your infrastructure.
- *Execute user-submitted code* to support tools such as code interpreters and notebook backends.
- *Run jobs* that benefit from a complete OS, such as builds, tests, data transformations, and scripts.

## At a glance

1. You [create a sandbox](#4-create-a-sandbox-wait-for-it-and-run-a-command) with the Render SDK or CLI.
2. Inside the sandbox, you (and your agents) can run commands and manage files without exposing your production infrastructure to untrusted code.
3. When your workload completes, you can terminate the sandbox.
    - If you don't terminate the sandbox, it eventually times out according to your settings.

> *Sandboxes have an ephemeral filesystem.*
>
> When you terminate a sandbox, its filesystem state is permanently lost. To preserve data from a sandbox, see [Snapshots](#snapshots).

## Quickstart

### 1. Confirm access

During the early access period, Render is rolling out sandbox support to select workspaces.

To confirm whether your workspace is enabled, open the *+ New* menu in the upper right of the [Render Dashboard](https://dashboard.render.com):

[image: Confirming sandbox support in the Render Dashboard]

If you don't see the *Sandbox Group* option, Render Sandboxes is not yet enabled for your workspace. Reach out to your Render contact and provide your *Workspace ID*, available from the top of your workspace's *Settings* page.

If Render Sandboxes is enabled for your workspace, you're ready to proceed.

### 2. Install supported clients

You can create and manage sandboxes with the Render CLI or the Render SDK (available for TypeScript and Python).

Install any combination of clients you want to use:

**Tab: CLI**

> Requires Render CLI version 2.28.0 or later.

Use any of the following methods to install the Render CLI or upgrade to the latest version:

**Subtab: Homebrew**

##### Homebrew installation

Run the following commands:

```shell
brew update
brew install render
```

**Subtab: WinGet**

##### WinGet installation

Run the following command:

```shell
winget install render.cli
```

**Subtab: curl**

##### Installing with `curl`

Run the following command:

```shell
curl -fsSL https://raw.githubusercontent.com/render-oss/cli/refs/heads/main/bin/install.sh | sh
```

**Subtab: Direct download**

##### Direct download

1. Open the CLI's [GitHub releases page](https://github.com/render-oss/cli/releases/).
2. Download the executable that corresponds to your system's architecture.

If you use an architecture besides those provided, you can build from source instead.

**Subtab: Build from source**

##### Building from source

> We recommend building from source only if no other installation method works for your system.

1. [Install the Go programming language](https://golang.org/doc/install) if you haven't already.

2. Clone and build the CLI project with the following commands:

   ```shell
   git clone git@github.com:render-oss/cli.git
   cd cli
   go build -o render
   ```

After installation completes, open a new terminal tab and run `render` with no arguments to confirm.

**Tab: TypeScript SDK**

> Requires Node.js 18+ and SDK version 1.2.0 or later.

##### Install with `npm`

```shell
npm install "@renderinc/sdk@^1.2.0"
```

##### Install with `bun`

```shell
bun add "@renderinc/sdk@^1.2.0"
```

**Tab: Python SDK**

> Requires Python 3.10+ and SDK version 1.2.0 or later.

##### Install with `pip`

```shell
pip install --upgrade "render>=1.2.0"
```

##### Install with `uv`

```shell
uv add "render>=1.2.0"
```

### 3. Configure credentials

Create an API key under *Account Settings > API Keys*, then export the key and your workspace ID:

> *Sandboxes require a valid Render API key and workspace ID to start successfully.*
>
> You can find your workspace's ID at the top of its *Settings* page in the [Render Dashboard](https://dashboard.render.com).

```shell
export RENDER_API_KEY='rnd_...'
export RENDER_WORKSPACE_ID='tea-...'
```

The SDKs read the workspace ID from `RENDER_WORKSPACE_ID`. To select the same workspace for the CLI, run:

```shell
render workspace set tea-abc123
```

Replace `tea-abc123` with your workspace ID. In scripts, you can set the CLI's `RENDER_WORKSPACE` environment variable instead.

### 4. Create a sandbox, wait for it, and run a command

```shell
# Create a five-minute sandbox and copy the sbx-... ID from the output.
render ea sandboxes create --timeout=300

# Creation is asynchronous. Repeat this command until your sandbox appears as running.
render ea sandboxes list --status running

# Replace sbx-abc123 with the ID returned by create.
render ea sandboxes exec sbx-abc123 -- echo "hello from Render"

# Terminate the sandbox when you are done.
render ea sandboxes stop sbx-abc123 --confirm
```

> `create` returns while the sandbox is still starting. Wait until its status is `running` before the first `exec`.

## SDK quickstarts

These single-file examples demonstrate the basic flow for creating a sandbox, waiting for it to finish starting, executing a command, and terminating the sandbox:

**Tab: TypeScript SDK**

##### TypeScript

```typescript
import { Render } from "@renderinc/sdk";

const sandboxes = new Render().experimental.sandboxes;
// Provision a sandbox with a five-minute lifetime.
const sandbox = await sandboxes.create({ timeoutSeconds: 300 });

try {
  let ready = false;
  // A new sandbox starts in `creating`; wait until it can accept commands.
  // Poll for up to one minute (40 attempts, 1.5 seconds apart).
  for (let attempt = 0; attempt < 40; attempt++) {
    const status = (await sandboxes.get(sandbox.id)).status;
    if (status === "running") {
      ready = true;
      break;
    }
    if (status === "errored" || status === "terminated") {
      throw new Error(`sandbox is ${status}`);
    }
    await new Promise((resolve) => setTimeout(resolve, 1500));
  }

  if (!ready) throw new Error(`Sandbox ${sandbox.id} is not running yet`);

  // Execute a command and stream its output and exit status.
  for await (const event of await sandboxes.exec(
    sandbox.id,
    "echo hello; exit 3",
  )) {
    if (event.type === "output") {
      process.stdout.write(`[${event.stream}] ${event.data}`);
    } else if (event.type === "exit") {
      console.log(`exit code: ${event.exit_code}`); // 3
    }
  }
} finally {
  // Release the ephemeral sandbox when the workload is complete.
  await sandboxes.terminate(sandbox.id);
}
```

**Tab: Python SDK**

##### Python

```python
import asyncio

from render import RenderAsync
from render.experimental.sandbox import SandboxExecExit, SandboxExecOutput


async def main():
    sandboxes = RenderAsync().experimental.sandboxes
    # Provision a sandbox with a five-minute lifetime.
    sandbox = await sandboxes.create(timeout_seconds=300)

    try:
        # A new sandbox starts in `creating`; wait until it can accept commands.
        # Poll for up to one minute (40 attempts, 1.5 seconds apart).
        for _ in range(40):
            status = (await sandboxes.from_id(sandbox.id)).status
            if status == "running":
                break
            if status in ("errored", "terminated"):
                raise RuntimeError(f"sandbox is {status}")
            await asyncio.sleep(1.5)
        else:
            raise TimeoutError(f"Sandbox {sandbox.id} is not running yet")

        # Execute a command and stream its output and exit status.
        async for event in sandboxes.exec(sandbox.id, "echo hello; exit 3"):
            if isinstance(event, SandboxExecOutput):
                print(f"[{event.stream}] {event.data}", end="")
            elif isinstance(event, SandboxExecExit):
                print(f"exit code: {event.exit_code}")  # 3
    finally:
        # Release the ephemeral sandbox when the workload is complete.
        await sandboxes.terminate(sandbox.id)


asyncio.run(main())
```

## Snapshots

A snapshot captures the state of a running sandbox. You can use a snapshot to reuse a prepared filesystem or resume a running process in a new sandbox. Creating a snapshot does not stop the source sandbox.

| Kind | Captures | Restore requirements |
| --- | --- | --- |
| `filesystem` (default) | The writable filesystem | Can restore using the available compute configuration |
| `runtime` | The writable filesystem, memory, and CPU state | Must restore using the snapshot's `plan` value |

Each snapshot belongs to the same sandbox group as its source sandbox. You can restore the snapshot only to a new sandbox in that group.

Snapshot creation returns before the capture completes. Wait for the snapshot's status to change to `available` before restoring it. If the status changes to `failed`, check the snapshot's `error` field.

Snapshots expire three days after creation by default. To determine when an individual snapshot expires, check its `expires_at` (Python) or `expiresAt` (TypeScript) value.

### Capture and restore snapshots

You can capture the filesystem of an existing running sandbox in a snapshot, then restore that snapshot to create a new sandbox. Wait for the snapshot to become available before restoring it, and wait for the restored sandbox to become running before using it.

**Tab: CLI**

First, select the source sandbox's workspace with `render workspace set`.

```shell
# Replace sbx-abc123 with a running sandbox's ID.
render ea sandboxes snapshots create sbx-abc123

# Copy the snp-... ID from the output. Repeat until the status is available.
# If the status is failed, check the error field instead of restoring.
render ea sandboxes snapshots get snp-abc123

# Create a new sandbox from the saved filesystem.
render ea sandboxes create --snapshot-id snp-abc123 --timeout 300

# Copy the new sbx-... ID. Wait until its status is running before exec.
render ea sandboxes list --status running

# Replace sbx-restored123 with the new ID. Stop it after your workload.
render ea sandboxes stop sbx-restored123 --confirm
```

To create a runtime snapshot, add `--kind runtime` to `snapshots create`. When you restore it, set `--plan` to the value of the snapshot's `plan` field.

**Tab: Python**

Before running this example, [configure your credentials](#3-configure-credentials).

```python
import asyncio

from render import RenderAsync


async def main():
    sandboxes = RenderAsync().experimental.sandboxes
    snapshot = await sandboxes.snapshots.create("sbx-abc123")

    # Poll for up to one minute (40 attempts, 1.5 seconds apart).
    for _ in range(40):
        snapshot = await sandboxes.snapshots.from_id(
            sandbox_group_id=snapshot.sandbox_group_id,
            snapshot_id=snapshot.id,
        )
        if snapshot.status == "available":
            break
        if snapshot.status == "failed":
            raise RuntimeError(snapshot.error or "Snapshot capture failed")
        await asyncio.sleep(1.5)
    else:
        raise TimeoutError(f"Snapshot {snapshot.id} is not available yet")

    restored = await sandboxes.create(snapshot_id=snapshot.id, timeout_seconds=300)
    try:
        # Poll for up to one minute (40 attempts, 1.5 seconds apart).
        for _ in range(40):
            status = (await sandboxes.from_id(restored.id)).status
            if status == "running":
                break
            if status in ("errored", "terminated"):
                raise RuntimeError(f"Restored sandbox is {status}")
            await asyncio.sleep(1.5)
        else:
            raise TimeoutError(f"Sandbox {restored.id} is not running yet")

        print(f"Ready to use: {restored.id}")
        # Run your workload here before the sandbox is terminated.
    finally:
        await sandboxes.terminate(restored.id)


asyncio.run(main())
```

To create a runtime snapshot, pass `kind="runtime"` to `snapshots.create`. When you restore it, pass `plan=snapshot.plan` to `sandboxes.create`. The sync client, `Render`, provides the same methods without `await`.

**Tab: TypeScript**

Before running this example, [configure your credentials](#3-configure-credentials).

```typescript
import { Render } from "@renderinc/sdk";

const sandboxes = new Render().experimental.sandboxes;
let snapshot = await sandboxes.snapshots.create({ sandboxId: "sbx-abc123" });

// Poll for up to one minute (40 attempts, 1.5 seconds apart).
for (let attempt = 0; attempt < 40; attempt++) {
  snapshot = await sandboxes.snapshots.get({
    sandboxGroupId: snapshot.sandboxGroupId,
    snapshotId: snapshot.id,
  });
  if (snapshot.status === "available") break;
  if (snapshot.status === "failed") {
    throw new Error(snapshot.error || "Snapshot capture failed");
  }
  await new Promise((resolve) => setTimeout(resolve, 1500));
}
if (snapshot.status !== "available") {
  throw new Error(`Snapshot ${snapshot.id} is not available yet`);
}

const restored = await sandboxes.create({
  snapshotId: snapshot.id,
  timeoutSeconds: 300,
});
try {
  let ready = false;
  // Poll for up to one minute (40 attempts, 1.5 seconds apart).
  for (let attempt = 0; attempt < 40; attempt++) {
    const status = (await sandboxes.get(restored.id)).status;
    if (status === "running") {
      ready = true;
      break;
    }
    if (status === "errored" || status === "terminated") {
      throw new Error(`Restored sandbox is ${status}`);
    }
    await new Promise((resolve) => setTimeout(resolve, 1500));
  }
  if (!ready) throw new Error(`Sandbox ${restored.id} is not running yet`);

  console.log(`Ready to use: ${restored.id}`);
  // Run your workload here before the sandbox is terminated.
} finally {
  await sandboxes.terminate(restored.id);
}
```

To create a runtime snapshot, pass `kind: "runtime"` to `snapshots.create`. When you restore it, pass `plan: snapshot.plan` to `sandboxes.create`.

The examples set a five-minute lifetime for the restored sandbox and terminate it after the workload completes. They do not delete the snapshot, so you can reuse it. If a polling deadline expires, check the snapshot or sandbox status before retrying because the operation might still complete.

### List and delete snapshots

**Tab: CLI**

```shell
# List snapshots that are ready to restore.
render ea sandboxes snapshots list --status available

# Preview deletion, then confirm it.
render ea sandboxes snapshots delete snp-abc123
render ea sandboxes snapshots delete snp-abc123 --confirm
```

By default, `snapshots get`, `snapshots list`, and `snapshots delete` use the active workspace's default sandbox group. Pass `--group sbg-...` to use a different group.

**Tab: TypeScript SDK**

```typescript
import { Render } from "@renderinc/sdk";

const sandboxes = new Render().experimental.sandboxes;
const groups = await sandboxes.listGroups();
const sandboxGroup = groups[0]?.sandboxGroup;
if (!sandboxGroup) throw new Error("No sandbox group found");

// List snapshots that are ready to restore.
const snapshots = await sandboxes.snapshots.list({
  sandboxGroupId: sandboxGroup.id,
  status: ["available"],
});

// Delete a snapshot by ID.
await sandboxes.snapshots.delete({
  sandboxGroupId: sandboxGroup.id,
  snapshotId: "snp-abc123",
});
```

The SDK requires a sandbox group ID for snapshot lookup, listing, and deletion. `listGroups()` returns zero or one group during the early access period.

**Tab: Python SDK**

```python
import asyncio

from render import RenderAsync


async def main():
    sandboxes = RenderAsync().experimental.sandboxes
    groups = await sandboxes.list_groups()
    if not groups.groups:
        raise RuntimeError("No sandbox group found")
    sandbox_group = groups.groups[0]

    # List snapshots that are ready to restore.
    snapshots = await sandboxes.snapshots.list(
        sandbox_group_id=sandbox_group.id,
        status="available",
    )

    # Delete a snapshot by ID.
    await sandboxes.snapshots.delete(
        sandbox_group_id=sandbox_group.id,
        snapshot_id="snp-abc123",
    )


asyncio.run(main())
```

The SDK requires a sandbox group ID for snapshot lookup, listing, and deletion. `list_groups()` returns zero or one group during the early access period.

You cannot delete a snapshot while its status is `creating`. Deleting a snapshot prevents future restores but does not affect sandboxes that you already restored from it. After the snapshot's `expires_at` (Python) or `expiresAt` (TypeScript), you can no longer retrieve or restore it. Snapshot lists omit deleted and expired snapshots.

## References

For detailed command and SDK reference material, see:

- [Sandboxes CLI reference](sandboxes-cli-reference)
- [Sandboxes SDK for TypeScript](sandboxes-sdk-typescript)
- [Sandboxes SDK for Python](sandboxes-sdk-python)

## Early access limits

> *These limits are subject to change during the early access period.*

Render enforces the following resource limits for sandboxes. As indicated below, Render can raise certain limits on request.

| Property | Early access limit | Can be raised? |
| --- | --- | --- |
| *CPU per sandbox* | 2 | ☑️ |
| *Memory per sandbox* | 4 GB | ☑️ |
| *Filesystem storage per sandbox* | 10 GB | ☑️ |
| *Concurrent sandboxes per workspace* | 100 | ☑️ |
| *API request rate* | 100 requests / minute | ☑️ |
| *Sandbox groups per workspace* | 1 | ➖ |
| *Timeout per sandbox* | 24 hours | ➖ |
| *Supported regions* | 1 (Oregon) | ➖ |

## Feedback and support

During the early access period, share feedback with us via email or in your shared Slack channel. To help us investigate quickly, include:

- Your workspace ID
- The sandbox ID
- The command or SDK method you ran
- What you expected to happen
- What happened instead, including relevant output or error messages