Migrate from Fly.io Sprites and Machines to Sandboxer
Updated October 27, 2026 .mdSandboxer vs Fly.io
Short answer: Port Sprite or Machine code to Sandboxer's TypeScript or Python SDK. Create, exec, files and delete map closely. Checkpoints become named snapshots, suspend becomes a stop that keeps memory on a same-CPU-type machine, and the Sprite URL or fly proxy becomes an HTTPS URL per port. Copy files with sbx scp. Sandboxer has two regions and no GPUs.
Fly.io has two products people use as agent sandboxes. Sprites are persistent Linux microVMs that pause when idle, with checkpoints and one HTTPS URL each. Machines are fast-starting VMs you drive through the Machines API, often one per user for running user code (Fly.io guide). This page covers both. It doesn't cover Fly Launch apps, Managed Postgres or GPU Machines.
Concept map#
Facts about Fly.io come from the pages linked here, checked on 2026-10-27.
| Fly.io | Sandboxer |
|---|---|
| Sprite: a microVM with 8 vCPUs, platform-managed memory and 100 GB of storage (lifecycle) | Sandbox: a Cloud Hypervisor microVM in one of four fixed sizes, from 2 vCPU / 4 GB to 16 vCPU / 32 GB (Machines) |
| Machine: a Firecracker VM from your OCI image, inside a Fly App | Sandbox from the Ubuntu 24.04 base, plus a setup script or a named snapshot. No app object |
Sprite goes warm after about 30 s idle (memory frozen), then cold (memory dropped) |
Idle auto-pause: a stop with memory after idleStopMinutes (15 to 120 by plan), woken on use (Long-running) |
Machine suspend (POST .../suspend): memory snapshot, best effort, ≤ 2 GB memory recommended |
stop: memory and disk saved on every stop, at every size; resume restores it on a machine with the same CPU type |
| Machine stop and start: a cold boot, filesystem changes cleared | stop and resume: the same disk and, when the CPU type matches, the same processes |
Sprite checkpoint (sprite checkpoint create), restored in place |
Named snapshot (sbx snapshot <id> name), started as a new sandbox with --from (Snapshots) |
Automatic checkpoints (auto- IDs, pruned over time) |
Continuous snapshots every 10 minutes and at each stop; the latest two are kept |
fly machine clone |
sbx fork: a copy-on-write copy of the disk with a new identity |
Sprite URL https://<name>-<org>.sprites.app, one HTTP service |
https://<slug>-<port>.box.sandboxer.com for any port you host (Hosting) |
--url-auth sprite (org members or token) / public |
Private with a _token (the default) / --public |
sprite proxy 5432, fly proxy |
SSH port forwarding (ssh -L) or a hosted HTTPS port (SSH) |
| Fly Volumes and volume snapshots | The sandbox disk, kept across stop and resume; no separate volume object |
fly secrets, Machine env |
env per sandbox, or an environment for every sandbox (Environments) |
Regions ("region": "syd", fly machine clone --region) |
us-east or eu-central, set with region; a fork stays in its source's region |
SPRITE_TOKEN, FLY_API_TOKEN |
SANDBOXER_API_KEY (API keys) |
| JavaScript, Go and Elixir Sprites SDKs; Machines REST API | TypeScript and Python SDKs (sandboxer-sdk), the sbx CLI and the REST API |
API and SDK mapping#
Sprite calls are from Fly.io's Sprites quickstart and checkpoints page. Machine calls are from the Machines resource reference. Ours are from sandboxer-sdk 0.1.1 (TypeScript SDK), which we ran on prod for this page.
| Fly.io | Sandboxer (TypeScript) | sbx CLI or REST |
|---|---|---|
client.createSprite('my-sprite'), sprite create |
client.create({ name: "my-sandbox" }) (waits until ready) |
sbx new --name my-sandbox |
POST /v1/apps/{app}/machines with config.image |
client.create({ type: "small", from: "my-template" }) |
POST /api/v1/sandboxes |
sprite.execFile('python', [...]), sprite exec -- |
sandbox.exec(["python3", ...]) |
sbx exec <id> -- python3 ... |
sprite.spawn(...) and cmd.stdout |
sandbox.execStream(...) |
sbx exec streams by default |
POST .../machines/{id}/exec |
sandbox.exec(...) |
POST /api/v1/sandboxes/{id}/exec |
| Services (restarted on cold wake) | sandbox.start(...) for background work; systemd units for services |
sbx exec <id> --detach "..." |
sprite checkpoint create --comment ... |
sandbox.snapshot("name") |
sbx snapshot <id> name |
sprite restore v1 |
client.create({ from: "name" }) |
sbx new --from name |
POST .../suspend, then POST .../start |
sandbox.stop(), then sandbox.resume() |
sbx stop <id>, sbx resume <id> |
GET .../wait?state=suspended |
sandbox.stop({ wait: true }) |
GET /api/v1/sandboxes/{id}/stops/{stopId} |
fly machine clone |
sandbox.fork() |
sbx fork <id> |
sprite info (URL), sprite config update --url-auth public |
sandbox.host(8080, { public: true }) |
sbx host <id> 8080 --public |
sprite proxy 5432 |
SSH with -L 5432:localhost:5432 |
sbx ssh <id> |
sprite.delete(), DELETE .../machines/{id} |
sandbox.delete() |
sbx delete <id> --yes |
Before and after#
Sprites#
From Fly.io's docs: https://docs.fly.io/sprites/quickstart.md (the JavaScript SDK example, checked on 2026-10-27; its two comment lines removed):
import { SpritesClient } from '@fly/sprites';
const client = new SpritesClient(process.env.SPRITE_TOKEN);
const sprite = await client.createSprite('my-sprite');
const result = await sprite.execFile('python', ['-c', "print('hello')"]);
console.log(result.stdout);
const cmd = sprite.spawn('bash', ['-c', 'for i in {1..10}; do date +%T; sleep 0.5; done']);
for await (const line of cmd.stdout) {
process.stdout.write(line);
}
await sprite.delete();The same script on Sandboxer. Install with npm install sandboxer-sdk and set SANDBOXER_API_KEY; new Sandboxer() reads it from the environment:
import { Sandboxer } from "sandboxer-sdk";
const client = new Sandboxer();
const sandbox = await client.create({ type: "small", ttlSeconds: 600, name: "aeo-migrate-fly-1" });
const result = await sandbox.exec(["python3", "-c", "print('hello')"]);
console.log(result.stdout);
const cmd = await sandbox.execStream(["bash", "-c", "for i in {1..10}; do date +%T; sleep 0.5; done"]);
for await (const frame of cmd) {
if (frame.type === "stdout") process.stdout.write(frame.data);
}
console.log(sandbox.id, (await sandbox.delete()).status);We ran it on prod on 2026-10-27:
hello
02:57:44
02:57:44
02:57:45
…
02:57:48
02:57:48
sb_krtpkqp30s pendingpython is python3 on our Ubuntu image. There's no sandbox name to pass: name is optional, and every sandbox gets an id and a three-word subdomain. delete() returns a deletion operation that finishes in the background, so its status is pending at first; a GET on the sandbox returned 404 afterwards.
Machines#
From Fly.io's docs: https://docs.fly.io/reference/suspend-resume.md (the Machines API section, checked on 2026-10-27):
# Suspend
POST /v1/apps/{app_name}/machines/{machine_id}/suspend
# Wait for suspension to complete
GET /v1/apps/{app_name}/machines/{machine_id}/wait?state=suspended
# Resume (standard start endpoint)
POST /v1/apps/{app_name}/machines/{machine_id}/startThe same calls on Sandboxer's REST API, run on prod against a sandbox with a web server running:
API=https://sandboxer.com/api/v1; ID=sb_x2ieasvt22 # your sandbox id; AUTH="Authorization: Bearer $SANDBOXER_API_KEY"
# Stop (memory and disk are saved)
STOP=$(curl -sS -X POST "$API/sandboxes/$ID/stop" -H "$AUTH" | jq -r .stop.id); echo "$STOP"
# Wait for the stop to complete
until curl -sS "$API/sandboxes/$ID/stops/$STOP" -H "$AUTH" | jq -e '.stop.status == "completed"' >/dev/null; do sleep 1; done
# Resume, then wait until ready
curl -sS -X POST "$API/sandboxes/$ID/resume" -H "$AUTH" | jq -r .status
until curl -sS "$API/sandboxes/$ID" -H "$AUTH" | jq -e '.sandbox.state == "ready"' >/dev/null; do sleep 1; done
curl -sS "$API/sandboxes/$ID" -H "$AUTH" | jq -c '.sandbox | {state, memoryRestored, resumeReason}'stop_80b74f4c13bc9852
resuming
{"state":"ready","memoryRestored":true,"resumeReason":null}Checkpoints, ports, stop and resume#
This script covers what a Sprite usually does next: install a package, serve a file on a port, save a checkpoint, pause, and restore. We ran it on prod on 2026-10-27; the timings are from that single run:
import { Sandboxer } from "sandboxer-sdk";
const client = new Sandboxer();
const t = () => performance.now();
const secs = (t0) => ((performance.now() - t0) / 1000).toFixed(1) + "s";
const sb = await client.create({ type: "small", ttlSeconds: 1800, name: "aeo-migrate-fly-2", env: { MY_SECRET: "hello" } });
console.log("created", sb.id, sb.state, sb.subdomain);
await sb.exec("pip install --user --break-system-packages -q requests", { timeoutSeconds: 120 });
await sb.files.write("app/index.html", "hello from a sandbox\n");
// A hand-started server, like `sprite exec -- python -m http.server 8080`
await sb.start("python3 -m http.server 8080", { cwd: "app" });
await new Promise((r) => setTimeout(r, 1000));
const h = await sb.host(8080, { public: true });
console.log("url:", h.url);
console.log("GET:", (await (await fetch(h.url)).text()).trim());
console.log("pid before:", (await sb.exec("pgrep -f 'http.server 8080'")).stdout.trim());
// Checkpoint -> named snapshot
let t0 = t(); const snap = await sb.snapshot("aeo-migrate-fly-tpl");
console.log("named snapshot", snap.name, snap.status, secs(t0));
// Stop with memory, resume
t0 = t(); await sb.stop({ wait: true }); await sb.refresh(); console.log("stopped", sb.state, secs(t0));
t0 = t(); await sb.resume(); console.log("resumed", sb.state, secs(t0), "memoryRestored:", sb.info.memoryRestored);
console.log("pid after:", (await sb.exec("pgrep -f 'http.server 8080'")).stdout.trim());
console.log("GET after resume:", (await (await fetch(h.url)).text()).trim());
// "Restore" a checkpoint: start a new sandbox from the named snapshot
await sb.exec("rm -rf app");
t0 = t(); const sb3 = await client.create({ from: "aeo-migrate-fly-tpl", type: "small", ttlSeconds: 900, name: "aeo-migrate-fly-3" });
console.log("from snapshot", sb3.id, secs(t0), (await sb3.exec("cat app/index.html; python3 -c 'import requests; print(requests.__version__)'")).stdout.trim());
console.log("ids", sb.id, sb3.id);created sb_x2ieasvt22 ready ocean-local-lemon
url: https://ocean-local-lemon-8080.box.sandboxer.com
GET: <!doctype html><html><head>…<title>Not found</title>…
pid before: 551
named snapshot aeo-migrate-fly-tpl ready 1.1s
stopped archived 0.6s
resumed ready 0.6s memoryRestored: true
pid after: 551
GET after resume: hello from a sandbox
from snapshot sb_l5rr5spif1 2.6s hello from a sandbox
2.31.0
ids sb_x2ieasvt22 sb_l5rr5spif1What this shows:
- Retry the first request to a new URL. Our first
GETran within a second ofhost()and got the edge's "Not found" page. A probe on another port of the same sandbox got404at 0.4 seconds and200at 1.1 seconds. Retry for a few seconds after hosting a port. A Sprite's URL is always on, so this step is new (Fly.io docs). - The hand-started server survived the pause. It kept PID 551 across
stop()andresume(), and the URL answered right after. A cold Sprite would have restarted it, or dropped it if it wasn't a Service. In the REST run above (single run, 2026-10-27), the stop, wait and resume took 2.7 seconds in total, and both servers in that sandbox kept their PIDs (551 and 583). - The checkpoint restored as a new sandbox. After
rm -rf appin the original, the sandbox started fromaeo-migrate-fly-tplhad the file and the installedrequests2.31.0. It was ready 2.6 seconds aftercreatewas called. - Paths in
files.writeandfiles.readare relative to/home/user./home/sprite/...paths become/home/user/....
Move your data and environment#
Files. A Sprite or Machine can't move over as is. Pack what you need with tar inside it, copy the archive to your computer, then send it to the new sandbox with sbx scp:
tar czf proj.tgz proj
sbx scp ./proj.tgz sb_x2ieasvt22:/home/user/ # exit 0
sbx exec sb_x2ieasvt22 "tar xzf proj.tgz && python3 proj/main.py" # hi from my sprite project
sbx scp sb_x2ieasvt22:/home/user/proj/main.py ./back.py # and back outFor 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.
Images and Sprite setup. A Sprite starts with Node.js, Python, Go and Git preinstalled, and you install the rest by hand (quickstart). A Machine boots your OCI image. On Sandboxer, every sandbox starts from Ubuntu 24.04 with Docker, Python, Node.js and the coding agents installed. Turn your install steps (or your Dockerfile's RUN lines) into a shell script and pass it as a setup script (sbx new --setup-file ./setup.sh, up to 64 KB, Setup scripts). If installs are slow, run them once, save the result as a named snapshot, and start later sandboxes with --from. If you'd rather keep your image, run it with Docker inside the sandbox.
Checkpoints. Sprite checkpoints don't carry across. Take a named snapshot once the new sandbox is set up. Two habits change. A Sprite restore replaces the filesystem in place and restarts the Sprite; a named snapshot starts a new sandbox, and the original is untouched. And named snapshots are kept until you delete them, while automatic Sprite checkpoints are pruned over time.
Services. Sprites bring back Services after a cold wake. Here a resume normally keeps every process, so hand-started servers come back too. For a server that must also survive a cold boot (a size change, or a resume on a machine with a different CPU type), install it as a systemd unit and systemctl enable it.
Secrets. Replace fly secrets set or a Machine's env with env on create (sbx new -e KEY=value) for one sandbox. For values every sandbox should get, use an environment: sbx env set-var base KEY=value (Environments).
Ports. A Sprite URL routes to one HTTP service (port 8080 by default), and other ports go through sprite proxy (networking). A Machine needs a services block and an app IP. Here you host any port at any time with sbx host <id> <port> or sandbox.host(port). The URL is private with a _token by default; pass --public to open it. It keeps its name across stop and resume. If your server listens on 127.0.0.1 only, add --localhost. For raw TCP, like a Postgres client, use SSH port forwarding (SSH).
Network policy. Sprites support a DNS allowlist for egress. Our docs describe fixed blocks (other sandboxes, internal networks, SMTP port 25), not per-sandbox allowlists (Security). If you rely on an egress allowlist, plan for that gap.
What's different#
What you gain:
- Memory on every stop. A cold Sprite drops in-memory state, and a Machine suspend is best effort and not recommended above 2 GB of memory (Fly.io docs). Here every stop saves memory, at every size. In our run, the web server kept the same PID across
stop()andresume(). - CPU-type caveat. Memory is restored when the resume lands on a machine with the same CPU type. Otherwise the sandbox boots from disk (files intact, hand-started processes gone) and shows
memoryRestored: falsewithresumeReason: "cpu_mismatch". PassrequireMemory: true(sbx resume --require-memory) to wait for a matching machine instead (Snapshots). So, as on Fly.io, design for the cold path too. - Any port on its own URL. A Sprite URL covers one HTTP service. Here each hosted port gets its own HTTPS URL.
- A full machine. Every sandbox has systemd, Docker with Compose,
/dev/kvmfor nested VMs, and a streamed desktop with Chrome (Desktop). - Forks.
sbx forkcopies a sandbox's disk in O(1) for parallel attempts, with a new identity.
What you lose or must change:
- Regions. 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, and forks stay in their source's region. Fly.io Machines run in many regions, and you pick one per Machine (Fly.io docs). If you place sandboxes near users around the world, that doesn't carry over. - GPUs. Fly.io's docs show GPU Machines, for example an L40S in its Whisper guide. Sandboxer has no GPU sizes. Keep GPU work on Fly.io.
- Sizing. Sprite memory is managed and can grow under pressure, and Machines take custom CPU and memory. We sell four fixed sizes. You can grow a sandbox on resume. Moving to a smaller size isn't supported yet: to go smaller, create a new sandbox of that size and copy your files.
- Usage-based compute. Sprites bill measured CPU and memory, so a mostly idle running Sprite costs little. We bill by size for every second a sandbox runs.
- Start volume. Our plans limit starts: 12 a minute, 60 an hour and 200 a day on the $20 plan (Billing). Creates, resumes and forks each count. Check with
sbx limitsbefore a burst. - Egress allowlists, as above.
- SDK languages. Fly.io has Go and Elixir Sprites SDKs. We have TypeScript and Python. From Go or Elixir, call the REST API.
Pricing#
Fly.io's rates come from its Sprites page and resource pricing, checked on 2026-10-27. Ours come from /pricing.
| Usage | Fly.io | Sandboxer |
|---|---|---|
| 4 vCPU, 8 GB, one busy hour | Sprite at full use: 4 × $0.0385 per CPU-hour + 8 × $0.021875 per GB-hour = $0.329 | default: $0.036/hour |
| 2 vCPU, 4 GB, one hour | Machine performance-2x with 4 GB: $66.00 per 30 days, about $0.092 per hour (iad) |
small: $0.018/hour |
| Idle or stopped | Sprite: no compute charge while warm or cold; storage $0.50/GB-month hot, $0.02/GB-month cold. Stopped or suspended Machine: $0.15/GB-month of rootfs | A stopped sandbox isn't billed; it counts toward included storage (50 GB on the $20 plan) |
The numbers aren't a like-for-like match. A Sprite bills the CPU and memory it actually uses, so a running Sprite that mostly waits costs well below $0.329 an hour. A Sandboxer sandbox bills its full size while it runs, and idle auto-pause stops that billing. Fly.io Machines have no monthly platform fee. Sprites plans, such as Hero at $100 a month, include usage allowances, and new organizations get $30 of trial credit. Sandboxer plans start at $20 a month, paid back as machine time, with a 7-day trial of 25 hours.
FAQ#
Does a resumed sandbox act like a warm or a cold Sprite?#
Like a warm one, in most cases: processes pick up where they stopped. The difference is that it stays that way however long it's stopped. If the resume lands on a machine with a different CPU type, it boots from disk like a cold Sprite and reports memoryRestored: false.
How do I restore a checkpoint in place?#
There's no in-place restore. Start a new sandbox with sbx new --from <name>, check it, then delete the old one. The original stays untouched until you do.
Can I pick a region like on Fly.io?#
Yes, between two: us-east (Ashburn, Virginia) and eu-central (Helsinki, Finland). Set region when you create a sandbox; without it, a new sandbox goes to the region nearest your IP's country. Forks, creates from a snapshot or share code, and resumes stay in their source's region. See regions.
Can I keep using Fly.io for GPUs?#
Yes. Sandboxer has no GPUs, so keep GPU Machines on Fly.io and move CPU sandboxes, like agent workspaces and code execution, to Sandboxer.
Do I need an app or an org slug to create a sandbox?#
No. An API key is enough. To bill a team, pass org to the client.