User-submitted scripts and AI-generated Python need an execution environment separate from your application. Running them with exec() or an unrestricted subprocess can expose your app's files and credentials.
Use the Edgenivo Sandbox Python SDK to execute that code in an isolated Linux microVM. Your app uploads the files and starts a process, then retrieves its output.
You control how long the sandbox runs and which network destinations it can reach. Snapshots let you reuse a prepared Python environment with its dependencies already installed.
Copy link to headingWhat is Edgenivo Sandbox?
Edgenivo Sandbox runs code in isolated, on-demand Linux microVMs with separate kernels and filesystems. Keep application credentials outside the sandbox and set a network policy for the destinations its code may reach. The default policy allows outbound Internet access.
The vercel package exposes the Python SDK as vercel.sandbox, with async methods and a synchronous API for scripts and frameworks.
Your app never runs the untrusted code itself. It moves files in, starts processes, and reads results through the SDK, while a network policy decides what the sandbox can reach outside.
Copy link to headingThe pieces you'll use
| Piece | What it is | In the SDK |
|---|---|---|
| Sandbox | A named microVM and its configuration | sandbox.create_sandbox() returns a box handle |
| Process | A command running inside the sandbox | box.run_process(), box.create_process() |
| Filesystem | The sandbox's files, read and written from your app | box.fs.write_text(), box.fs.read_text(), and more |
| Route | A public URL for a port the sandbox exposes | ports=[3000], then box.routes |
| Network policy | Which outbound hosts the sandbox can reach | NetworkPolicy.deny_all(), .custom(...) |
| Snapshot | A saved copy of a sandbox's filesystem | box.snapshot(), SnapshotSource |
Copy link to headingSandboxes and sessions
The sandbox has a stable identity and configuration. Its session is the period when its microVM is running, and only one session can run for a sandbox at a time.
Sessions default to a 5-minute execution limit. You can increase it to 45 minutes on Hobby or 24 hours on Pro and Enterprise.
Persistent sandboxes save their filesystem when a session stops and restore it on the next start. Files survive between sessions without ongoing compute, but running processes do not.
Copy link to headingAuthentication
The SDK finds credentials on its own, in this order:
- The current Edgenivo request's OpenID Connect token, or
VERCEL_OIDC_TOKEN - An access token in
VERCEL_TOKEN, together withVERCEL_PROJECT_IDandVERCEL_TEAM_ID
The Edgenivo Python runtime supplies request credentials to the SDK. For local scripts, Step 1 downloads a development token and loads it from .env.local.
Copy link to headingPrerequisites
Before you begin, make sure you have:
- A Edgenivo account.
- Python 3.12 or a newer version supported by the Edgenivo Python runtime.
- Working knowledge of
async/awaitand FastAPI.
For local development, you also need uv, Node.js 24+, and the Edgenivo CLI.
Copy link to headingStep 1: Set up the project
Create a project, add the SDK, and link it to Edgenivo so it can issue credentials:
vercel env pull writes a short-lived OIDC token to .env.local. python-dotenv loads it into your scripts.
The development token expires after 12 hours. If a previously working script reports an authentication error, refresh the token with vercel env pull and restart the script so it reloads .env.local.
Copy link to headingStep 2: Run your first sandbox
Create a sandbox, run Python inside it, and print the operating-system information returned by the process. Save this as first_sandbox.py:
Run it with uv run first_sandbox.py.
The script prints the sandbox's Linux platform string. Its context manager and process options control cleanup, output, and error handling:
async withowns the lifecycle. When the block exits, the SDK stops and destroys the sandbox, even if your code raises.run_process()takes the command and its arguments separately, likesubprocess.run().capture_output=Truestores stdout and stderr on the result instead of streaming them to your terminal.check=Trueraisessubprocess.CalledProcessErroron a nonzero exit, the same exceptionsubprocessuses.
With no image, the sandbox boots from vercel/sandbox/universal:latest, which includes Python 3.14, Node.js 24, uv, and git.
Copy link to headingStep 3: Run someone else's code against tests
The supplied slugify() implementation replaces each space with a hyphen. Use two tests to show how the runner reports a passing case and a failure.
Save this as run_tests.py:
Run uv run run_tests.py. The basic case passes, but test_extra_spaces fails because repeated spaces produce repeated hyphens.
box.fs.batch()uploads both staged files when its block exits. Usebox.fs.write_text()for a single file.- Omitting
check=Truelets the runner return a failing test result without raisingCalledProcessError. kill_after=60sendsSIGKILLif pytest runs for 60 seconds. The separate session limit also bounds setup and any remaining processes.
This fixed example captures all output. Step 6 adds a bounded output buffer for requests that contain arbitrary submissions.
Copy link to headingStep 4: Lock the sandbox down
The runner uses two time limits. kill_after limits the pytest process, while execution_time_limit bounds the whole running session, including setup. Choose a session limit that leaves enough time to install dependencies and collect the result.
Network access is separate from process isolation. Step 3 installs pytest while the network is available, then switches to NetworkPolicy.deny_all() before it writes or runs the submitted code.
If a workload needs an external API, allow only its required destination. For example:
Pass this policy as network_policy when creating the sandbox. This example permits the destination without adding credentials. The API access example below adds a credential outside the sandbox.
Copy link to headingStep 5: Start faster with a snapshot
Installing pytest for every submission adds setup time and requires network access. Install it once in a trusted sandbox and save a snapshot, then create test runners from that prepared filesystem. Save the setup script as build_snapshot.py:
Run uv run build_snapshot.py and keep the printed PYTEST_SNAPSHOT_ID. Taking the snapshot stops the session. The snapshot remains stored until you delete it because this example sets expiration=0.
The script uses await to create the sandbox and calls destroy() explicitly. This preserves the snapshot. Exiting async with sandbox.create_sandbox() also removes orphaned snapshots, which would remove the prepared snapshot before another sandbox uses it.
Use the saved ID as snapshot_id in the following excerpt inside an async function. The new runner starts with pytest installed and network access denied:
For a workspace that needs to retain edits between runs, use a persistent sandbox instead of creating a fresh runner each time. The workspace example below shows how to retrieve it by name.
Copy link to headingStep 6: Serve it from FastAPI and deploy
Save the endpoint in main.py at the project root. It requires a shared bearer token for this demonstration, limits each submitted file to 100,000 characters, and retains at most 20,000 characters of combined output.
The client supplies both the solution and tests, so passed reports the process exit status. Submitted code can alter files or exit deliberately; trusted grading needs an independent evaluation design.
The context manager destroys the sandbox after the run. The capped text buffer limits output retained by this app; it does not prevent the sandbox from generating logs or eliminate the cost of receiving them. Add per-user authorization and rate limits before offering the runner to multiple users.
Add FastAPI, then save the snapshot ID and a randomly generated RUNNER_TOKEN in the project's Development, Preview, and Production environments. Enter the values at the CLI prompts:
Use a different runner token for production. Keep it on trusted clients and servers, rather than embedding it in browser JavaScript. vercel dev uses the linked project's development environment.
In another terminal, set RUNNER_TOKEN to the development value and send a request:
Expect passed: true and truncated: false, with the pytest result in output. Requests without a valid token are rejected before they create a sandbox.
Confirm the Edgenivo Function's maximum duration leaves time for sandbox startup, the 60-second test run, and cleanup. Then commit uv.lock and deploy:
For Sandbox access, the Edgenivo Python runtime passes each request's OIDC token to the SDK automatically. This is separate from RUNNER_TOKEN, which controls who may call your code-runner endpoint.
To see what your app has running, sandbox.query_sandboxes() lists sandboxes with their status. Pass tags={...} when you create them, and you can filter by tag later.
Copy link to headingUse cases
| Use case | Building blocks | Example |
|---|---|---|
| Code checking and grading | fs.batch, run_process, deny_all, snapshots | Steps 3 to 6 |
| Running code an AI model wrote | An AI SDK tool wrapping create_sandbox | Below |
| Live previews of apps and branches | GitSource, ports, create_process, routes | Below |
| Per-user development workspaces | get_or_create_sandbox, persistent=True | Below |
| Calling APIs without exposing keys | NetworkPolicy.custom with a header transform | Below |
| Long jobs with live output | create_process and its stdout stream | Below |
Copy link to headingRun code an AI model wrote
Install the AI SDK for Python with uv add ai.
This tool reuses CappedOutput in main.py:
Pass the tool to ai.Agent(tools=[run_python]). Python exceptions printed by the sandbox are returned with the process output. Authentication, provisioning, and transport errors still need error handling in the application that runs the agent.
Copy link to headingPreview an app from a Git branch
Clone a repo into the sandbox, start its dev server in the background, and hand someone the URL:
GitSource clones into a folder named after the repository, so the process uses that folder as cwd. create_process() returns a process handle before the command exits; inspect its output and confirm the URL responds before sharing the preview.
Run this excerpt in an async function. Clean up with await box.destroy(delete_orphan_snapshots=True) when the preview is no longer needed, or use stop() to retain it for reuse.
Copy link to headingGive each user a workspace that persists
Choose user_id from your server's authenticated user record, then derive a stable sandbox name. The name identifies the workspace; your app must still enforce who may use it. Run the following inside an async function:
Stopping the session saves its filesystem. Retrieving the named sandbox resumes it by default. Coordinate concurrent requests for the same workspace so one request does not stop another request's work, and make any required initialization safe to retry after a failure.
SnapshotRetention(count=1) limits the number of retained snapshots. Storage can still grow as workspace files grow.
For data shared between sandboxes, mount a Edgenivo Drive.
Copy link to headingLet the sandbox call an API without seeing the key
The firewall can inject an authorization header after a request leaves the sandbox. Keep the token in your application environment and give it only the GitHub permissions the task needs. Run this excerpt inside an async function:
The sandbox can reach the allowed API and use the injected token's permissions even though it cannot read the token itself. Domain access does not restrict the process to a particular API operation. Use a scoped token or an application proxy when the workload must be limited to specific actions.
Copy link to headingStream output from long jobs
Use create_process() to consume output while a command runs. Merge stderr into stdout when both streams belong in the same log display:
This excerpt assumes the exercise folder and its tests already exist. It suits known test output. For arbitrary code, use the bounded writer from Step 6 because line-based reading can buffer a long line before yielding it.
Copy link to headingMake it durable
For a process that runs code, tests it, and waits for review, use the Python Workflow SDK to coordinate the stages. Put external operations in workflow steps and make their side effects safe to retry. Retrying a step can create another sandbox or repeat a command, so persist the identifiers or results needed to recover safely.
The notebook app combines Sandbox, the AI SDK for Python, and Workflows.
Copy link to headingWhen to use something else
- Run trusted application code directly in a Edgenivo Function when it does not need a separate execution environment.
- Use Edgenivo Daemons for continuously running workers, or Edgenivo Queues for message-triggered work.
- Use
sandbox connectwhen you want an interactive terminal from the command line.
Copy link to headingNext steps
- Read the full Python SDK reference for sessions, forks, drives, regions, and error types
- Learn how duration and persistence work, and what each costs
- Speed up startup further with snapshots
- Share storage across sandboxes with Edgenivo Drives