| name | sandbox |
| description | Creates isolated Linux MicroVMs using Vercel Sandbox SDK. Use when building code execution environments, running untrusted code, spinning up dev servers, testing in isolation, or when the user mentions "sandbox", "microvm", "isolated execution", or "@vercel/sandbox". |
| metadata | {"author":"Vercel Inc.","version":"2.0"} |
CRITICAL: Always Use Correct @vercel/sandbox Documentation
Your knowledge of @vercel/sandbox may be outdated.
Follow these instructions before starting on any sandbox-related tasks:
Official Resources
What changed in v2
The @vercel/sandbox@2 SDK and sandbox@3 CLI replace anonymous, ephemeral
sandboxes with named, persistent sandboxes. Key differences from v1:
- Sandboxes are identified by
name (not sandboxId). Names are unique per project.
- Sandboxes are persistent by default — when a sandbox stops, the SDK
automatically snapshots it and restores the filesystem on the next resume.
- A Session is a single running VM instance inside a sandbox. SDK calls
like
runCommand, writeFiles, etc. automatically resume a stopped sandbox.
- New methods:
Sandbox.getOrCreate, Sandbox.fork, sandbox.update,
sandbox.delete, sandbox.listSessions, sandbox.listSnapshots,
Snapshot.tree, defineSandboxProxy.
- New params:
name, persistent, tags, onResume, snapshotExpiration,
keepLastSnapshots, L7 network policy matchers, forwardURL.
- Pagination uses cursor-based iterators (async-iterable) instead of
since/until.
Sandbox.list({ since, until }) → Sandbox.list({ cursor, namePrefix, sortBy, tags }).
- v1 sandboxes are backfilled so the only required code change is using
name
instead of sandboxId.
Quick Reference
Essential imports:
import {
Sandbox,
Session,
Snapshot,
Command,
CommandFinished,
} from "@vercel/sandbox";
import { APIError, StreamError } from "@vercel/sandbox";
import type {
NetworkPolicy,
NetworkPolicyRule,
NetworkTransformer,
} from "@vercel/sandbox";
import { defineSandboxProxy } from "@vercel/sandbox/proxy";
import ms from "ms";
Available runtimes:
type RUNTIMES = "node26" | "node24" | "node22" | "python3.13";
Creating Sandboxes
Basic Creation
import { Sandbox } from "@vercel/sandbox";
const sandbox = await Sandbox.create({
name: "my-dev-env",
runtime: "node24",
resources: { vcpus: 4 },
ports: [3000],
timeout: ms("10m"),
env: { NODE_ENV: "production" },
tags: { env: "staging", team: "infra" },
persistent: true,
snapshotExpiration: ms("7d"),
});
console.log(sandbox.name);
Retrieve an Existing Sandbox
const sandbox = await Sandbox.get({ name: "my-dev-env" });
Get-or-Create (Idempotent)
Sandbox.getOrCreate is the recommended pattern for long-lived sandboxes.
const sandbox = await Sandbox.getOrCreate({
name: "my-workspace",
runtime: "node24",
onCreate: async (sbx) => {
await sbx.writeFiles([
{ path: "README.md", content: Buffer.from("# Hello") },
]);
await sbx.runCommand("npm", ["install"]);
},
onResume: async (sbx) => {
await sbx.runCommand({ cmd: "npm", args: ["run", "dev"], detached: true });
},
});
Behavior:
- If a sandbox with that
name exists → resumes it and fires onResume.
- If it does not exist → creates a fresh sandbox and fires
onCreate.
- If the sandbox exists but its snapshot expired → deletes the stale sandbox,
re-creates it with the same name, and fires
onCreate.
Re-warming on Resume
Use onResume to restart background services or rehydrate caches whenever a
persistent sandbox's session is resumed:
const sandbox = await Sandbox.get({
name: "my-workspace",
onResume: async (sbx) => {
await sbx.runCommand({ cmd: "npm", args: ["run", "dev"], detached: true });
},
});
onResume also fires when a SDK call (e.g. runCommand, writeFiles)
auto-resumes a stopped sandbox.
With Git Source
const sandbox = await Sandbox.create({
source: {
type: "git",
url: "https://github.com/vercel/sandbox-example-next.git",
depth: 1,
revision: "main",
},
runtime: "node24",
ports: [3000],
});
With Private Git Repository
const sandbox = await Sandbox.create({
source: {
type: "git",
url: "https://github.com/org/private-repo.git",
username: process.env.GIT_USERNAME!,
password: process.env.GIT_TOKEN!,
},
runtime: "node24",
});
From Tarball
const sandbox = await Sandbox.create({
source: {
type: "tarball",
url: "https://example.com/project.tar.gz",
},
runtime: "node24",
ports: [3000],
});
From a Snapshot
const sandbox = await Sandbox.create({
source: {
type: "snapshot",
snapshotId: "snap_abc123",
},
ports: [3000],
});
From a Custom Image (VCR)
Instead of a stock runtime, start a sandbox from a Vercel Container
Registry (VCR) image stored in the
sandbox's project. image and runtime are mutually exclusive — pass one
or the other, never both.
const sandbox = await Sandbox.create({
image: "my-repo:v1",
ports: [3000],
});
image accepts a repository in the sandbox's project with an optional tag or
digest, or a fully-qualified VCR URL. A bare repository name resolves to the
latest tag:
await Sandbox.create({ image: "my-repo" });
await Sandbox.create({ image: "my-repo:v1" });
await Sandbox.create({ image: "my-repo@sha256:..." });
await Sandbox.create({
image: "vcr.vercel.com/my-team/my-project/my-repo:v1",
});
Push images to VCR with Docker-compatible tooling before referencing them. See the
Container Registry docs for push
instructions.
Forking an Existing Sandbox
Sandbox.fork seeds a new sandbox from another sandbox's current snapshot
and copies its config (resources, timeout, networkPolicy, tags,
ports, persistent, snapshotExpiration, keepLastSnapshots). Any field
you pass overrides the inherited value. env is not copied (encrypted
server-side) and must be re-supplied. If the source has no current snapshot,
the fork falls back to a fresh create using the source's runtime plus the
copied config.
const fork = await Sandbox.fork({ sourceSandbox: "prod-agent" });
const fork = await Sandbox.fork({
sourceSandbox: "prod-agent",
name: "forked-prod-agent",
resources: { vcpus: 4 },
env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY! },
});
Auto-Dispose Pattern
Use await using for automatic cleanup:
async function runInSandbox() {
await using sandbox = await Sandbox.create();
await sandbox.runCommand("echo", ["Hello"]);
}
Running Commands
Basic Command Execution
const result = await sandbox.runCommand("npm", ["install"]);
if (result.exitCode !== 0) {
console.error("Install failed:", await result.stderr());
}
With Options
const result = await sandbox.runCommand({
cmd: "npm",
args: ["run", "build"],
cwd: "/vercel/sandbox/app",
env: { NODE_ENV: "production" },
sudo: false,
stdout: process.stdout,
stderr: process.stderr,
});
Detached Commands (Background Processes)
const devServer = await sandbox.runCommand({
cmd: "npm",
args: ["run", "dev"],
detached: true,
stdout: process.stdout,
});
const finished = await devServer.wait();
await devServer.kill("SIGTERM");
Root Access
await sandbox.runCommand({
cmd: "dnf",
args: ["install", "-y", "golang"],
sudo: true,
});
File Operations
Write Files
await sandbox.writeFiles([
{
path: "/vercel/sandbox/config.json",
content: Buffer.from(JSON.stringify({ key: "value" })),
},
{
path: "/vercel/sandbox/script.sh",
content: Buffer.from("#!/bin/bash\necho 'Hello'"),
mode: 0o755,
},
]);
Read Files
const buffer = await sandbox.readFileToBuffer({
path: "/vercel/sandbox/output.txt",
});
const stream = await sandbox.readFile({
path: "/vercel/sandbox/large-file.bin",
});
Download Files
const localPath = await sandbox.downloadFile(
{ path: "/vercel/sandbox/report.pdf" },
{ path: "./downloads/report.pdf" },
{ mkdirRecursive: true },
);
Create Directories
await sandbox.mkDir("/vercel/sandbox/my-app/src");
sandbox.fs — node:fs/promises-compatible API
const content = await sandbox.fs.readFile("/etc/hostname", "utf8");
await sandbox.fs.writeFile("/tmp/hello.txt", "Hello, world!");
const files = await sandbox.fs.readdir("/tmp");
const stats = await sandbox.fs.stat("/tmp/hello.txt");
Network Policy
Full Internet Access (Default)
const sandbox = await Sandbox.create({
networkPolicy: "allow-all",
});
No Network Access
const sandbox = await Sandbox.create({
networkPolicy: "deny-all",
});
Restricted Access (Simple Domain List)
const sandbox = await Sandbox.create({
networkPolicy: {
allow: ["*.npmjs.org", "github.com", "registry.yarnpkg.com"],
subnets: {
allow: ["10.0.0.0/8"],
deny: ["10.1.0.0/16"],
},
},
});
Restricted Access with Credential Brokering
const sandbox = await Sandbox.create({
networkPolicy: {
allow: {
"ai-gateway.vercel.sh": [
{
transform: [
{
headers: { authorization: "Bearer ..." },
},
],
},
],
"*": [],
},
},
});
L7 Request Matchers
Rules can match on method, path, query string, and headers. All specified
dimensions must match; multiple methods are ORed; multiple header and
query-string matchers are ANDed.
const sandbox = await Sandbox.create({
networkPolicy: {
allow: {
"ai-gateway.vercel.sh": [
{
match: {
method: ["POST"],
path: { startsWith: "/v1/" },
headers: [
{ key: { exact: "x-api-key" }, value: { exact: "placeholder" } },
],
},
transform: [{ headers: { authorization: "Bearer ..." } }],
},
],
},
},
});
Matchers support exact, startsWith, and regex (RE2).
Forward Matching Requests to a Proxy
Use forwardURL to redirect any matching request through an HTTPS proxy you
control. The proxy receives the original request along with sandbox metadata in
forwarded headers.
const sandbox = await Sandbox.create({
networkPolicy: {
allow: {
"api.example.com": [
{
match: { path: { startsWith: "/secure/" } },
forwardURL: "https://my-proxy.example.com",
},
],
},
},
});
Implement the proxy handler with defineSandboxProxy, using the Web Request & Response objects — it verifies the
sandbox OIDC token and extracts metadata about the source sandbox:
import { defineSandboxProxy } from "@vercel/sandbox/proxy";
const handler = defineSandboxProxy(async (request, meta) => {
console.log("Proxied from sandbox", meta.sandboxName);
return fetch(request);
});
export {
handler as GET,
handler as POST,
handler as PUT,
handler as PATCH,
handler as DELETE,
};
Updating Network Policy at Runtime
Use sandbox.update (preferred). updateNetworkPolicy is deprecated but still
works.
await sandbox.update({ networkPolicy: { allow: ["api.openai.com"] } });
Updating Sandbox Configuration
sandbox.update replaces individual update helpers and accepts any of the
mutable parameters. When ports is provided, it is treated as the full
desired port list — any currently exposed port not present in the array is
deregistered.
await sandbox.update({
resources: { vcpus: 4 },
timeout: ms("30m"),
networkPolicy: "deny-all",
ports: [3000, 8000],
tags: { env: "prod" },
persistent: false,
snapshotExpiration: ms("14d"),
keepLastSnapshots: { count: 1 },
currentSnapshotId: "snap_xyz",
});
Deleting a Sandbox
await sandbox.delete();
Stopping a Sandbox
stop() is synchronous: it blocks until the VM is fully stopped and returns
the final session state, including the snapshot created during shutdown (when
persistent: true).
const result = await sandbox.stop();
console.log(result.snapshot?.id);
console.log(result.activeCpuUsageMs);
console.log(result.networkTransfer);
Tags
Sandboxes support up to 5 key-value tags. Tags can be set at creation, updated
via sandbox.update({ tags }), and used as filters when listing.
await Sandbox.create({ tags: { env: "staging", team: "infra" } });
const result = await Sandbox.list({ tags: { env: "staging" } });
Listing Sandboxes, Sessions, and Snapshots
All list APIs use cursor-based pagination and return an async-iterable that
auto-paginates through every page. You can also iterate page-by-page or
collect all items at once.
Sandbox.list
const result = await Sandbox.list({
namePrefix: "ci-",
tags: { env: "staging" },
sortBy: "createdAt",
sortOrder: "desc",
limit: 50,
});
for await (const sandbox of result) {
console.log(sandbox.name);
}
for await (const page of result.pages()) {
console.log(page.sandboxes.length);
}
const all = await result.toArray();
const next = result.pagination.next;
sandbox.listSessions and sandbox.listSnapshots
const sessions = await sandbox.listSessions();
for await (const session of sessions) {
console.log(session.sessionId, session.status);
}
const snapshots = await sandbox.listSnapshots();
for await (const snapshot of snapshots) {
console.log(snapshot.snapshotId, snapshot.status);
}
Snapshot.list
const snapshots = await Snapshot.list({
name: "my-dev-env",
sortOrder: "desc",
limit: 50,
});
for await (const snapshot of snapshots) {
console.log(snapshot.snapshotId, snapshot.status);
}
Sessions
A Session is a single running VM instance inside a sandbox. You typically
do not interact with sessions directly — the SDK creates and resumes them for
you — but you can inspect the current one.
const session = sandbox.currentSession();
console.log(session.sessionId);
console.log(session.status);
Snapshots
Snapshots save the entire sandbox filesystem to be reused later, for any
number of sandboxes.
Create a Snapshot
const sandbox = await Sandbox.create({ runtime: "node24" });
await sandbox.runCommand("npm", ["install"]);
const snapshot = await sandbox.snapshot({
expiration: ms("14d"),
});
console.log("Snapshot ID:", snapshot.snapshotId);
Default Snapshot Expiration and Retention
Configure default expiration and retention policy per sandbox:
await Sandbox.create({
name: "my-app",
snapshotExpiration: ms("7d"),
keepLastSnapshots: {
count: 1,
expiration: ms("30d"),
deleteEvicted: true,
},
});
keepLastSnapshots: { count: 1 } is the recommended setting when you only
care about the latest snapshot — it lets the SDK keep snapshot storage costs flat.
List, Get, and Delete
const snapshots = await Snapshot.list();
for await (const snapshot of snapshots) {
console.log(snapshot.snapshotId, snapshot.status);
}
const snapshot = await Snapshot.get({ snapshotId: "snap_abc123" });
await snapshot.delete();
Snapshot Tree
Snapshots form a tree: any sandbox created from another snapshot inherits a
parent → child relationship. Walk that tree to see ancestors or descendants of
a given snapshot.
const ancestors = await Snapshot.tree({
snapshotId: "snap_abc",
sortOrder: "desc",
});
for await (const node of ancestors) {
console.log(node.snapshotId, node.parentId);
}
const descendants = await Snapshot.tree({
snapshotId: "snap_abc",
sortOrder: "asc",
});
Snapshot Rollback
Point an existing sandbox at a previous snapshot by updating
currentSnapshotId. New sessions will resume from that snapshot.
await sandbox.update({ currentSnapshotId: "snap_previous" });
Exposed Ports
const sandbox = await Sandbox.create({ ports: [3000, 8000] });
const url = sandbox.domain(3000);
Replace the exposed port list at runtime with sandbox.update({ ports }). Any
currently exposed port not present in the new array is deregistered.
await sandbox.update({ ports: [3000, 8443] });
Timeout Management
const sandbox = await Sandbox.create({
timeout: ms("10m"),
});
await sandbox.extendTimeout(ms("5m"));
Authentication
Vercel OIDC Token (Recommended)
vercel link
vercel env pull
The SDK automatically uses VERCEL_OIDC_TOKEN from environment.
Access Token (Alternative)
const sandbox = await Sandbox.create({
teamId: process.env.VERCEL_TEAM_ID!,
projectId: process.env.VERCEL_PROJECT_ID!,
token: process.env.VERCEL_TOKEN!,
});
Error Handling
import { APIError, StreamError } from "@vercel/sandbox";
try {
const sandbox = await Sandbox.create();
} catch (error) {
if (error instanceof APIError) {
console.error("API Error:", error.message, error.response.status);
} else if (error instanceof StreamError) {
console.error("Stream Error:", error.message);
}
throw error;
}
Cancellation with AbortSignal
const controller = new AbortController();
setTimeout(() => controller.abort(), 30000);
const sandbox = await Sandbox.create({
signal: controller.signal,
});
const result = await sandbox.runCommand({
cmd: "npm",
args: ["test"],
signal: controller.signal,
});
Limitations
| Limitation | Details |
|---|
| Max vCPUs | 4 vCPUs on Hobby, 8 vCPUs on Pro, 32 vCPUs on Enterprise (2048 MB RAM per vCPU) |
| Max ports | 15 exposed ports |
| Max tags | 5 key-value tags per sandbox |
| Max timeout | 24 hours (Pro/Enterprise), 45 minutes (Hobby) |
| Default timeout | 5 minutes |
| Base system | Amazon Linux 2023 |
| User context | vercel-sandbox user |
| Writable path | /vercel/sandbox |
System Packages
Pre-installed: git, tar, gzip, unzip, curl, openssl, procps, findutils, which.
Install additional packages with sudo:
await sandbox.runCommand({
cmd: "dnf",
args: ["install", "-y", "package-name"],
sudo: true,
});
CLI Quick Reference
pnpm i -g sandbox
sandbox login
sandbox logout
sandbox create --connect
sandbox create --name my-app
sandbox create --image my-repo:v1
sandbox create --non-persistent
sandbox create --snapshot-expiration 7d
sandbox create --keep-last-snapshots 1
sandbox create --tag env=staging
sandbox fork <source>
sandbox fork <source> --name my-fork --vcpus 4 --env FOO=1
sandbox ls
sandbox ls --name-prefix ci- --sort-by name
sandbox ls --tag env=staging --limit 100 --cursor <token>
sandbox run -- node -e "console.log('hello')"
sandbox run --name my-app -- npm test
sandbox run --stop -- npm build
sandbox run --rm -- npm build
sandbox exec <name> -- npm install
sandbox exec <name> --stop -- npm build
sandbox connect <name>
sandbox cp local-file.txt <name>:/vercel/sandbox/
sandbox stop <name>
sandbox remove <name>
sandbox sessions list <name>
sandbox snapshot <name>
sandbox snapshots list --name <name>
sandbox snapshots get <snapshot-id>
sandbox snapshots remove <snapshot-id>
sandbox snapshots tree <name>
sandbox snapshots tree <name> --cursor <snapshot-id> --sort-order asc
sandbox config list <name>
sandbox config vcpus <name> <count>
sandbox config timeout <name> <duration>
sandbox config persistent <name> <true|false>
sandbox config snapshot-expiration <name> <duration|none>
sandbox config keep-last-snapshots <name> <count>
sandbox config keep-last-snapshots-for <name> <duration|none>
sandbox config delete-evicted-snapshots <name> <true|false>
sandbox config current-snapshot <name> <snapshot-id>
sandbox config network-policy <name> --network-policy deny-all
sandbox config tags <name> --tag env=prod
Common Patterns
Dev Server Pattern (Persistent)
const sandbox = await Sandbox.getOrCreate({
name: "my-dev-env",
source: { type: "git", url: "https://github.com/org/repo.git" },
ports: [3000],
timeout: ms("30m"),
onCreate: async (sbx) => {
await sbx.runCommand("npm", ["install"]);
},
onResume: async (sbx) => {
await sbx.runCommand({ cmd: "npm", args: ["run", "dev"], detached: true });
},
});
console.log("App running at:", sandbox.domain(3000));
Build and Test Pattern (Ephemeral)
await using sandbox = await Sandbox.create({
source: { type: "git", url: repoUrl },
persistent: false,
snapshotExpiration: ms("1d"),
});
const install = await sandbox.runCommand("npm", ["ci"]);
if (install.exitCode !== 0) throw new Error("Install failed");
const build = await sandbox.runCommand("npm", ["run", "build"]);
if (build.exitCode !== 0) throw new Error("Build failed");
const test = await sandbox.runCommand("npm", ["test"]);
process.exit(test.exitCode);
Base Sandbox + Forks Pattern
Maintain a single "base" sandbox with dependencies installed, and spawn fresh
children from it with Sandbox.fork. Each fork inherits the base's config
and is seeded from its current snapshot — no need to store snapshot IDs in
your code. New base snapshots are picked up automatically on the next fork.
await Sandbox.getOrCreate({
name: "my-base",
runtime: "node24",
keepLastSnapshots: { count: 5 },
onCreate: async (sbx) => {
await sbx.runCommand("npm", ["install", "-g", "typescript", "tsx"]);
},
});
async function runFromBase(code: string) {
await using sandbox = await Sandbox.fork({
sourceSandbox: "my-base",
persistent: false,
});
await sandbox.writeFiles([
{ path: "/vercel/sandbox/index.ts", content: Buffer.from(code) },
]);
return sandbox.runCommand("tsx", ["index.ts"]);
}
Long-Lived Workspace Pattern
const sandbox = await Sandbox.getOrCreate({
name: `workspace-${userId}`,
runtime: "node24",
keepLastSnapshots: { count: 1, expiration: ms("5d") },
onCreate: async (sbx) => {
await sbx.runCommand("git", ["clone", repoUrl, "."]);
await sbx.runCommand("npm", ["install"]);
},
});
await sandbox.runCommand("git", ["pull"]);