Skip to content
FLAVIO COPES
flaviocopes.com
2026

Vercel Sandbox tutorial: run untrusted code safely

By

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:

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.

Tagged: Services · All topics
~~~

Related posts about services: