Execute Python in an isolated microVM via one API call
How to execute Python in an isolated microVM with one API call — create a Firecracker sandbox, run code, and read stdout from the Python or TypeScript SDK or raw curl.
The fastest way to run untrusted or LLM-generated Python safely: create a PandaStack sandbox, exec your code, read stdout. Every sandbox is a real Firecracker microVM with its own kernel — not a container, not a shared interpreter — and it boots in 179ms at p50 because every create restores a pre-baked snapshot. This page shows the minimum: a couple of SDK lines, or one POST if you're calling the HTTP API directly.
Setup
Install an SDK and export your API key. Never hardcode the key — the SDKs read PANDASTACK_API_KEY from the environment by default.
export PANDASTACK_API_KEY=pds_...
# Python
pip install pandastack
# TypeScript
npm install @pandastack/sdkGet a key from the dashboard. The public API base is https://api.pandastack.ai; the SDKs target it automatically and you can override the base URL via config if you self-host.
The minimum: create, exec, read stdout
Create a sandbox on the code-interpreter template, run python -c, and read the result off the returned object. The exec call runs a shell command inside the guest and returns stdout, stderr, exit code, and wall-clock duration.
from pandastack import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
result = sandbox.exec("python -c 'print(2 ** 10)'")
print(result.stdout) # "1024\n"
print(result.exit_code) # 0
sandbox.kill()import { Sandbox } from "@pandastack/sdk";
const sandbox = await Sandbox.create({ template: "code-interpreter" });
const result = await sandbox.exec("python -c 'print(2 ** 10)'");
console.log(result.stdout); // "1024\n"
console.log(result.exit_code); // 0
await sandbox.kill();That's the whole loop. Sandbox.create does the snapshot-restore boot, exec runs your command, and you read stdout. Call kill() when you're done — or pass ttl_seconds / ttlSeconds and let it expire on its own.
ExecResult carries everything you need to act on the run:
| Field | Type | Meaning |
|---|---|---|
stdout | str | Standard output of the command |
stderr | str | Standard error |
exit_code | int | Process exit status (0 = success) |
duration_ms | int | Wall-clock execution time |
Pass check=True to raise CommandFailed on a non-zero exit instead of inspecting exit_code yourself.
Capturing rich results: run_code
exec("python -c ...") is fine for plain stdout. But if you're running LLM-generated snippets and want typed results — a returned value, a DataFrame as HTML, a chart as a PNG — use a code context instead. It's a persistent Jupyter-style kernel: state survives across calls, and rich output is captured automatically (no savefig, no stdout scraping).
from pandastack import Sandbox
sandbox = Sandbox.create(template="code-interpreter")
ctx = sandbox.create_code_context() # persistent Python kernel
ex = ctx.run_code("sum(range(101))")
print(ex.text) # "5050" — the kernel's repr of the last expression
print(ex.stdout) # anything the code printed
ctx.close()
sandbox.kill()import { Sandbox } from "@pandastack/sdk";
const sandbox = await Sandbox.create({ template: "code-interpreter" });
const ctx = await sandbox.createCodeContext(); // persistent Python kernel
const ex = await ctx.runCode("sum(range(101))");
console.log(ex.text); // "5050"
console.log(ex.stdout);
await ctx.close();
await sandbox.kill();The Execution object exposes .results (a list of typed Results), plus convenience accessors .text, .png, .stdout, .stderr, .error, and .exit_code. The code-interpreter template bakes IPython, matplotlib, plotly, kaleido, and pandas, so charts and tables come back as objects ready to hand to a model. See the code interpreter guide for the full result surface.
Raw HTTP (no SDK)
The SDKs are thin wrappers over a REST API. If you're calling from a language without an SDK, or wiring this into an existing service, hit the endpoints directly. Authenticate every request with Authorization: Bearer $PANDASTACK_API_KEY.
First, create a sandbox:
curl -sS https://api.pandastack.ai/v1/sandboxes \
-H "Authorization: Bearer $PANDASTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template": "code-interpreter"}'The response includes the sandbox id:
{ "id": "sbx_3f9a...", "template": "code-interpreter", "status": "running" }Then exec a command against that id and read stdout off the JSON response:
curl -sS https://api.pandastack.ai/v1/sandboxes/sbx_3f9a.../exec \
-H "Authorization: Bearer $PANDASTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"cmd": "python -c \"print(2 ** 10)\""}'{ "stdout": "1024\n", "stderr": "", "exit_code": 0, "duration_ms": 41 }When you're finished, delete the sandbox so it stops counting against your quota:
curl -sS -X DELETE https://api.pandastack.ai/v1/sandboxes/sbx_3f9a... \
-H "Authorization: Bearer $PANDASTACK_API_KEY"| Step | Method + path | Body |
|---|---|---|
| Create | POST /v1/sandboxes | {"template": "code-interpreter"} |
| Run | POST /v1/sandboxes/{id}/exec | {"cmd": "python -c '...'"} |
| Delete | DELETE /v1/sandboxes/{id} | — |
Why a microVM, not a container
Each sandbox is a full Firecracker guest: its own Linux kernel, its own page tables, hardware-virtualized isolation via KVM. A container shares the host kernel; a microVM does not, which is what makes it safe to run code you don't trust — model output, user submissions, arbitrary pip-installed packages. The cost of that isolation used to be slow boots; PandaStack pays it once at bake time and restores a frozen snapshot on every create, so the strong-isolation path stays sub-200ms.
Next steps
- Code interpreter guide — persistent kernels, typed results, charts as objects
- Install packages in a sandbox —
pip installinside the microVM - Stream code output —
exec_streamfor long-running commands via SSE callbacks - Snapshots and forks — freeze a warmed-up environment and clone it in ~400ms