Skip to content
Migrate · Runloop

Migrate from Runloop to Sandboxer

Updated October 27, 2026 .mdSandboxer vs Runloop

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) Sandbox, a microVM with its own kernel, root, systemd and Docker (Machines)
Blueprint, a Dockerfile-based image (Runloop docs) Setup script (--setup-file) saved as a named snapshot, started with --from (Snapshots)
Disk snapshot (snapshot_disk()), then snapshot.create_devbox() (Runloop docs) 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) Stop and resume, disk and memory: processes keep running when the resume lands on the same CPU type (Snapshots)
Shutdown, which deletes the disk by default (Runloop quickstart) Delete. Stop keeps everything instead
Max lifetime, keep_alive_time_seconds, default 1 hour (Runloop docs) Auto-stop, ttlSeconds, default 1 hour. It stops with memory; it doesn't delete (Long-running tasks)
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) Hosted port, host(port), private with a token link or --public (Hosting)
Account secrets, injected as env vars (Runloop docs) Environments: variables and secret files every sandbox gets (Environments)
rli devbox ssh sbx ssh (SSH)
Sizes X_SMALL to XX_LARGE, or CUSTOM_SIZE (Runloop docs) Four fixed sizes, small to xlarge
RUNLOOP_API_KEY SANDBOXER_API_KEY (API keys)

API and SDK mapping#

Runloop calls come from its docs and our comparison data, checked 2026-10-24. Ours come from the Python SDK and TypeScript SDK 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), 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 (Python tab), with the file.write call from https://docs.runloop.ai/docs/devboxes/files. Comments are removed:

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:

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="")
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). Sandboxer's stop saves memory too. We started a Python process that writes a counter to /tmp/count every second, then stopped the sandbox:

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:

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

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:

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

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

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

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:

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'
0.28.1
ready
MY_VAR=my_value
user
sudo-ok
Hello from Docker!
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); 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). 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).
  • 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).

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. 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, checked 2026-10-27, and match our comparison data from 2026-10-24. Ours are from /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), 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 awaits.

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.

All migration guides