Vercel Sandbox tutorial: run untrusted code safely
By Flavio Copes
Create a Vercel Sandbox, upload and run JavaScript, collect its output, restrict network access, enforce a timeout, and stop it safely.
Vercel Sandbox runs code inside an isolated Linux microVM.
This is useful when the code did not come from you. It might come from a user, an AI agent, a plugin, or a repository you want to inspect.
In this tutorial we’ll send a JavaScript file to a sandbox, run it, read its output, and destroy the running session.
Why not run the code in a Function?
A normal Vercel Function runs as part of your application.
It can access the environment variables and services you give it. Running unknown code there would let that code inspect the same environment.
A sandbox creates a separate machine with its own filesystem and network policy. The code runs there, away from the Function handling the request.
This gives us an important boundary:
application -> Sandbox API -> isolated microVM
The sandbox is still not a replacement for input validation, authentication, or spending limits. It is the place where potentially unsafe execution belongs.
Create a Node.js project
Create a small TypeScript project:
mkdir sandbox-demo
cd sandbox-demo
npm init -y
Install the Sandbox SDK and tsx:
npm install @vercel/sandbox
npm install --save-dev tsx typescript
Vercel recommends OIDC for authentication.
Link this directory to a Vercel project:
npx vercel link
Then pull a development token:
npx vercel env pull
The SDK reads the VERCEL_OIDC_TOKEN created in .env.local. Authentication is automatic when the code runs inside the linked Vercel project.
Create, run, and stop a sandbox
Create run.ts:
import { Sandbox } from '@vercel/sandbox'
const sandbox = await Sandbox.create()
const result = await sandbox.runCommand('node', [
'-e',
"console.log('Hello from the sandbox')",
])
console.log(result.stdout)
console.log(result.exitCode)
await sandbox.stop()
Run it:
npx tsx --env-file=.env.local run.ts
You should see:
Hello from the sandbox
0
An exit code of 0 means the command completed successfully.
The default runtime is Node.js. Vercel also provides other Node.js versions and Python runtimes, so check the current runtime list before choosing one explicitly.
Always stop in a finally block
Our first script leaves the sandbox running when runCommand() throws.
Wrap work in try and stop the sandbox in finally:
import { Sandbox } from '@vercel/sandbox'
const sandbox = await Sandbox.create()
try {
const result = await sandbox.runCommand('node', [
'-e',
"console.log('Hello from the sandbox')",
])
console.log(result.stdout)
} finally {
await sandbox.stop()
}
finally runs after success or failure.
Stopping promptly also controls cost. Don’t wait for the session timeout when the work is already done.
Upload a file
Passing a tiny program with node -e is convenient. Larger programs should be written to the sandbox filesystem.
Add this code inside the try block:
const code = `
const prices = [12, 18, 25]
const total = prices.reduce((sum, price) => sum + price, 0)
console.log(JSON.stringify({ total }))
`
await sandbox.writeFiles([
{
path: 'calculate.js',
content: Buffer.from(code),
},
])
writeFiles() accepts an array, so you can upload several files in one call.
Now run the uploaded file:
const result = await sandbox.runCommand('node', ['calculate.js'])
if (result.exitCode !== 0) {
throw new Error(result.stderr)
}
const output = JSON.parse(result.stdout)
console.log(output.total)
The application receives 55.
Be careful when parsing output. Unknown code can print anything, including an enormous string or invalid JSON. Check the output size and catch parsing errors.
Add a hard timeout
A user can submit code that never exits:
while (true) {}
Set a sandbox timeout when you create it:
const sandbox = await Sandbox.create({
timeout: 60 * 1000,
})
This session can run for up to one minute.
The default is longer, and maximum durations depend on the Vercel plan. Use the shortest duration that fits the job.
A timeout limits the whole session. Your application should also stop waiting for an individual command after its own deadline.
Block outgoing network requests
Code does not need open internet access for our calculation.
Create the sandbox with a deny-all network policy:
const sandbox = await Sandbox.create({
timeout: 60 * 1000,
networkPolicy: 'deny-all',
})
Now a command inside the sandbox cannot call arbitrary external services.
Some jobs need a package install before execution. Start with network access, perform the trusted setup, then lock the network before running unknown code:
const sandbox = await Sandbox.create({
networkPolicy: 'allow-all',
})
await sandbox.runCommand('npm', ['install'])
await sandbox.update({
networkPolicy: 'deny-all',
})
For a tool that needs one API, use an allowlist instead of opening the entire internet.
Vercel Sandbox can also broker credentials at the network boundary. The firewall adds a credential only to an approved outgoing request, so the secret never becomes an environment variable inside the sandbox.
That is safer than giving unknown code a raw API key.
Return command output from an API route
We can now put the same flow inside a Next.js Route Handler.
The route receives JavaScript and returns its standard output:
import { Sandbox } from '@vercel/sandbox'
export async function POST(request: Request) {
const code = await request.text()
if (code.length > 10_000) {
return new Response('Code is too large', { status: 413 })
}
const sandbox = await Sandbox.create({
timeout: 30 * 1000,
networkPolicy: 'deny-all',
})
try {
await sandbox.writeFiles([
{
path: 'program.js',
content: Buffer.from(code),
},
])
const result = await sandbox.runCommand('node', ['program.js'])
return Response.json({
stdout: result.stdout.slice(0, 20_000),
stderr: result.stderr.slice(0, 20_000),
exitCode: result.exitCode,
})
} finally {
await sandbox.stop()
}
}
The route limits the input and output size. Add authentication and rate limiting before exposing it.
Also cap how many sandboxes one user can create. Isolation protects your application, but it does not protect your bill from an unlimited public endpoint.
Use snapshots for repeated setup
Suppose every run needs the same packages.
Installing them each time adds delay. A snapshot captures the filesystem and installed packages:
const snapshot = await sandbox.snapshot()
console.log(snapshot.snapshotId)
Creating a snapshot stops that sandbox.
Later, start another sandbox from it:
const sandbox = await Sandbox.create({
source: {
type: 'snapshot',
snapshotId,
},
})
Use snapshots for trusted, repeatable setup. Don’t create a shared base snapshot after running unknown code inside it.
What the sandbox does not solve
A sandbox reduces the damage untrusted code can cause. You still need application rules around it.
Before shipping, decide:
- who may run code
- which runtimes are allowed
- how much CPU time each user gets
- which network destinations are allowed
- how much input and output you retain
- when files and snapshots expire
You should also treat output as untrusted. Escape it before inserting it into HTML.
Vercel eve uses Sandbox for agent-written code. The agent can work with files and commands, but the execution stays outside the main application.
That is the core pattern: let the application coordinate the task, and let an isolated sandbox execute the risky part.
The general Vercel tutorial explains how to deploy this API as a Preview before giving it real code to run.
Related posts about services: