Migrate from E2B
A method-by-method mapping from the E2B SDK to PandaStack — sandboxes, commands, files, pause/resume, and templates — plus an honest list of what actually changes.
E2B and PandaStack solve the same problem: isolated, fast-booting sandboxes for AI agents and untrusted code, both built on Firecracker microVMs. The core concepts map almost one-to-one, so most migrations are a mechanical rename pass — the SDKs even share the sandbox.commands.run(...) spelling.
This guide is a straight mapping, not a sales pitch. Where E2B has something we don't, we say so.
Setup
| E2B | PandaStack | |
|---|---|---|
| Python package | e2b / e2b-code-interpreter | pandastack |
| JS/TS package | e2b / @e2b/code-interpreter | @pandastack/sdk |
| API key env var | E2B_API_KEY | PANDASTACK_API_KEY |
| CLI | e2b (npm) | pandastack (npm, npm i -g @pandastack/sdk) |
| API base | E2B's API | https://api.pandastack.ai (Authorization: Bearer $PANDASTACK_API_KEY) |
pip install pandastack # Python SDK
npm install @pandastack/sdk # TypeScript SDK
npm install -g @pandastack/sdk # CLI ("pandastack" command)
export PANDASTACK_API_KEY=pds_... # or: pandastack auth loginCheat sheet
The full mapping, Python spellings (TypeScript is the same shape in camelCase):
| E2B | PandaStack | Notes |
|---|---|---|
Sandbox() / Sandbox.create() | Sandbox.create(template="code-interpreter") | Template picked at create time. |
Sandbox(template="my-tpl") | Sandbox.create(template="my-tpl") | Same idea. |
sbx.commands.run(cmd) | sandbox.exec(cmd) — or sandbox.commands.run(cmd) | The commands.run alias exists in both PandaStack SDKs for drop-in compatibility. |
sbx.commands.run(cmd, on_stdout=...) | sandbox.exec_stream(cmd, on_stdout=...) | SSE-backed streaming. |
sbx.run_code(code) | ctx = sandbox.create_code_context() then ctx.run_code(code) | Persistent kernel with rich MIME results (PNG, HTML, JSON). |
sbx.files.read(path) | sandbox.filesystem.read(path) | Python returns bytes. |
sbx.files.write(path, data) | sandbox.filesystem.write(path, data) | Accepts str or bytes. |
sbx.files.list(dir) | sandbox.filesystem.listdir(dir) (TS: filesystem.list) | Returns FileInfo entries. |
sbx.files.exists(path) | sandbox.filesystem.exists(path) | |
sbx.pause() | sandbox.hibernate() | Memory + disk snapshotted to storage, VM stops billing RAM. |
Sandbox.resume(sandbox_id) | sandbox.wake() — or just send any request | Hibernated sandboxes auto-wake on the next request. |
Sandbox.connect(sandbox_id) | Sandbox.get(sandbox_id) | Reattach to a running sandbox by id. |
sbx.set_timeout(seconds) | sandbox.set_ttl(seconds) | TTL-based lifetime. |
sbx.kill() | sandbox.kill() | Same name. |
e2b template build (e2b.toml) | pandastack template build --name my-tpl -f Dockerfile | Server-side build from a plain Dockerfile; no config file format. |
| — | sandbox.fork() / sandbox.fork_tree(n) | Copy-on-write clones of a running sandbox (memory + disk), 400–750 ms same-host. |
Creating a sandbox
# E2B
from e2b_code_interpreter import Sandbox
sbx = Sandbox()
# PandaStack
from pandastack import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
# Options map directly:
sandbox = Sandbox.create(
template="code-interpreter",
ttl_seconds=600, # ≈ E2B's timeout
metadata={"run_id": "abc123"}, # same concept as E2B metadata
)PandaStack sandboxes are also context managers — non-persistent ones are killed on exit:
with Sandbox.create(template="code-interpreter") as sandbox:
print(sandbox.exec("echo hello").stdout)// E2B
import { Sandbox } from "@e2b/code-interpreter";
const sbx = await Sandbox.create();
// PandaStack
import { Sandbox } from "@pandastack/sdk";
const sandbox = await Sandbox.create({ template: "code-interpreter" });
// Options map directly:
const sandbox2 = await Sandbox.create({
template: "code-interpreter",
ttlSeconds: 600, // ≈ E2B's timeoutMs
metadata: { runId: "abc123" },
});pandastack sandbox create --template code-interpreter --ttl 600 --metadata run_id=abc123Create restores a pre-baked memory snapshot rather than cold-booting, at ~179 ms p50. There is no warm pool — every create takes the snapshot-restore path, so latency doesn't depend on pool hit rate.
Running commands
E2B's sbx.commands.run(...) maps to sandbox.exec(...). Both PandaStack SDKs also ship a sandbox.commands.run(...) alias with the same result shape, so this call site often needs no change beyond the import.
# E2B
result = sbx.commands.run("ls -la /home/user")
print(result.stdout, result.exit_code)
# PandaStack — either spelling works
result = sandbox.exec("ls -la /root")
result = sandbox.commands.run("ls -la /root", timeout=30)
print(result.stdout, result.exit_code)
# check=True raises CommandFailed on non-zero exit (like subprocess.run)
sandbox.exec("make test", timeout_seconds=300, check=True)Streaming output (E2B's on_stdout / on_stderr callbacks):
exit_code = sandbox.exec_stream(
"pip install -r requirements.txt",
on_stdout=lambda chunk: print(chunk, end=""),
on_stderr=lambda chunk: print(chunk, end=""),
)// E2B
const result = await sbx.commands.run("ls -la /home/user");
// PandaStack — either spelling works
const result2 = await sandbox.exec("ls -la /root");
const result3 = await sandbox.commands.run("ls -la /root", { timeoutSeconds: 30 });
console.log(result2.stdout, result2.exitCode);
// Streaming (≈ E2B's onStdout / onStderr)
const exitCode = await sandbox.execStream("pip install -r requirements.txt", {
onStdout: (chunk) => process.stdout.write(chunk),
onStderr: (chunk) => process.stderr.write(chunk),
});pandastack sandbox exec <sandbox-id> --timeout 30 -- "ls -la /root"One behavioral difference to know about: PandaStack's exec takes a single string run under sh -c and has no separate cwd=, env=, or background= parameters. Express those in the shell instead — cd /work && ..., KEY=value cmd, setsid cmd & — or bake environment into your template. If your E2B code leans on per-command kwargs, this is the main thing you'll rewrite. See Exec — running commands.
Running code (code interpreter)
E2B's sbx.run_code(...) with rich outputs maps to a PandaStack code context — a persistent Jupyter-style kernel where variables survive across calls and charts/DataFrames come back as MIME bundles.
# E2B
execution = sbx.run_code("x = 1; x + 1")
# PandaStack
ctx = sandbox.create_code_context() # persistent kernel
ctx.run_code("x = 41")
ex = ctx.run_code("x + 1") # state persists
print(ex.text) # "42"
ex = ctx.run_code("import matplotlib.pyplot as plt; plt.plot([1,2,3]); plt.show()")
png_base64 = ex.results[0].png # like E2B's execution.results
print(ex.logs) # {"stdout": ..., "stderr": ...}For one-off snippets without kernel state, sandbox.run_code("print(2+2)", language="python") wraps exec directly.
// E2B
const execution = await sbx.runCode("x = 1; x + 1");
// PandaStack
const ctx = await sandbox.createCodeContext(); // persistent kernel
await ctx.runCode("x = 41");
const ex = await ctx.runCode("x + 1"); // state persists
console.log(ex.text); // "42"
const chart = await ctx.runCode("import matplotlib.pyplot as plt; plt.plot([1,2,3]); plt.show()");
const pngBase64 = chart.results[0]?.png; // like E2B's execution.resultsThe Execution shape is deliberately close to E2B's: results (MIME bundles with .png, .html, .json, .text accessors), logs (stdout/stderr), and error for tracebacks. See Code interpreter / REPL.
Files
E2B's files namespace maps to PandaStack's filesystem namespace.
# E2B
sbx.files.write("/home/user/data.csv", "a,b\n1,2")
content = sbx.files.read("/home/user/data.csv")
entries = sbx.files.list("/home/user")
# PandaStack
sandbox.filesystem.write("/root/data.csv", "a,b\n1,2") # str or bytes
content = sandbox.filesystem.read("/root/data.csv") # returns bytes
entries = sandbox.filesystem.listdir("/root") # list[FileInfo]
info = sandbox.filesystem.stat("/root/data.csv") # size, mode, mtime
ok = sandbox.filesystem.exists("/root/data.csv")
# Local ↔ sandbox transfer helpers
sandbox.filesystem.upload("./local.csv", "/root/data.csv")
sandbox.filesystem.download("/root/out.json", "./out.json")Watch out: PandaStack's read returns bytes in Python — call .decode() where your E2B code expected str.
// E2B
await sbx.files.write("/home/user/data.csv", "a,b\n1,2");
const content = await sbx.files.read("/home/user/data.csv");
// PandaStack
await sandbox.filesystem.write("/root/data.csv", "a,b\n1,2");
const text = await sandbox.filesystem.read("/root/data.csv"); // string
const entries = await sandbox.filesystem.list("/root"); // FileInfo[]
const info = await sandbox.filesystem.stat("/root/data.csv");
const ok = await sandbox.filesystem.exists("/root/data.csv");
// Local ↔ sandbox transfer helpers
await sandbox.filesystem.upload("./local.csv", "/root/data.csv");
await sandbox.filesystem.download("/root/out.json", "./out.json");pandastack fs write <sandbox-id> --path /root/data.csv --content "a,b"
pandastack fs read <sandbox-id> --path /root/data.csv
pandastack fs upload <sandbox-id> --local ./local.csv --remote /root/data.csv
pandastack fs download <sandbox-id> --remote /root/out.json --local ./out.jsonPandaStack does not currently have an equivalent of E2B's filesystem watch API — poll with stat/listdir, or run inotifywait inside the sandbox. Details: Filesystem.
Pause/resume → hibernate/wake
E2B's pause/resume maps to PandaStack's hibernate/wake, with one nice difference: a hibernated PandaStack sandbox auto-wakes on the next request to it, so you usually don't need an explicit resume call.
# E2B (beta)
sbx.pause()
sbx = Sandbox.resume(sandbox_id)
# PandaStack
sandbox.hibernate() # memory + disk persisted, VM stops
# ...later, from anywhere:
sandbox = Sandbox.get(sandbox_id)
sandbox.exec("echo back") # auto-wakes transparently
# or force it without a workload request:
sandbox.wake()// E2B (beta)
await sbx.pause();
const resumed = await Sandbox.resume(sandboxId);
// PandaStack
await sandbox.hibernate();
// ...later:
const sb = await Sandbox.get(sandboxId);
await sb.exec("echo back"); // auto-wakes transparently
// or: await sb.wake();pandastack sandbox hibernate <sandbox-id>
pandastack sandbox wake <sandbox-id>PandaStack also has a lighter-weight pause()/resume() pair that freezes the VM but keeps it resident in host RAM — faster to resume, but it keeps billing committed memory. Hibernate is the scale-to-zero one: RAM billing stops while asleep. There's also snapshot() (point-in-time image you can create new sandboxes from) and fork() (live copy-on-write clone, 400–750 ms same-host) — the latter has no E2B equivalent and is worth a look for tree-search agent patterns: Snapshots and forks.
Templates
Both platforms build custom sandbox images from a Dockerfile. E2B uses its CLI with an e2b.toml config; PandaStack builds server-side from a plain Dockerfile — no config file format, just a name.
# E2B
e2b template build --name my-agent-env
# PandaStack (server-side build; needs only an API key, no local Docker)
pandastack template build --name my-agent-env -f Dockerfile
pandastack template listThen use it exactly like a first-party template:
sandbox = Sandbox.create(template="my-agent-env")First-party templates: base, code-interpreter, agent, browser, postgres-16. Custom builds get their snapshot baked automatically, so creates from your template hit the same fast restore path. RAM is a template property (baked into the snapshot), not a per-create knob. See Templates.
What's actually different
Being honest about the deltas in both directions:
Things PandaStack has that E2B doesn't (or that work differently):
- Copy-on-write forks of running sandboxes —
fork()/fork_tree(n)/explore()clone live memory + disk in 400–750 ms for branch-and-explore agent patterns. - Managed PostgreSQL — real Postgres 16 VMs with backups, PITR clones, and failover (
POST /v1/databases, 1g/4g/16g tiers, 30–90 s create). Databases - Git-driven app hosting — connect a repo, get builds + blue-green deploys + a stable URL, Vercel/Render-style, with scale-to-zero (~1–2 s wake). Apps
- Serverless functions + cron schedules — deploy a bundle, invoke over HTTP, schedule with cron. Functions
- Self-hosting — the whole platform is open source; run it on your own AWS/GCP account or on-prem KVM hosts. Self-host
- Pricing model — one rate card: $0.054/vCPU-hr for actively burned CPU + $0.0162/GiB-hr for committed RAM. No per-request pricing.
Things E2B has that PandaStack doesn't (today):
- Per-command
cwd/env/backgroundkwargs and background process handles — PandaStack expresses these through the shell. - A filesystem watch API.
- A desktop/GUI sandbox product line.
- A larger ecosystem of framework integrations and a longer production track record — E2B has been at this longer, and it shows in the breadth of examples and third-party glue.
If your workload depends on any of the latter, weigh that honestly before migrating. If it's create → exec → files → kill (the vast majority of agent workloads), the migration is an afternoon.
Migration checklist
pip install pandastack/npm i @pandastack/sdk; setPANDASTACK_API_KEY.- Rebuild your E2B template Dockerfile with
pandastack template build --name ... -f Dockerfile(drope2b.toml). - Swap imports; change
Sandbox()→Sandbox.create(template=...). sbx.commands.run→ keep assandbox.commands.runor rename tosandbox.exec; move anycwd=/env=kwargs into the command string.sbx.files.*→sandbox.filesystem.*(list→listdirin Python;readreturns bytes in Python).sbx.run_code→create_code_context()+ctx.run_code().pause/resume→hibernate/wake(or lean on auto-wake and delete the resume call).- Run your suite; the REST reference covers anything the SDKs don't.