# Migrate from Fly.io Sprites and Machines to Sandboxer

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

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

**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](https://docs.fly.io/sprites/index.md) are persistent Linux microVMs that pause when idle, with checkpoints and one HTTPS URL each. [Machines](https://docs.fly.io/machines/overview.md) are fast-starting VMs you drive through the Machines API, often one per user for running user code ([Fly.io guide](https://docs.fly.io/machines/guides-examples/functions-with-machines.md)). 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](https://docs.fly.io/sprites/concepts/lifecycle.md)) | Sandbox: a Cloud Hypervisor microVM in one of four fixed sizes, from 2 vCPU / 4 GB to 16 vCPU / 32 GB ([Machines](/docs/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](/docs/long-running#idle-auto-pause-and-wake-on-use)) |
| 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](/docs/snapshots#named-snapshots-templates)) |
| 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](/docs/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](/docs/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](/docs/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](/docs/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](https://docs.fly.io/sprites/quickstart.md) and [checkpoints page](https://docs.fly.io/sprites/concepts/checkpoints.md). Machine calls are from the [Machines resource reference](https://docs.fly.io/machines/api/machines-resource.md). Ours are from `sandboxer-sdk` 0.1.1 ([TypeScript SDK](/docs/sdk-typescript)), 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](https://docs.fly.io/sprites/quickstart.md) (the JavaScript SDK example, checked on 2026-10-27; its two comment lines removed):

```javascript
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:

```javascript
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:

```text
hello

02:57:44
02:57:44
02:57:45
…
02:57:48
02:57:48
sb_krtpkqp30s pending
```

`python` 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](https://docs.fly.io/reference/suspend-resume.md) (the Machines API section, checked on 2026-10-27):

```bash
# 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}/start
```

The same calls on Sandboxer's REST API, run on prod against a sandbox with a web server running:

```bash
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}'
```

```text
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:

```javascript
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);
```

```text
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_l5rr5spif1
```

What this shows:

- **Retry the first request to a new URL.** Our first `GET` ran within a second of `host()` and got the edge's "Not found" page. A probe on another port of the same sandbox got `404` at 0.4 seconds and `200` at 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](https://docs.fly.io/sprites/concepts/networking.md)).
- **The hand-started server survived the pause.** It kept PID 551 across `stop()` and `resume()`, 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 app` in the original, the sandbox started from `aeo-migrate-fly-tpl` had the file and the installed `requests` 2.31.0. It was ready 2.6 seconds after `create` was called.
- Paths in `files.write` and `files.read` are 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`:

```bash
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 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.

**Images and Sprite setup.** A Sprite starts with Node.js, Python, Go and Git preinstalled, and you install the rest by hand ([quickstart](https://docs.fly.io/sprites/quickstart.md)). 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](/docs/long-running#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](https://docs.fly.io/sprites/concepts/services.md) 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](/docs/environments)).

**Ports.** A Sprite URL routes to one HTTP service (port 8080 by default), and other ports go through `sprite proxy` ([networking](https://docs.fly.io/sprites/concepts/networking.md)). 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](/docs/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](/docs/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](https://docs.fly.io/reference/suspend-resume.md)). Here every stop saves memory, at every size. In our run, the web server kept the same PID across `stop()` and `resume()`.
- **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: false` with `resumeReason: "cpu_mismatch"`. Pass `requireMemory: true` (`sbx resume --require-memory`) to wait for a matching machine instead ([Snapshots](/docs/snapshots#resume)). 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/kvm` for nested VMs, and a streamed desktop with Chrome ([Desktop](/docs/desktop)).
- **Forks.** `sbx fork` copies 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](/docs/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](https://docs.fly.io/machines/overview.md)). 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](https://docs.fly.io/deep-dive/whisper.md). 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](/docs/billing#plans)). Creates, resumes and forks each count. Check with `sbx limits` before 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](/docs/api).

## Pricing

Fly.io's rates come from its [Sprites page](https://fly.io/sprites) and [resource pricing](https://docs.fly.io/about/pricing.md), checked on 2026-10-27. Ours come from [/pricing](/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](/docs/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.

## Related

- [Sandboxer vs Fly.io Sprites](/compare/fly-sprites)
- [Quickstart](/docs/quickstart)
- [TypeScript SDK](/docs/sdk-typescript)
- [Snapshots](/docs/snapshots)
