# Migrate from Cloudflare Sandbox to Sandboxer

> Replace getSandbox() and the Sandbox Durable Object with sandboxer-sdk calls, keyed by a stored sandbox id. Swap the Dockerfile for our Ubuntu image plus a setup script or named snapshot, and exposePort() for host(port). Stopping keeps the disk, and memory on a same-CPU resume. Billing moves from active CPU plus provisioned memory to a flat per-second price.

Source: https://sandboxer.com/migrate/cloudflare-sandbox. Updated 2026-10-27.

**Short answer:** Replace getSandbox() and the Sandbox Durable Object with sandboxer-sdk calls, keyed by a stored sandbox id. Swap the Dockerfile for our Ubuntu image plus a setup script or named snapshot, and exposePort() for host(port). Stopping keeps the disk, and memory on a same-CPU resume. Billing moves from active CPU plus provisioned memory to a flat per-second price.

## Concept map

Cloudflare facts come from the [Cloudflare Sandbox docs](https://developers.cloudflare.com/sandbox/), read on 2026-10-27. Cloudflare now documents two versions of `@cloudflare/sandbox`. In 0.x, the package's `Sandbox` class is your Durable Object, and you call `getSandbox()`, `exec()`, `writeFile()` and `exposePort()`. In 1.0, you write the Durable Object yourself and call `this.ctx.container` ([Changes in Sandbox SDK 1.0](https://developers.cloudflare.com/sandbox/sdk/migrate/changes-in-1-0/)). This page maps the 0.x calls. The same Sandboxer code replaces either version.

| Cloudflare Sandbox | Sandboxer |
| - | - |
| A sandbox is a Durable Object, addressed by a name you choose, that starts a container ([lifetime](https://developers.cloudflare.com/sandbox/concepts/lifetime/)) | A sandbox is a Cloud Hypervisor microVM running Ubuntu 24.04, with an id (`sb_…`), an optional `name` and `labels` ([Machines](/docs/machines)) |
| `getSandbox(env.Sandbox, "user-123")` creates on first use, then reaches the same one | `sandboxer.create()` returns an id. Store it, or tag the sandbox with labels and list by them |
| Container image: a `Dockerfile` `FROM docker.io/cloudflare/sandbox:<version>`, built by `wrangler deploy` | One base image with Node.js 22, Python 3 and Docker, plus a setup script, a named snapshot or an [environment](/docs/environments) |
| `instance_type`: `lite` (1/16 vCPU) up to `standard-4` (4 vCPU, 12 GiB) | `type`: `small` 2 vCPU / 4 GB up to `xlarge` 16 vCPU / 32 GB |
| 0.x: sleep after 10 minutes idle (`sleepAfter`); files and processes are lost. 1.0: your app can save a container snapshot that carries files (not processes) to the next start ([lifetime](https://developers.cloudflare.com/sandbox/concepts/lifetime/)) | Idle auto-pause (`idleStopMinutes`): a stop **with memory**, woken by the next request ([Long-running](/docs/long-running)) |
| `keepAlive: true` | `sbx idle <id> 0`, plus `ttlSeconds` (or no auto-stop on paid plans) |
| `createBackup()` / `restoreBackup()` of a directory to R2, default TTL 3 days | Named snapshot of the whole sandbox, kept until you delete it ([Snapshots](/docs/snapshots)) |
| `exposePort()` with `proxyToSandbox()`, on your own wildcard domain | `host(port)` on `https://<subdomain>-<port>.box.sandboxer.com`, private with a token or public ([Hosting](/docs/hosting)) |
| `setEnvVars()`, Worker secrets | `env` on create, or account-wide [environments](/docs/environments) |
| Worker binding `env.Sandbox` | One API key, `sbx_live_…` ([API keys](/docs/api-keys)) |
| `wrangler dev` runs the container in local Docker | No local mode. Every sandbox runs in our cloud |

The 0.x lifecycle facts are from [Sandbox lifecycle](https://developers.cloudflare.com/sandbox/sdk/concepts/sandboxes/). The image tags are from the [Dockerfile reference](https://developers.cloudflare.com/sandbox/sdk/configuration/dockerfile/). Backups are from the [Backups API](https://developers.cloudflare.com/sandbox/sdk/api/backups/).

## API and SDK mapping

Their calls are from the [Sandbox SDK 0.x API reference](https://developers.cloudflare.com/sandbox/sdk/api/). Ours are from `sandboxer-sdk` 0.1.1, which we ran against prod.

| Cloudflare (`@cloudflare/sandbox` 0.x) | Sandboxer SDK | Sandboxer CLI |
| - | - | - |
| `getSandbox(env.Sandbox, id)` (new) | `sandboxer.create({ name, type, ttlSeconds })` | `sbx new --name … --type …` |
| `getSandbox(env.Sandbox, id)` (existing) | `sandboxer.get(id)` | `sbx info <id>` |
| `sandbox.exec("npm test")` | `sandbox.exec("npm test")` | `sbx exec <id> "npm test"` |
| `sandbox.startProcess("node server.js")` | `sandbox.start("node server.js")` | `sbx exec <id> --detach "…"` |
| `sandbox.writeFile(path, content)` | `sandbox.files.write(path, content)` | `sbx scp ./file <id>:/home/user/` |
| `sandbox.readFile(path)` | `sandbox.files.read(path)` | `sbx scp <id>:/home/user/file .` |
| `sandbox.exposePort(8000, { hostname })` | `sandbox.host(8000)` | `sbx host <id> 8000` |
| `sandbox.unexposePort(8000)` | `DELETE /sandboxes/{id}/host?port=8000` | `sbx host hide <id> 8000` |
| `sandbox.setEnvVars({ … })` | `create({ env: { … } })` | `sbx new -e KEY=value` |
| `sandbox.setKeepAlive(true)` | `PATCH /sandboxes/{id}` with `idleStopMinutes: 0` | `sbx idle <id> 0` |
| `sandbox.createBackup()` | `sandbox.snapshot("name")` | `sbx snapshot <id> <name>` |
| `sandbox.restoreBackup()` | `sandboxer.create({ from: "name" })` | `sbx new --from <name>` |
| `sandbox.destroy()` | `sandbox.delete()` | `sbx delete <id> --yes` |
| Sleep (automatic) | `sandbox.stop()`, `sandbox.resume()` | `sbx stop <id> --wait`, `sbx resume <id>` |

Two calls behave differently. Our `exec` waits 30 seconds by default and at most 600, so start servers and long jobs with `start`. And our `create` makes a new sandbox each time. To make it safe to retry, pass `idempotencyKey`: the same key returns the same sandbox for 24 hours ([API](/docs/api#idempotent-creates)).

## Before and after

From Cloudflare's docs: [Sandbox SDK 0.x Getting started](https://developers.cloudflare.com/sandbox/sdk/get-started/). We cut the routing between `/run` and `/file`.

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

export default {
  async fetch(request: Request, env: { Sandbox: DurableObjectNamespace<Sandbox> }) {
    const sandbox = getSandbox(env.Sandbox, "my-sandbox");
    const result = await sandbox.exec('python3 -c "print(2 + 2)"');
    await sandbox.writeFile("/workspace/hello.txt", "Hello, Sandbox!");
    const file = await sandbox.readFile("/workspace/hello.txt");
    return Response.json({ output: result.stdout, content: file.content });
  },
};
```

Exposing a port, from [Container runtime](https://developers.cloudflare.com/sandbox/sdk/concepts/containers/#network-capabilities):

```typescript
const { hostname } = new URL(request.url);
await sandbox.startProcess("python -m http.server 8000");
const exposed = await sandbox.exposePort(8000, { hostname });
```

In production that URL needs a custom domain with wildcard DNS, not `workers.dev` ([Ports API](https://developers.cloudflare.com/sandbox/sdk/api/ports/)). You also need the `Dockerfile`, the `containers` and `durable_objects` blocks in `wrangler.jsonc`, and a migration for the `Sandbox` class.

The same handler on Sandboxer. It is still a Worker-style `fetch` handler, but it needs no bindings, Durable Objects or Dockerfile. `wrangler.jsonc` is just `name`, `main` and `compatibility_date`. Install with `npm i sandboxer-sdk` and set `SANDBOXER_API_KEY` as a Worker secret.

```typescript
import { Sandboxer } from "sandboxer-sdk";

export default {
  async fetch(request: Request, env: { SANDBOXER_API_KEY: string }): Promise<Response> {
    const sandboxer = new Sandboxer({ apiKey: env.SANDBOXER_API_KEY });
    const id = new URL(request.url).searchParams.get("id"); // your stored sandbox id
    const sandbox = id
      ? await sandboxer.get(id)
      : await sandboxer.create({ type: "small", name: "aeo-migrate-cf-1", ttlSeconds: 1800 });

    const run = await sandbox.exec('python3 -c "print(2 + 2)"');
    await sandbox.files.write("workspace/hello.txt", "Hello, Sandbox!");
    const file = await sandbox.files.read("workspace/hello.txt");

    await sandbox.start("python3 -m http.server 8000", { cwd: "workspace" });
    const { url } = await sandbox.host(8000, { public: true });
    return Response.json({ id: sandbox.id, output: run.stdout, exitCode: run.exitCode, file, url });
  },
};
```

We ran it with `wrangler dev` (Wrangler 4.153.0, workerd 2026-10-23) on our own machine, against the Sandboxer API. One request created the sandbox and returned in 3.2 seconds (single run, October 27, 2026):

```json
{"id":"sb_hp3kunhv39","output":"4\n","exitCode":0,"file":"Hello, Sandbox!","url":"https://nimble-cozy-glacier-8000.box.sandboxer.com"}
```

`curl https://nimble-cozy-glacier-8000.box.sandboxer.com/hello.txt` returned `Hello, Sandbox!`. The URL works on any domain you run the Worker on, because it isn't on your zone. We did not deploy this Worker to Cloudflare's network, so treat deployed Workers as untested. The SDK also runs in Node 20+ and Bun ([TypeScript SDK](/docs/sdk-typescript)).

### Stop and resume with the CLI

On Cloudflare, an idle sandbox goes to sleep and the next request gets a new instance: in 0.x a fresh container, in 1.0 one your app can start from a container snapshot with the files but not the processes. On Sandboxer, a stop saves memory too, when the resume lands on a machine with the same CPU type; otherwise it boots from disk with files kept and processes lost ([Snapshots](/docs/snapshots#resume)). We stopped the same sandbox while the Python server ran:

```bash
sbx exec sb_hp3kunhv39 "pgrep -f 'http.server 8000'"   # 529
sbx stop sb_hp3kunhv39 --wait
sbx resume sb_hp3kunhv39
# {"event":"ready","id":"sb_hp3kunhv39","state":"ready",…}
sbx exec sb_hp3kunhv39 "ps -eo pid,args | grep [h]ttp.server"
#     529 python3 -m http.server 8000
```

The resume took about a second (single run, October 27, 2026). The server kept PID 529, and the hosted URL answered `200` with no restart. `sbx stop` without `--wait` returns at once, and a `resume` sent before the stop finishes gets `409 sandbox_stopping`.

### Find a sandbox by your own key

`getSandbox()` finds a sandbox by a name such as `user-123`. On Sandboxer, store the id, or put the key in labels and filter on them. Labels are set on `POST /sandboxes` or `PATCH /sandboxes/{id}`:

```bash
curl -sS -X PATCH https://sandboxer.com/api/v1/sandboxes/sb_hp3kunhv39 \
  -H "Authorization: Bearer $SANDBOXER_API_KEY" -H "Content-Type: application/json" \
  -d '{"labels":{"user":"user-123","app":"aeo-migrate-cf"}}'
curl -sS "https://sandboxer.com/api/v1/sandboxes?labels=user=user-123,app=aeo-migrate-cf" \
  -H "Authorization: Bearer $SANDBOXER_API_KEY"
# one match: sb_hp3kunhv39, name aeo-migrate-cf-1, labels {"app":"aeo-migrate-cf","user":"user-123"}
```

Unlike a Durable Object name, a label is not unique. Two creates with the same labels make two sandboxes. Use `idempotencyKey` for a create you might retry.

## Move your data and environment

**Image.** There is no Dockerfile build. The base image is Ubuntu 24.04 with Node.js, Python 3 and Docker. Put your `RUN` lines in a setup script, or build once and save a named snapshot. We did both:

```bash
sbx snapshot sb_hp3kunhv39 aeo-migrate-cf-tpl
# {"type":"snapshot.named.info","snapshot":{"name":"aeo-migrate-cf-tpl",…}}
sbx new --from aeo-migrate-cf-tpl --name aeo-migrate-cf-2 --type small --ttl 900 \
  -e APP_MODE=staging --setup-file ./setup.sh
# {"event":"ready","id":"sb_23ijnphdwl","state":"ready",…}   (3 seconds)
sbx exec sb_23ijnphdwl 'cat workspace/hello.txt; echo; python3 proj/main.py; echo APP_MODE=$APP_MODE; cat setup.log'
# Hello, Sandbox!
# hi from my project
# APP_MODE=staging
# setup-done
```

`setup.sh` ran a `pip install` in the background after the sandbox was ready, then wrote `setup.log`. A sandbox started from a named snapshot boots from its disk, so the Python server was not running in it. For repos and secrets that every sandbox gets, use an [environment](/docs/environments).

**Paths.** Cloudflare's default working directory is `/workspace`. Ours is `/home/user`, and `files.write` paths are relative to it. Change absolute `/workspace` paths, or keep a `workspace` folder as above.

**Files.** Containers don't move between providers. Read files out with `readFile()`, or restore a backup locally. Then tar them and upload with `sbx scp ./proj.tgz <id>:/home/user/`, which we ran. Use `sbx scp -r` for a directory. `files.write` takes files up to 5 MiB, and `files.download` reads up to 50 MiB.

**Backups.** R2 backups don't carry over. A named snapshot covers the whole disk, not one directory, and has no TTL. It counts toward your plan's included storage ([Billing](/docs/billing#storage)).

**Env vars and secrets.** `setEnvVars()` becomes `env` on create (`-e KEY=value` on the CLI). For values every sandbox needs, use `sbx env set-var base KEY=value` or `sbx env set-file base path --from ./file` ([Environments](/docs/environments)).

**Ports and preview URLs.** Drop `proxyToSandbox()`, the wildcard DNS route and the `EXPOSE` lines. Call `host(port)` once the server is up. URLs are private by default: our `sbx host sb_23ijnphdwl 8000` URL returned `401` without its `_token`. Pass `public: true` or `--public` for an open URL. The subdomain stays the same across stop and resume. A sandbox started from a snapshot gets its own subdomain, so host its ports again.

## What's different

What you gain:

- **Memory on stop.** A resume on a machine with the same CPU type brings back running processes with the same PIDs, as above. Cloudflare's 0.x sleep deletes files and processes. Its 1.0 container snapshots save files only, and expire after 30 days ([lifetime](https://developers.cloudflare.com/sandbox/concepts/lifetime/#snapshots-carry-files-to-the-next-instance)).
- **Work inside the VM keeps it running.** On Cloudflare, code running inside the instance doesn't count as activity, so a long build can stop when the inactivity timeout ends. Our idle pause waits while a command runs or the load average is at least 0.5 ([Long-running](/docs/long-running)). Auto-stop is a deadline you set, and on paid plans you can turn it off.
- **Bigger machines and a full VM.** Up to 16 vCPU and 32 GB, against `standard-4` at 4 vCPU and 12 GiB. Every sandbox has root, Docker, `/dev/kvm`, SSH and a desktop ([Machines](/docs/machines)).
- **Preview URLs without a domain.** You don't need a zone, wildcard DNS or TLS setup.
- **Any caller.** The REST API, the CLI, SSH and the Python SDK work from anywhere, not only from a Worker.

What you lose or must change:

- **Workers integration.** No bindings: the sandbox can't call your KV, R2 or Durable Objects directly. Cloudflare can intercept outbound HTTP in a Worker and add credentials the sandbox never sees. Our docs describe no outbound interception, and sandboxes reach the internet by default ([Security](/docs/security)).
- **Edge placement.** On Cloudflare, the first request decides where a sandbox runs. On Sandboxer, a new sandbox goes to the region nearest the caller's IP country unless you set `region`. Sandboxer has two regions, us-east (Ashburn, Virginia) and eu-central (Helsinki, Finland); see [regions](/docs/regions).
- **Local development.** `wrangler dev` runs Cloudflare containers in Docker on your machine. Every Sandboxer sandbox is a cloud machine.
- **R2 mounts and Dynamic Workers.** Cloudflare mounts R2 buckets and runs JavaScript-only code in Dynamic Workers. We have neither.
- **Billing granularity.** Cloudflare bills in 10 ms steps and charges CPU only when it is busy. Small or bursty sandboxes can cost less there. See below.
- **One bill.** If you already pay for Workers, Sandboxer is a second vendor and a second key.

## Pricing

Cloudflare's rates are from its [Containers pricing page](https://developers.cloudflare.com/containers/platform/pricing/) (last updated 2026-10-05, checked 2026-10-27). Ours are from [/pricing](/pricing). Cloudflare bills active CPU at $0.000020 per vCPU-second ($0.072 per vCPU-hour). It bills provisioned memory at $0.0000025 per GiB-second ($0.009 per GiB-hour) and disk at $0.00000007 per GB-second, while the instance runs. Sandboxer bills a flat rate per second while a sandbox runs.

Cloudflare has no 4 vCPU / 8 GB instance, and custom types need at least 3 GiB per vCPU ([limits](https://developers.cloudflare.com/containers/platform/limits/)). So we compare our `default` size with `standard-4` (4 vCPU, 12 GiB, 20 GB disk):

| One hour | Price |
| - | - |
| Sandboxer `default` (4 vCPU / 8 GB) | $0.036/hour |
| Cloudflare `standard-4`, CPU busy the whole hour | $0.401 (4 × $0.072 + 12 × $0.009 + disk) |
| Cloudflare `standard-4`, CPU busy 10% of the hour | $0.142 (0.4 × $0.072 + 12 × $0.009 + disk) |

Memory is billed on Cloudflare while the instance runs, even when idle. So `standard-4` costs at least $0.113 an hour before any CPU. A machine running 8 hours a day for 22 days (176 hours) costs $6.34 on Sandboxer, and $24.96 to $70.58 on Cloudflare, depending on CPU use. That excludes Workers and Durable Objects charges and the $5 a month Workers Paid plan. The plan includes 375 vCPU-minutes, 25 GiB-hours of memory and 200 GB-hours of disk a month. For many short runs on `lite` or `basic` instances, Cloudflare's 10 ms billing can come out cheaper. Our trial is 7 days with 25 hours and needs a card.

## FAQ

### Does sandboxer-sdk run inside a Cloudflare Worker?

We ran it in `wrangler dev`, which uses the same workerd runtime, and it created a sandbox, ran commands, wrote files and hosted a port. We did not test a deployed Worker. Our test `wrangler.jsonc` had no `nodejs_compat` flag.

### What replaces the Durable Object name in getSandbox()?

The sandbox id that `create` returns. Store it where you kept the name, for example in KV or your database. To look sandboxes up by user, set labels and list with `?labels=user=user-123`. The filter matches sandboxes that carry all the labels you pass.

### Do I still need a Dockerfile?

No. Start from our base image and add a setup script with `--setup-file`, or save a configured sandbox as a named snapshot and create from it with `--from`.

### What happens to an idle sandbox?

It pauses after `idleStopMinutes` (30 minutes on the $20 plan) with its memory saved. A wake restores that memory on a machine with the same CPU type; otherwise it boots from disk with files kept. The next API call or hosted-URL request wakes it. A sandbox you stop yourself stays stopped until you resume it.

### Is there a Python SDK?

Yes. Install `sandboxer-sdk` from PyPI. It imports as `sandboxer` and has the same methods in snake_case ([Python SDK](/docs/sdk-python)).

## Related

- [Sandboxer vs Cloudflare Sandbox](/compare/cloudflare-sandbox)
- [Quickstart](/docs/quickstart)
- [TypeScript SDK](/docs/sdk-typescript)
- [Hosting](/docs/hosting)
- [Snapshots](/docs/snapshots)
