What Do AI App Builders Need a Sandbox For?
Updated October 27, 2026 .md
Short answer: Six jobs: run the code a model wrote, give each user a separate VM, keep your secrets out of it, show users what is running on a URL, keep their state between visits, and cap what it all costs. On prod, a noEnv small sandbox was ready in 2.66 seconds and ran model-written Python in 0.16 seconds.
This page is for you if your product does one of two things: it runs code an LLM wrote (a code interpreter, a data-analysis tool, an agent with a shell), or it gives each of your users their own environment (an app builder, a notebook, a personal agent). Below, each job gets the answer, the Sandboxer feature that does it, and the guide that shows it working. If you analyze data in notebooks yourself, see Sandboxer for data scientists; if you run agent evals, see Sandboxer for eval and research teams.
The six jobs at a glance#
| Job | Sandboxer feature | Guide that shows it |
|---|---|---|
| Run model-written code and return the output | exec with a timeout, files.write |
Fresh sandbox per tool call |
| Give each user a separate machine | One microVM per sandbox, labels | Give every user their own sandbox |
| Keep your secrets and account out of user code | noEnv, scoped API keys |
Pass secrets to a sandbox safely |
| Show the user what their code is serving | sbx host: a private HTTPS URL per port |
Give a port an HTTPS URL |
| Keep a user's state between visits | Idle auto-pause with memory, resume on use | Idle auto-pause and wake on use |
| Cap cost and stay under limits | TTL, idle timeout, sizes, plan limits | Cap what agent sandboxes can cost |
Job 1: Run the code a model wrote#
Write the model's code to a file in the sandbox, run it with exec and a timeout, and return stdout, stderr and the exit code to the model. A synchronous exec waits up to 30 seconds by default and 600 at most; longer jobs run detached and you poll them. A runaway loop hits the timeout and comes back with timedOut: true while the sandbox stays usable. Decide early whether each call gets a fresh VM or a session shares one: in our measurements a reused sandbox answered in 0.55 seconds per call, a fresh one from a template in 3.2 seconds. See Should each agent tool call get a fresh sandbox?. If variables must persist between cells, keep a kernel running: Build a stateful code interpreter with Jupyter. For a ready tool definition, see Add code execution to the Vercel AI SDK.
Job 2: Give each user a separate machine#
Create one sandbox per user (or per session) and store its id against the user in your database. Each sandbox is its own Cloud Hypervisor microVM with its own kernel and its own copy-on-write disk, and sandbox-to-sandbox traffic is blocked (security). Put labels such as user=u_123 on it so you can find and delete a user's sandboxes from the API alone. Labels are set through the REST API today. The SDKs have no labels option yet, but the JavaScript SDK passes a labels field on create through to the API, as the script below does. Don't pool VMs between users: a shared VM is the isolation you were trying to buy. See Give every user of your product their own sandbox.
Job 3: Keep your secrets and account out of user code#
Create user sandboxes with noEnv (sbx new --no-env). They receive none of your environment's variables, secret files, GitHub token or agent logins. They still get a SANDBOXER_TOKEN, limited to reading and hosting that one sandbox: in our run it got 200 for its own sandbox and 403 api_key_action_forbidden on /me. Pass the user's own secrets with env, never in a command line. Run your backend on a scoped key (--preset ci), not an admin key. Outbound internet is open and Sandboxer has no egress allowlist; if you need one, set it inside the VM. See Pass secrets to a sandbox safely and What can reach a sandbox and what it can reach.
Job 4: Show the user what their code is serving#
When the model builds a web app, a chart server or an API, host the port: sbx host <id> <port> or sandbox.host(port). You get https://<subdomain>-<port>.box.sandboxer.com with TLS and WebSockets. It is private by default: without the URL's _token the request gets 401 and never reaches the server; with it, a 302 sets a cookie. Put the URL in an iframe or a link for that user only, and treat it as a secret. See Give a port an HTTPS URL, and for a framework dev server with hot reload, Run a Next.js dev server with a preview URL.
Job 5: Keep a user's state between visits#
Set idleStopMinutes on create. After that many idle minutes the sandbox pauses with its memory, compute stops billing, and the user's next API call or hosted-URL request wakes it. Memory comes back only when the resume lands on a machine with the same CPU type; otherwise it boots from disk, keeps files and loses processes, and reports memoryRestored: false. Run servers as systemd units so they come back either way. To give every new user the same packages without installing each time, save one sandbox as a named snapshot and create from it with --from. See Idle auto-pause and wake on use and Turn a sandbox into a reusable template.
Job 6: Cap cost and stay under limits#
Billing is per second while a sandbox runs; stopped and paused sandboxes cost no compute and count toward your plan's included storage. Per sandbox, pick small, set ttlSeconds (a new sandbox auto-stops after one hour by default) and an idle timeout, and delete when the user leaves. Per account, your plan caps active sandboxes and starts; creates, resumes and forks each count as a start, and going over returns 429 with Retry-After. See Cap what agent sandboxes can cost and Handle rate limits and concurrency caps.
One request, end to end, on prod#
This is the request path of a typical AI app: a user asks a question, the model writes Python, and your backend runs it in that user's own noEnv sandbox, returns the output, shows a hosted URL and deletes the VM. We ran it on 2026-10-27 at 11:32 UTC with sandboxer-sdk 0.1.1 on Node 24, from a machine outside Sandboxer's network. No model was called: the "model-written" code is fixed in the script, so the timings are the sandbox side only.
import { Sandboxer } from "sandboxer-sdk";
const sandboxer = new Sandboxer(); // SANDBOXER_API_KEY: your backend's key
const t = () => performance.now();
const ms = (a) => `${((t() - a) / 1000).toFixed(2)} s`;
// Code a model wrote for this user's request (fixed here; no model was called in this test)
const modelCode = `
import csv, statistics
rows = list(csv.DictReader(open("sales.csv")))
by = {}
for r in rows: by.setdefault(r["region"], []).append(float(r["amount"]))
for k, v in sorted(by.items()): print(k, len(v), round(statistics.mean(v), 2))
`;
let a = t();
const box = await sandboxer.create({
type: "small",
name: "user-u123",
noEnv: true, // none of your secrets inside
labels: { app: "my-ai-app", user: "u_123" },
idleStopMinutes: 10,
ttlSeconds: 900,
});
console.log("create -> ready:", ms(a), box.id);
a = t();
await box.files.write("job/sales.csv", "region,amount\neu,120\nus,80\neu,95\nus,210\napac,40\n");
await box.files.write("job/main.py", modelCode);
console.log("upload 2 files:", ms(a));
a = t();
const run = await box.exec("python3 main.py", { cwd: "job", timeoutSeconds: 20 });
console.log("run:", ms(a), "exit", run.exitCode);
console.log(run.stdout.trim());
// A second model attempt that never ends
a = t();
const hang = await box.exec("python3 -c 'while True: pass'", { timeoutSeconds: 5 });
console.log("runaway:", ms(a), { exitCode: hang.exitCode, timedOut: hang.timedOut });
// What the user's code can see of your account
const probe = await box.exec(
`env | grep -c -i -E 'openai|anthropic|aws|github' ; ` +
`for p in /sandboxes/${box.id} /me; do printf "%s " $p; curl -s -o /tmp/r -w '%{http_code} ' -H "Authorization: Bearer $SANDBOXER_TOKEN" "$SANDBOXER_API_URL$p"; jq -c '.error.code // .sandbox.id' /tmp/r; done`
);
console.log(probe.stdout.trim());
// Show the user something running: a private HTTPS URL for port 8000
await box.start("python3 -m http.server 8000", { cwd: "job" });
a = t();
const h = await box.host(8000);
console.log("host call:", ms(a), h.access);
const u = new URL(h.url);
const bare = `${u.origin}/sales.csv`;
const withTok = `${u.origin}/sales.csv${u.search}`;
let codes = [];
for (let i = 0; i < 10; i++) {
const r0 = await fetch(bare, { redirect: "manual" });
const r1 = await fetch(withTok, { redirect: "manual" });
codes = [r0.status, r1.status];
if (r1.status === 302) {
const cookie = r1.headers.get("set-cookie").split(";")[0];
const r2 = await fetch(r1.headers.get("location").startsWith("http") ? r1.headers.get("location") : u.origin + r1.headers.get("location"), { headers: { cookie }, redirect: "manual" });
codes.push(r2.status, (await r2.text()).split("\n")[1]);
break;
}
await new Promise((r) => setTimeout(r, 1000));
}
console.log("host -> served:", ms(a), "no token:", codes[0], "with token:", codes[1], "then with cookie:", codes[2], JSON.stringify(codes[3]));
a = t();
await box.delete();
console.log("delete call:", ms(a));create -> ready: 2.66 s sb_kozdptu21e
upload 2 files: 0.31 s
run: 0.16 s exit 0
apac 1 40.0
eu 2 107.5
us 2 145.0
runaway: 5.13 s { exitCode: 137, timedOut: true }
0
/sandboxes/sb_kozdptu21e 200 "sb_kozdptu21e"
/me 403 "api_key_action_forbidden"
host call: 0.23 s private
host -> served: 0.75 s no token: 401 with token: 302 then with cookie: 200 "eu,120"
delete call: 0.03 sWhat the numbers say:
| Step | Measured | What it means for your app |
|---|---|---|
| Create to ready | 2.66 s (2.65 s on an earlier run) | Create on the user's first request, not at sign-up; reuse it for the rest of the session |
| Upload code and data | 0.31 s for 2 small files | files.write is fine for a model's code and small inputs |
| Run the code | 0.16 s | The VM adds little once it exists; the model call will dominate |
| Runaway loop | 5.13 s at a 5 s timeout, exit 137, timedOut: true |
Always pass a timeout; the next exec on the same sandbox worked |
| Secrets visible | 0 matching variables | noEnv kept your environment out |
| Token reach | own sandbox 200, /me 403 |
User code can't act on your account |
| Host a port | 0.23 s for the call, 0.75 s until the first 200 |
No token: 401. A new route can 404 for a second or two; retry |
| Delete | 0.03 s for the call | The VM is torn down in the background; we confirmed 404 not_found after |
Exit 137 here means the process was killed at the timeout. An out-of-memory kill also exits 137, so check timedOut (and oomKilled) before you tell the model what happened.
What it costs#
Prices come from the pricing page: default is $0.036/hour and small (half the rate) is $0.018/hour, or $0.000005 per second. Billing is per second from when the sandbox is up until it stops or is deleted.
| Pattern | Arithmetic | Compute cost |
|---|---|---|
| The run above (the whole script took 11 s of wall clock, so the VM ran less) | 11 s × $0.000005 | under $0.000055 |
| 1,000 such one-request sessions | 1,000 × $0.000055 | under $0.06 |
One user's persistent small sandbox, awake 2 h a day for 30 days, paused the rest |
60 h × $0.018 | $1.08 a month |
The same on default (4 vCPU, 8 GB) |
60 h × $0.036 | $2.16 a month |
Paused hours cost no compute; the paused disk counts toward your plan's included storage (50 GB on the $20 plan).
When Sandboxer isn't the right fit#
- You only evaluate tiny expressions at very high volume. A new VM takes about 2.7 seconds to be ready, and every create counts as a start. The $20 plan allows 12 starts a minute, 60 an hour and 200 a day; the $2,000 plan allows 90, 600 and 2,400. If you need a fresh environment for thousands of calls an hour, reuse one sandbox per session instead, or use a lighter in-process sandbox for pure expressions.
- You need the platform to enforce an egress allowlist. Outbound internet is open by default and Sandboxer has no allowlist or domain filter. You can restrict outbound with
ufwinside each VM, but the user's code runs as a user withsudo, so that isn't a boundary against it. - You need a region other than us-east or eu-central. Sandboxes run on dedicated servers we operate. Sandboxer has two regions, us-east (Ashburn, Virginia) and eu-central (Helsinki, Finland); see regions.
- Your workload needs a GPU. Sandboxes are CPU-only. Every size is CPU and RAM only, up to 16 vCPU and 32 GB on
xlarge(which needs the $100 plan). Run model inference elsewhere and send only the code to the sandbox. - You need your own base image format. Every sandbox starts from Sandboxer's Ubuntu image. You customize it by setting one up and saving a named snapshot, not by pushing a container image.
- You are still on the trial. The 7-day trial allows 2 sandboxes at once,
smallanddefaultonly, with at most an 8-hour TTL. That is enough to build the request path above, not to run a user launch.
FAQ#
Should I create a user's sandbox at sign-up?#
No. Create it on the first request that needs code execution and store its id against the user. A sandbox created at sign-up spends a start and storage on users who may never use it.
Can one user's code see another user's files or reach their VM?#
No. Each sandbox is a separate microVM with its own kernel and disk, and traffic between sandboxes is blocked. Use one sandbox per user or per session, never a shared pool.
What does model-written code see inside a noEnv sandbox?#
Only what you pass in, plus SANDBOXER_API_URL, SANDBOXER_ID, SANDBOXER_SUBDOMAIN and a SANDBOXER_TOKEN limited to that one sandbox. In our run that token got 403 on /me.
How do I stop runaway code?#
Pass timeoutSeconds on every exec. In our run a while True loop came back after 5.13 seconds with timedOut: true, and the sandbox kept working.
Do paused users cost money?#
Not for compute. A paused sandbox counts toward your plan's included storage until you delete it, so delete sandboxes of users who leave.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.