# Migrate from Beam Sandboxes to Sandboxer

> Port Beam Sandbox code to Sandboxer's Python SDK. Create, exec, file and port calls map closely. Images become setup scripts or named snapshots, memory snapshots become stop and resume, and expose_port becomes a hosted HTTPS port. Copy files with sbx scp. Keep GPU work on Beam, because Sandboxer has no GPUs.

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

**Short answer:** Port Beam Sandbox code to Sandboxer's Python SDK. Create, exec, file and port calls map closely. Images become setup scripts or named snapshots, memory snapshots become stop and resume, and expose_port becomes a hosted HTTPS port. Copy files with sbx scp. Keep GPU work on Beam, because Sandboxer has no GPUs.

This page covers Beam **Sandboxes**: `Sandbox(...).create()`, `sb.process`, `sb.fs`, `expose_port` and snapshots. It doesn't cover Beam's VMs, endpoints or GPU tasks. If your sandboxes need a GPU, stay on Beam: Sandboxer has no GPU sizes ([Machines](/docs/machines)).

## Concept map

Facts about Beam come from its [sandbox overview](https://docs.beam.cloud/v2/sandbox/overview) and the pages linked below, checked on 2026-10-27.

| Beam | Sandboxer |
| - | - |
| Sandbox: "a container you control through process, filesystem, networking, and snapshot APIs" ([overview](https://docs.beam.cloud/v2/sandbox/overview)) | Sandbox: a Cloud Hypervisor microVM with its own kernel, systemd and `sudo` ([Machines](/docs/machines)) |
| `cpu` and `memory` per sandbox; default 1 CPU and 128 MiB | Four fixed sizes: `small` (2 vCPU, 4 GB), `default`, `large`, `xlarge` |
| `Image(python_version=...).add_python_packages([...])` | Ubuntu 24.04 base, plus a setup script or a named snapshot (`from_=`) |
| `keep_warm_seconds`: default 600 s, `-1` until you terminate it | `ttl_seconds`: default 1 hour, up to 30 days, or none on a paid plan ([Long-running](/docs/long-running)) |
| `sb.terminate()` ends the sandbox | `sb.delete()`, or `sb.stop()` to keep it for later |
| `create_image_from_filesystem()` returns an image ID for `Image.from_id` | `sb.snapshot("name")`, then `client.create(from_="name")` ([Snapshots](/docs/snapshots#named-snapshots-templates)) |
| `snapshot_memory()` returns a checkpoint ID for `create_from_memory_snapshot`, "on supported runtimes" | Every stop saves memory; `sb.resume()` brings back the same sandbox, with memory on a machine with the same CPU type |
| Several sandboxes from one memory checkpoint, each with its own identity | `sb.fork()`: a copy of the disk, booted fresh with a new identity (no memory) |
| `expose_port(8000)` or `ports=[...]`, public URLs | `sb.host(8000)`: `https://<slug>-8000.box.sandboxer.com`, private with a token by default ([Hosting](/docs/hosting)) |
| `env={...}`, and brokered `secrets=[...]` | `env={...}` per sandbox, or an environment (`sbx env`) for every sandbox ([Environments](/docs/environments)) |
| `docker_enabled=True` plus `Image(...).with_docker()` | Docker with BuildKit and Compose in every sandbox |
| `gpu`, `gpu_count` | No GPUs |
| `Volume`, `CloudBucket`, `DurableDisk` | The sandbox's own disk, kept across stop and resume |
| `Sandbox().connect(container_id)` | `client.get(sandbox_id)` |
| Python and TypeScript SDKs, `beam` CLI | Python and TypeScript SDKs (`sandboxer-sdk`), the `sbx` CLI and the REST API |

## API and SDK mapping

Beam's calls are from its [processes](https://docs.beam.cloud/v2/sandbox/processes), [filesystem](https://docs.beam.cloud/v2/sandbox/filesystem), [networking](https://docs.beam.cloud/v2/sandbox/networking) and [snapshots](https://docs.beam.cloud/v2/sandbox/snapshots) pages. Ours are from `sandboxer-sdk` 0.1.1 ([Python SDK](/docs/sdk-python)), which we ran on prod for this page.

| Beam (Python) | Sandboxer (Python) | `sbx` CLI |
| - | - | - |
| `Sandbox(cpu=1, memory="1Gi", keep_warm_seconds=600).create()` | `client.create(type="small", ttl_seconds=600)` (waits until ready) | `sbx new --type small --ttl 600` |
| `Sandbox(image=Image.from_id(image_id)).create()` | `client.create(from_="name")` | `sbx new --from name` |
| `sb.process.run_code("print(6 * 7)")` | `sb.exec(["python3", "-c", "print(6 * 7)"])` | `sbx exec <id> -- python3 -c "print(6 * 7)"` |
| `p = sb.process.exec("ls", "-la")`, `p.wait()`, `p.stdout.read()` | `run = sb.exec("ls -la")`, `run.stdout`, `run.exit_code` | `sbx exec <id> "ls -la"` |
| `sb.process.exec(...)` left running (a server) | `sb.start("...")`, then `attach()`, `status()`, `kill()` | `sbx exec <id> --detach "..."` |
| `for line in p.logs:` | `for frame in sb.exec_stream(...):` | `sbx exec` streams by default |
| `sb.fs.write_text(path, text)`, `sb.fs.read_text(path)` | `sb.files.write(path, text)`, `sb.files.read(path)` | |
| `sb.fs.upload_file("input.csv", "/workspace/input.csv")` | `sb.files.write("workspace/input.csv", open("input.csv", "rb").read())` | `sbx scp ./input.csv <id>:/home/user/workspace/` |
| `sb.fs.download_file(remote, local)` | `sb.files.download(path)` (up to 50 MiB) | `sbx scp <id>:/home/user/workspace/out.csv .` |
| `sb.expose_port(8000)`, `sb.list_urls()` | `sb.host(8000, public=True).url` | `sbx host <id> 8000 --public` |
| `sb.update_ttl(600)` | `sb.extend(600)` | `sbx extend <id> --ttl 600` |
| `sb.snapshot_memory()`, `create_from_memory_snapshot(id)` | `sb.stop()`, `sb.resume()` | `sbx stop <id>`, `sbx resume <id>` |
| `sb.create_image_from_filesystem()` | `sb.snapshot("name")` | `sbx snapshot <id> name` |
| `sb.terminate()` | `sb.delete()` | `sbx delete <id> --yes` |

Two habits change. Beam's `process.exec` takes the command as separate arguments and returns a handle; our `exec` takes a shell string or one list and returns the finished result, and `start` is the call for a process that keeps running. And Beam ends a sandbox when `keep_warm_seconds` runs out, while ours stops it with memory and disk kept, so you can resume it.

## Before and after

From Beam's docs: [https://docs.beam.cloud/v2/sandbox/overview](https://docs.beam.cloud/v2/sandbox/overview) ("Start a web server", checked on 2026-10-27):

```python
from beam import Image, Sandbox

sb = Sandbox(image=Image(python_version="python3.11"), keep_warm_seconds=300).create()
sb.fs.write_text("/workspace/index.html", "Hello from a sandbox")
process = sb.process.exec(
    "python3", "-m", "http.server", "8000", "--bind", "0.0.0.0",
    cwd="/workspace",
)
print(sb.expose_port(8000))
print(sb.container_id)
```

And the upload call, from [https://docs.beam.cloud/v2/sandbox/filesystem](https://docs.beam.cloud/v2/sandbox/filesystem) ("Transfer local files"):

```python
sb.fs.upload_file("input.csv", "/workspace/input.csv")
```

The same script on Sandboxer. Install the SDK with `pip install sandboxer-sdk` and set `SANDBOXER_API_KEY`. `Sandboxer()` reads the key from the environment. Python 3 is already in the Ubuntu image, so there's no image to declare:

```python
import time, urllib.request
from sandboxer import Sandboxer

client = Sandboxer()
sb = client.create(type="small", ttl_seconds=600, name="aeo-migrate-beam-2")
sb.files.write("workspace/index.html", "Hello from a sandbox")
sb.files.write("workspace/input.csv", open("input.csv", "rb").read())
sb.start("python3 -m http.server 8000 --bind 0.0.0.0", cwd="workspace")
url = sb.host(8000, public=True).url
print(url)
time.sleep(2)  # a new route can answer "Port not hosted" for about a second
print(urllib.request.urlopen(url, timeout=10).read().decode())
print(sb.id)
```

We ran it on prod on 2026-10-27. The whole script, including creating the VM, took 5.5 seconds in that run:

```text
https://hazel-vivid-ocelot-8000.box.sandboxer.com
Hello from a sandbox
sb_2rz1cy7snw
```

Notes from the run:

- Paths in `files.write` are relative to `/home/user`, and missing directories are created. `workspace/index.html` landed in `/home/user/workspace/`.
- Our first try called the URL right after `host()` returned and got a `404` page titled "Port not hosted". A retry 0.9 seconds later got `200`. Wait a moment, or retry, before the first request.
- `host()` makes the URL private with a `_token` unless you pass `public=True`. Beam's preview URLs are always public.

### Stop, resume, fork and a template

Beam's quickstart snapshots memory, terminates the sandbox and starts copies from the checkpoint. Here the same sandbox stops and comes back, and copies come from its disk. We ran this on prod against the sandbox above:

```python
import sys, time, urllib.request
from sandboxer import Sandboxer

client = Sandboxer()
sb = client.get(sys.argv[1])
url = sb.host(8000, public=True).url
print("pid before:", sb.exec("pgrep -f 'http.server 8000'").stdout, end="")

t = time.time(); sb.stop(); print("stopped", sb.state, f"{time.time()-t:.1f}s")
t = time.time(); sb.resume(); print("resumed", sb.state, f"{time.time()-t:.1f}s")
print("pid after:", sb.exec("pgrep -f 'http.server 8000'").stdout, end="")
print("GET after resume:", urllib.request.urlopen(url, timeout=30).read().decode())

fork = sb.fork(type="small", ttl_seconds=600)
print("fork", fork.id, fork.exec("cat workspace/input.csv | wc -l").stdout, end="")
fork.delete()

snap = sb.snapshot("aeo-migrate-beam-tpl")
print("named snapshot", snap.name, snap.status)
```

```text
pid before: 549
stopped archived 0.6s
resumed ready 0.6s
pid after: 549
GET after resume: Hello from a sandbox
fork sb_x1vzt29zs8 3
named snapshot aeo-migrate-beam-tpl ready
```

What this shows:

- The web server kept the same PID (549) across `stop()` and `resume()`, and the same URL answered right away. After the resume the sandbox showed `memoryRestored: true`. Memory comes back only when the resume lands on a machine with the same CPU type. Otherwise the sandbox cold-boots from its disk: files are kept, running processes are not, and it shows `memoryRestored: false` with `resumeReason: "cpu_mismatch"` ([Snapshots](/docs/snapshots#resume)).
- The fork had the uploaded CSV (3 lines). A fork copies the **disk only**, never memory or running processes, and boots with a new id and subdomain ([Snapshots](/docs/snapshots)). Start your server again in the fork, or `systemctl enable` it so it starts on boot. If you need several copies with processes already running, as Beam's memory checkpoints give you, that has no direct counterpart here.
- The named snapshot is the counterpart of `create_image_from_filesystem()`. Start new sandboxes from it with `client.create(from_="aeo-migrate-beam-tpl")` or `sbx new --from aeo-migrate-beam-tpl`. It's kept until you delete it (`sbx snapshot rm <name>`).

## Move your data and environment

**Files.** A Beam sandbox can't move over. Copy out what you need with `sb.fs.download_file(...)` ([Beam docs](https://docs.beam.cloud/v2/sandbox/filesystem)), or read it from your Volume. Then send it to the new sandbox with `sbx scp`. We ran these on prod against `sb_2rz1cy7snw`:

```bash
tar czf proj.tgz proj
sbx scp ./proj.tgz sb_2rz1cy7snw:/home/user/                              # exit 0
sbx exec sb_2rz1cy7snw "tar xzf proj.tgz && python3 proj/main.py"         # hi from my beam project
sbx exec sb_2rz1cy7snw --cwd /home/user/workspace -- wc -l input.csv      # 3 input.csv
sbx scp sb_2rz1cy7snw:/home/user/workspace/input.csv ./back.csv           # and back out
```

For a directory, use `sbx scp -r`. The SDK's `files.write` and `files.read` take files up to 5 MiB, and `files.download` up to 50 MiB.

**Volumes and durable disks.** Our docs don't describe a volume shared by many sandboxes or a mounted bucket. A sandbox's disk is kept across stop and resume, so data you put there stays. To give many sandboxes the same data, put it in a named snapshot and start them `from_=` it, or download it in a setup script.

**Images.** Turn your Beam `Image` into a shell script. `add_python_packages(["requests"])` becomes `pip install --user --break-system-packages requests`, and system packages become `sudo apt-get install -y ...`. Pass the script as `setup_script=` (up to 64 KB). It runs in the background once the sandbox is ready; pass `wait_for_setup=True` to have `create` return after it finishes ([Long-running](/docs/long-running#setup-scripts)). If installs are slow, run the script once, save the result with `sb.snapshot("web-stack")`, and start later sandboxes with `client.create(from_="web-stack")`. Drop `with_docker()` and `docker_enabled=True`: Docker is already installed.

**Snapshots and checkpoints.** Beam image IDs and checkpoint IDs don't carry across. Rebuild a template from a setup script as above. Then pick the counterpart by what you used the Beam snapshot for:

| You used Beam's | To | Use on Sandboxer |
| - | - | - |
| Memory snapshot | Pause one sandbox and come back to it | `sb.stop()` and `sb.resume()`: memory kept on the same CPU type, otherwise a cold boot with files kept |
| Memory snapshot | Start many copies with processes running | `sb.fork()` (disk only), then start the processes |
| Filesystem snapshot | A reusable base image | `sb.snapshot("name")`, then `from_="name"` |

**Secrets.** Beam's brokered `secrets=[...]` hand the guest a placeholder and substitute the real value in HTTPS headers to approved hosts ([Beam docs](https://docs.beam.cloud/v2/sandbox/configuration)). We don't have that: values you pass are visible inside the sandbox. Use `env={...}` on `create` for one sandbox, or an environment for values every sandbox should get: `sbx env set-var base KEY=value` or `POST /secrets` ([Environments](/docs/environments)).

**Ports.** Replace `expose_port(port)` and `ports=[...]` with `sb.host(port)` or `sbx host <id> <port>`. The URL is private with a `_token` by default; pass `public=True` (CLI `--public`) to open it. The name stays the same across stop and resume. If your server listens on `127.0.0.1` only, use `host(port, localhost=True)`; Beam requires `0.0.0.0`.

**Lifetimes.** Beam's default is 600 seconds and the sandbox ends. Ours is 1 hour, and the sandbox stops instead of ending. Set `ttl_seconds` on create, call `sb.extend(ttl_seconds)` in place of `update_ttl`, or pass `ttl_seconds=None` on a paid plan to turn auto-stop off. During the trial the most is 8 hours. Synchronous `exec` calls time out after at most 600 seconds; use `sb.start(...)` for longer jobs ([Long-running](/docs/long-running)).

## What's different

What you gain:

- **Memory survives a stop of the same sandbox.** In our run the server kept its PID and URL through `stop()` and `resume()`. Memory comes back when the resume lands on a machine with the same CPU type; otherwise the sandbox boots from disk with files kept. On Beam, a memory snapshot is restored into a new sandbox with its own identity and URLs ([Beam docs](https://docs.beam.cloud/v2/sandbox/snapshots)).
- **A full machine.** Every sandbox is a VM with systemd, Docker and `/dev/kvm`. On our `small` sandbox, `docker --version` printed `Docker version 29.9.0` and `/dev/kvm` was present, with no image changes.
- **Longer lifetimes and idle pause.** A paid-plan sandbox can run with no time limit. `sbx new --idle 60` pauses it with memory after an hour without activity and wakes it on the next use ([Long-running](/docs/long-running#idle-auto-pause-and-wake-on-use)).
- **A desktop.** Every sandbox has a streamed desktop with Chrome ([Desktop](/docs/desktop)).

What you lose or must change:

- **GPUs.** Beam sandboxes can request a GPU (`gpu="RTX4090"`). Sandboxer has none: on our sandbox, `nvidia-smi` was not found and there was no `/dev/nvidia*`. Keep GPU work on Beam.
- **Sizing.** Beam takes any `cpu` and `memory`. We sell four fixed sizes. You can grow a sandbox on resume (`sbx resume <id> --type large`), and a size change cold-boots from disk. Moving to a smaller size isn't supported yet: to go smaller, create a new sandbox of that size and copy your files.
- **Outbound network rules.** Beam has `block_network`, allowlists of up to 64 hostnames, IPs or CIDRs, and runtime changes ([Beam docs](https://docs.beam.cloud/v2/sandbox/networking)). Our docs describe fixed blocks (other sandboxes, internal networks, SMTP port 25), not per-sandbox allowlists ([Security](/docs/security)).
- **Brokered secrets.** See above: there's no placeholder substitution here.
- **Start volume.** Our plans limit starts: 12 a minute, 60 an hour and 200 a day on the $20 plan, up to 2,400 a day on the $2,000 plan ([Billing](/docs/billing#plans)). Creates, resumes and forks each count. Check with `sbx limits` before a burst.
- **Process API.** Beam's `exec` accepts `stdin` and returns a handle you `wait()` on. Ours runs to completion (`exec`) or in the background (`start`, with `attach`, `status` and `kill`).

## Pricing

Beam's sandbox rates come from its [pricing page](https://www.beam.cloud/pricing), checked on 2026-10-27: $0.0000375 per physical core per second (one core is 2 vCPU) and $0.0000064 per GiB of memory per second, billed only while the sandbox runs. Ours come from [/pricing](/pricing).

| Machine | Beam Sandboxes | Sandboxer |
| - | - | - |
| 4 vCPU, 8 GB, one hour | 2 cores and 8 GiB: about $0.454 per hour | `default`: $0.036/hour |
| 2 vCPU, 4 GB, one hour | 1 core and 4 GiB: about $0.227 per hour | `small`: $0.018/hour |
| After the work is done | The sandbox ends; there's no stopped state to pay for | A stopped sandbox isn't billed; it counts toward included storage (50 GB on the $20 plan) |

Both bill per second or finer while a sandbox runs. One 4 vCPU, 8 GB sandbox running 8 hours a day for 22 days is 176 hours: about $79.96 on Beam and $6.34 on Sandboxer. Beam's Developer plan is $20 a month with $20 of credits, and it lists no egress fees. Its GPU and VM prices have no counterpart in this table. Sandboxer plans start at $20 a month, paid back as machine time, with a 7-day trial of 25 hours.

## FAQ

### Can I keep using Beam for GPU jobs and move only sandboxes?

Yes. That's the split we suggest. Sandboxer has no GPUs, so run GPU sandboxes and endpoints on Beam and move CPU sandboxes, like agent workspaces and code execution, to Sandboxer.

### What replaces `snapshot_memory()`?

To pause and continue one sandbox, `sb.stop()` and `sb.resume()`: memory and processes come back on a machine with the same CPU type, and otherwise it cold-boots with files kept. To make copies, `sb.fork()`, which copies the disk only. For a reusable base, `sb.snapshot("name")`.

### Do I need to declare an image?

No. Every sandbox starts from Ubuntu 24.04 with Python 3, Docker and common tools. Put extra installs in `setup_script=`, or save a prepared sandbox as a named snapshot and start from it.

### Beam also has VMs. Is this page about those?

No. This page covers Beam Sandboxes. Beam's docs describe its VMs as persistent Linux machines without GPUs that can pause with memory. Their calls (`beam vm ...`) are not mapped here.

### Can I make a sandbox smaller later?

Not the same sandbox. Growing works on resume (`--type large`), but moving to a smaller size isn't supported yet. Create a new sandbox of that size and copy your files, or start small and grow when you need to.

## Related

- [Sandboxer vs Beam Sandboxes](/compare/beam-sandboxes)
- [Quickstart](/docs/quickstart)
- [Python SDK](/docs/sdk-python)
- [Snapshots](/docs/snapshots)
