# Migrate from Runloop to Sandboxer

> Swap @runloop/api-client or runloop_api_client for sandboxer-sdk. devbox.create() becomes create(), cmd.exec() becomes exec(), file.write() becomes files.write(), and suspend and resume become stop and resume, which keep memory and processes on a matching CPU type. Blueprints become a setup script saved as a named snapshot. Forks copy disk only.

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

**Short answer:** Swap @runloop/api-client or runloop_api_client for sandboxer-sdk. devbox.create() becomes create(), cmd.exec() becomes exec(), file.write() becomes files.write(), and suspend and resume become stop and resume, which keep memory and processes on a matching CPU type. Blueprints become a setup script saved as a named snapshot. Forks copy disk only.

## Concept map

| Runloop | Sandboxer |
| - | - |
| Devbox, isolated with "virtual machine technology" ([Runloop docs](https://docs.runloop.ai/docs/devboxes/overview)) | Sandbox, a microVM with its own kernel, root, systemd and Docker ([Machines](/docs/machines)) |
| Blueprint, a Dockerfile-based image ([Runloop docs](https://docs.runloop.ai/docs/devboxes/blueprints/overview)) | Setup script (`--setup-file`) saved as a named snapshot, started with `--from` ([Snapshots](/docs/snapshots#named-snapshots-templates)) |
| Disk snapshot (`snapshot_disk()`), then `snapshot.create_devbox()` ([Runloop docs](https://docs.runloop.ai/docs/devboxes/snapshots)) | Named snapshot for "start many from here", or `fork` for "copy this one now". Both copy the disk; new sandboxes boot fresh |
| Suspend and resume, disk only: processes must be restarted ([Runloop docs](https://docs.runloop.ai/docs/devboxes/lifecycle)) | Stop and resume, disk **and memory**: processes keep running when the resume lands on the same CPU type ([Snapshots](/docs/snapshots#resume)) |
| Shutdown, which deletes the disk by default ([Runloop quickstart](https://docs.runloop.ai/docs/tutorials/quickstart)) | Delete. Stop keeps everything instead |
| Max lifetime, `keep_alive_time_seconds`, default 1 hour ([Runloop docs](https://docs.runloop.ai/docs/devboxes/start-stop)) | Auto-stop, `ttlSeconds`, default 1 hour. It stops with memory; it doesn't delete ([Long-running tasks](/docs/long-running)) |
| `lifecycle.after_idle` with `on_idle: "suspend"` | Idle auto-pause (`idleStopMinutes`), on by default, keeps memory and wakes on use |
| Tunnel, `get_tunnel_url(port)`, `auth_mode` open or authenticated ([Runloop docs](https://docs.runloop.ai/docs/devboxes/tunnels)) | Hosted port, `host(port)`, private with a token link or `--public` ([Hosting](/docs/hosting)) |
| Account secrets, injected as env vars ([Runloop docs](https://docs.runloop.ai/docs/devboxes/configuration/account-secrets)) | Environments: variables and secret files every sandbox gets ([Environments](/docs/environments)) |
| `rli devbox ssh` | `sbx ssh` ([SSH](/docs/ssh)) |
| Sizes `X_SMALL` to `XX_LARGE`, or `CUSTOM_SIZE` ([Runloop docs](https://docs.runloop.ai/docs/devboxes/configuration/sizes)) | Four fixed sizes, `small` to `xlarge` |
| `RUNLOOP_API_KEY` | `SANDBOXER_API_KEY` ([API keys](/docs/api-keys)) |

## API and SDK mapping

Runloop calls come from its [docs](https://docs.runloop.ai/llms.txt) and our [comparison data](/compare/runloop), checked 2026-10-24. Ours come from the [Python SDK](/docs/sdk-python) and [TypeScript SDK](/docs/sdk-typescript) docs. The table uses the Python names; TypeScript uses the camelCase forms on both sides.

| Runloop (Python) | Sandboxer SDK | `sbx` CLI |
| - | - | - |
| `AsyncRunloopSDK()` | `Sandboxer()` (`from sandboxer import Sandboxer`) | |
| `runloop.devbox.create()` | `client.create(type="small")` | `sbx new --type small` |
| `devbox.create(blueprint_name="my-env")` | `client.create(from_="my-env")` | `sbx new --from my-env` |
| `launch_parameters={"keep_alive_time_seconds": 1800}` | `ttl_seconds=1800` | `sbx new --ttl 1800` |
| `devbox.cmd.exec("ls")` | `sandbox.exec("ls")` | `sbx exec <id> "ls"` |
| `devbox.cmd.exec_async("npm run dev")` | `sandbox.start("npm run dev")` | `sbx exec <id> --detach "npm run dev"` |
| `devbox.file.write(file_path=..., contents=...)` | `sandbox.files.write(path, contents)` | |
| `devbox.file.read(file_path=...)` | `sandbox.files.read(path)` | |
| `devbox.file.upload(...)` | `sandbox.files.write(path, data)` (up to 5 MiB) | `sbx scp ./file <id>:/home/user/` |
| `devbox.suspend()` | `sandbox.stop()` | `sbx stop <id>` |
| `devbox.resume()` | `sandbox.resume()` | `sbx resume <id>` |
| `devbox.snapshot_disk()`, then `snapshot.create_devbox()` | `sandbox.fork()` | `sbx fork <id>` |
| Blueprint build | `sandbox.snapshot("my-env")` after a setup script | `sbx snapshot <id> my-env` |
| `get_tunnel_url(8080)` | `sandbox.host(8080)` | `sbx host <id> 8080` |
| `devbox.shutdown()` | `sandbox.delete()` | `sbx delete <id> --yes` |

Two behaviors carry over. Each Runloop `exec` runs in a new shell ([Runloop docs](https://docs.runloop.ai/docs/devboxes/execute-commands)), and so does each Sandboxer `exec`: chain commands with `&&`, or pass `cwd` and `env` to `exec`. Runloop's named shells have no direct equivalent; use `sbx ssh` or `pty()` for an interactive session. Sandboxer's `exec` waits 1 to 600 seconds (default 30), so start servers and long jobs with `start` or `--detach`.

## Before and after

From Runloop's docs: [https://docs.runloop.ai/docs/tutorials/quickstart](https://docs.runloop.ai/docs/tutorials/quickstart) (Python tab), with the `file.write` call from [https://docs.runloop.ai/docs/devboxes/files](https://docs.runloop.ai/docs/devboxes/files). Comments are removed:

```python
import asyncio
from runloop_api_client import AsyncRunloopSDK

runloop = AsyncRunloopSDK()

async def run_example():
  devbox = await runloop.devbox.create()
  print(f'Created Runloop Devbox: {devbox.id}')
  result = await devbox.cmd.exec(command="echo 'Runloop!!'")
  print(await result.stdout())
  print(f'Exit code: {result.exit_code}')
  await devbox.file.write(file_path="/home/user/main.py", contents='print("Hello, World!")')
  await devbox.shutdown()

asyncio.run(run_example())
```

The same steps with `sandboxer-sdk` 0.1.1 (`pip install sandboxer-sdk`). We ran this on prod on 2026-10-27:

```python
from sandboxer import Sandboxer

client = Sandboxer()  # reads SANDBOXER_API_KEY
sandbox = client.create(type="small", ttl_seconds=1800, name="aeo-migrate-runloop-1")
print(f"Created sandbox: {sandbox.id}")

result = sandbox.exec("echo 'Sandboxer!!'")
print(result.stdout, end="")
print(f"Exit code: {result.exit_code}")

sandbox.files.write("/home/user/main.py", 'print("Hello, World!")')
print(sandbox.exec("python3 main.py").stdout, end="")
```

```text
Created sandbox: sb_fvmwea1ien
Sandboxer!!
Exit code: 0
Hello, World!
```

In this single run (2026-10-27) the script took 3.5 seconds, including the create, which waits until the sandbox is ready. Two changes from the Runloop version: `stdout` is a string, not a coroutine, and `exec` runs in `/home/user`, so `python3 main.py` finds the file. We left out `delete()` here to keep using the sandbox below; call `sandbox.delete()` where you called `shutdown()`, or pass `on_exit="delete"` and use `with client.create(...) as sandbox:`.

### Suspend and resume, with memory

Runloop's suspend keeps the disk only, and "daemons or other processes running at suspend time must be manually restarted after resuming" ([Runloop docs](https://docs.runloop.ai/docs/devboxes/lifecycle)). Sandboxer's stop saves memory too. We started a Python process that writes a counter to `/tmp/count` every second, then stopped the sandbox:

```bash
sbx scp ./counter.py sb_fvmwea1ien:/home/user/
sbx exec sb_fvmwea1ien --detach "python3 counter.py"
# {"ok":true,"type":"command.started",…,"pid":559,…}
sbx exec sb_fvmwea1ien "cat /tmp/count"    # 9
sbx stop sb_fvmwea1ien                      # {"ok":true,"type":"sandbox.stopping",…}
```

Once it showed `archived`, we resumed it. In this single run the command returned in 1.3 seconds:

```bash
sbx resume sb_fvmwea1ien
# {"event":"ready","id":"sb_fvmwea1ien","state":"ready",…}
sbx exec sb_fvmwea1ien "ps -o pid,etime,cmd -C python3; cat /tmp/count"
```

```text
    PID     ELAPSED CMD
    559       00:34 python3 counter.py
11
```

Same PID, and the count carried on from where it stopped. This holds when the resume lands on a machine with the same CPU type. If none is free within 2 minutes, the resume boots from disk instead, keeps your files but not your processes, and reports `memoryRestored: false` with `resumeReason: "cpu_mismatch"`. Pass `sbx resume --require-memory` to wait for a matching machine instead ([Snapshots](/docs/snapshots#resume)).

### Snapshot fan-out becomes fork

Runloop's docs describe snapshotting a devbox and starting several devboxes from it to try different approaches. On Sandboxer that is one call. A fork copies the disk and boots fresh, like a devbox started from a Runloop disk snapshot:

```python
base = client.get("sb_fvmwea1ien")
fork = base.fork(ttl_seconds=900)
fork.update(name="aeo-migrate-runloop-2")
print(f"Forked {base.id} -> {fork.id}")
```

```text
Forked sb_fvmwea1ien -> sb_duhgoah9hf
```

The fork script took about 3 seconds in this single run. Inside it, the files were there, including `notes.txt`, which we wrote moments before the fork. The counter was not running, and `/tmp/count` was gone:

```bash
sbx exec sb_duhgoah9hf "ps -o pid,cmd -C python3 || echo no python3 process; cat /tmp/count 2>/dev/null || echo no /tmp/count; cat notes.txt"
```

```text
    PID CMD
no python3 process
no /tmp/count
attempt-base
```

Only stop and resume keep memory. Forks and named snapshots carry the disk.

### Tunnels become hosted ports

```bash
sbx exec sb_fvmwea1ien --detach "python3 -m http.server 8080"
sbx host sb_fvmwea1ien 8080 --public
# {"ok":true,"type":"port.hosted","port":8080,"url":"https://lofty-calm-glade-8080.box.sandboxer.com","access":"public",…}
curl https://lofty-calm-glade-8080.box.sandboxer.com/main.py
# print("Hello, World!")
sbx delete sb_fvmwea1ien --yes
```

Without `--public`, the URL is private and carries a `_token`, which is close to Runloop's `authenticated` mode. Runloop requires services to bind `0.0.0.0`. On Sandboxer an app bound to `0.0.0.0` works, and for one bound to `127.0.0.1` you pass `--localhost` ([Hosting](/docs/hosting)).

## Move your data and environment

**Files.** Devboxes don't move between providers. Read what you need with `file.download`, or tar the folder in the devbox and download the archive. Upload it with `sbx scp` (`-r` for a folder) and untar it with `sbx exec`. Small files can go through `files.write`, up to 5 MiB each.

**Blueprints.** A blueprint is a Dockerfile. Turn its `RUN` steps into a setup script, create a sandbox with it, and save the result as a named snapshot:

```bash
cat setup.sh
# #!/bin/bash
# set -e
# pip install --user --break-system-packages -q httpx
# mkdir -p /home/user/agent && echo ready > /home/user/agent/STATUS

sbx new --type small --name aeo-migrate-runloop-3 --ttl 900 --setup-file ./setup.sh -e MY_VAR=my_value
sbx info sb_zdw52fwl3p          # …"setupStatus":"done"…
sbx exec sb_zdw52fwl3p 'python3 -c "import httpx;print(httpx.__version__)"; cat agent/STATUS; echo MY_VAR=$MY_VAR; whoami; sudo -n true && echo sudo-ok; docker run --rm hello-world 2>&1 | grep -m1 Hello'
```

```text
0.28.1
ready
MY_VAR=my_value
user
sudo-ok
Hello from Docker!
```

```bash
sbx snapshot sb_zdw52fwl3p aeo-migrate-runloop-tpl
# {"ok":true,"type":"snapshot.named.info","snapshot":{…,"name":"aeo-migrate-runloop-tpl",…,"status":"ready",…}}
```

New sandboxes then start from it with `sbx new --from aeo-migrate-runloop-tpl`, or `client.create(from_="aeo-migrate-runloop-tpl")` in the Python SDK. Commands run on Ubuntu as `user`, with passwordless `sudo`, not inside a Docker build, so `COPY` steps become `sbx scp` or files fetched by the script. Runloop's Docker-in-Docker needs one of its `dnd` blueprints ([Runloop docs](https://docs.runloop.ai/docs/devboxes/capabilities/docker-in-docker)); Docker is preinstalled in every Sandboxer sandbox, as the `hello-world` run above shows. A named snapshot doesn't capture running processes: start services from the setup script or as enabled systemd services. Named snapshots count toward your plan's included storage, so delete the ones you stop using with `sbx snapshot rm <name>`.

**Secrets.** Runloop maps account secrets to env vars at create time (`secrets={"DEVBOX_SECRET": "SECRET_NAME"}`). On Sandboxer, set them once in an environment and every new sandbox gets them: `sbx env set-var base KEY=value` for variables, and `sbx env set-file base path --from ./file` for secret files ([Environments](/docs/environments)). For one sandbox, pass `-e KEY=value` on `sbx new`.

**Ports and preview URLs.** `https://{port}-{tunnel_key}.tunnel.runloop.ai` becomes `https://<subdomain>-<port>.box.sandboxer.com`. Update WebSocket origin allow-lists and dev-server allowed hosts to `.box.sandboxer.com`.

## What's different

What you gain:

- **Memory across a stop.** Running processes, shells and in-memory state come back after a resume on a matching CPU. Runloop's suspend keeps disk only.
- **Stop and resume on every paid plan.** Runloop lists suspend and resume on its $250-a-month Pro plan ([Runloop pricing](https://runloop.ai/pricing)).
- **Safer defaults.** Auto-stop and idle pause both keep the disk, and memory too when the resume lands on a matching CPU type. On Runloop, shutdown deletes the disk unless you set up suspend.
- **A full Linux machine.** Docker, systemd and `/dev/kvm` for nested VMs are in every sandbox, with no special image.
- **Coding agents.** Claude Code, Codex and others are installed and driven through `sandbox.prompt()` ([Agents](/docs/agents)).

What you lose or must change:

- **VPC deployment.** Runloop's Enterprise plan deploys into your VPC. Sandboxer is hosted only.
- **Custom sizes and arm64.** Runloop lets you set CPU and memory per devbox, up to 16 cores and 64 GiB, and offers arm64 blueprints. Sandboxer has four fixed x86 sizes, from `small` (2 vCPU, 4 GB) to `xlarge` (16 vCPU, 32 GB).
- **Platform extras.** Runloop's Axons, Agent Gateways, MCP Hub, network policies and storage objects have no direct equivalent here. Plan those parts of your stack separately.
- **Memory in snapshots and forks.** Neither side keeps memory in a snapshot. Forks boot fresh.
- **Region.** Sandboxer has two regions, us-east (Ashburn, Virginia) and eu-central (Helsinki, Finland); see [regions](/docs/regions). New sandboxes go to the nearest one unless you set `region`.
- **Trial.** Runloop's trial gives $50 of credit with no card. Sandboxer's trial is 7 days and 25 hours and needs a card.

## Pricing

Runloop's prices are from its [pricing page](https://runloop.ai/pricing), checked 2026-10-27, and match our [comparison data](/compare/runloop) from 2026-10-24. Ours are from [/pricing](/pricing).

| | Runloop | Sandboxer |
| - | - | - |
| Compute | $0.108 per CPU-hour plus $0.0252 per GB-hour, billed per second | Fixed sizes, from $0.018/hour for `small`, billed per second |
| 4 vCPU and 8 GB | $0.6336 per hour (4 × $0.108 + 8 × $0.0252), plus disk storage | `default` size: $0.036/hour |
| Plans | Basic: free plus usage. Pro: $250 a month plus usage. Enterprise: contact sales | From $20 a month, all returned as machine time |
| Suspend and resume | Pro and Enterprise | Every paid plan |
| Storage | Devbox disk $0.00034236 per GB-hour; 100 GB free on Basic, 1 TB on Pro | Not billed; 50 GB included on the $20 plan |
| Trial | $50 credit, no card, 3 devboxes at once, up to Medium size | 7 days and 25 hours, 2 at once, needs a card |

Example: one 4 vCPU, 8 GB machine running 8 hours a day for 22 days is 176 hours. On Runloop that is about $111.51 of compute, plus $250 for Pro if you need suspend and resume. On Sandboxer, the `default` size costs 176 × $0.036 = $6.34, covered by the $20 plan's included time. The sizes don't match exactly: Runloop's preset `X_LARGE` is 4 CPU with 16 GB at $0.8407 an hour ([Runloop docs](https://docs.runloop.ai/docs/devboxes/configuration/sizes)), so the 8 GB figure assumes `CUSTOM_SIZE`.

## FAQ

### Do I have to rewrite my Runloop code?

You change the import, the client and some method names. The flow stays the same: create, exec, read and write files, suspend, resume and shut down. The tables above map each call, and Sandboxer's Python SDK is synchronous, so you drop the `await`s.

### Will my processes survive a stop?

Yes, when the resume lands on a machine with the same CPU type. In our test a Python process kept its PID and its count. Otherwise the resume boots from disk and reports `memoryRestored: false`; pass `--require-memory` to wait instead.

### What replaces my blueprints?

A setup script plus a named snapshot. Turn the Dockerfile's steps into shell commands, run `sbx new --setup-file`, then save the result with `sbx snapshot <id> <name>` and start new sandboxes with `--from <name>`.

### Does a fork copy memory?

No. A fork copies the disk and boots fresh with a new id, like a devbox started from a Runloop disk snapshot. Use stop and resume when you need memory back.

### Can I deploy Sandboxer in my own VPC?

No. Sandboxer is hosted only. If VPC deployment is a requirement, Runloop's Enterprise plan offers it.

## Related

- [Sandboxer vs Runloop](/compare/runloop)
- [Quickstart](/docs/quickstart)
- [Python SDK](/docs/sdk-python)
- [Snapshots](/docs/snapshots)
