E2B, sandboxes for model-generated code
E2B runs foreign code inside an isolated cloud virtual machine that you create with a single call and pay for by the second it stays alive. The e2b-dev/E2B repository holds around thirteen and a half thousand stars, the infrastructure code is public, and the current SDK version is 2.44.0 for both JavaScript and Python.
Why a separate sandbox when you could run the code yourself
This question belongs at the start, because the answer decides whether the rest of the text is any use to you.
When a model returns a fragment of code, you have three routes. The first is running it inside your own process, through eval or exec. That is the simplest route and the worst one: the code gets access to the process environment variables, meaning your API keys, to the server filesystem, and to the internal network the server sits in. No amount of filtering the generated text fixes this, because you are filtering something whose space of possible forms is unbounded.
The second route is a container on your own machine. Isolation becomes real, but containers share a kernel with the host, so the attack surface covers the entire system call interface. On top of that comes operational work that is easy to forget during a first prototype: processor and memory limits, cleaning up after containers that hung, network rules, an image with the libraries installed, and a queue for when ten users ask for code execution in the same second.
The third route is a service that already has all of this done, and that is what E2B is. You get a virtual machine with its own kernel, created on demand and destroyed after a set time, plus a library that lets you run commands inside it, write files, and expose ports outward. It makes sense when the code genuinely comes from a model or from a user rather than from you, and when you do not want to maintain a fleet of machines.
Equally important is knowing when E2B is the wrong choice. If the code you run is written by you and deterministic, isolation buys you nothing beyond a bill and an extra network hop. If the data the code touches cannot leave your infrastructure, you are left with self-hosting or a different solution. If the latency budget in your agent loop is measured in tens of milliseconds, every call to an external service will show. And if the task can be expressed as a pure function without executing code at all, calling that function usually beats generating a script.
The tool is model-agnostic, so it works the same with OpenAI, Claude, and locally hosted models. It plugs into agent frameworks such as LangChain, CrewAI, Pydantic AI, or the OpenAI Agents SDK as an ordinary tool whose body is an SDK call.
What sits underneath
Every sandbox is a microVM built on Firecracker, the same virtual machine monitor Amazon uses beneath its serverless functions. The difference against a container is fundamental: the machine has its own kernel, and the isolation boundary sits at the virtualisation level rather than at the level of host kernel namespaces.
A control agent runs inside the machine, and the SDK talks to it over the network. That agent executes commands, handles file operations, and streams output. Which is why every library method is asynchronous and carries its own request timeout, sixty seconds by default.
Startup time is the main selling point of this service, and here a warning is due. The E2B homepage states in one place that a sandbox in the same region as the client starts in under 200 milliseconds, and a few sections lower that it starts in 80 milliseconds. Both numbers sit on the same page, so treat them as an order of magnitude rather than a guarantee. The order of magnitude itself is credible, and it is what separates a microVM from a container image pulled from a registry on every run.
A sandbox receives an identifier and a domain of its own. If you start an HTTP server inside it, the getHost method returns the address at which the port is visible from outside, which is convenient for previewing an application the model just generated.
First run
Installation comes down to one package and one environment variable.
# core SDK, sandbox control
npm install e2b # JavaScript, Node 20.18.1 or newer in the 20 line, or 22 and up
pip install e2b # Python 3.10 or newer
# the wrapper for stateful code execution
npm install @e2b/code-interpreter
pip install e2b-code-interpreter
# command line tool, a separate package
npm install -g @e2b/cli
export E2B_API_KEY=e2b_your_keyThe variable name E2B_API_KEY is the default for the apiKey option, so in a typical project you never pass the key in code at all. The library checks on its side that the key starts with e2b_, and that can be turned off with validateApiKey, which matters only for a self-hosted deployment issuing keys in a different format.
The shortest sensible example looks like this.
import { Sandbox } from 'e2b'
const sandbox = await Sandbox.create({
template: 'base',
timeoutMs: 120_000,
metadata: { user: 'u-8891', task: 'csv-analysis' },
envs: { TZ: 'Europe/Warsaw' },
allowInternetAccess: false
})
try {
await sandbox.files.write('/home/user/data.csv', 'a,b\n1,2\n3,4\n')
const result = await sandbox.commands.run('python -c "import csv; print(sum(1 for _ in open(\'/home/user/data.csv\')))"', {
cwd: '/home/user',
timeoutMs: 30_000,
onStdout: (line) => console.log('OUT', line)
})
console.log(result.exitCode, result.stdout, result.stderr)
} finally {
await sandbox.kill()
}Four things from that example come back in every project. timeoutMs is the lifetime of the whole sandbox, 300,000 milliseconds by default, which is five minutes. metadata holds arbitrary string pairs you can later filter the sandbox list by, and it is the only sensible way to tie a machine to a user in your own system. allowInternetAccess set to false cuts outbound traffic and behaves exactly like putting 0.0.0.0/0 into network.denyOut. The finally block with a kill call is what separates a predictable bill from a surprising one.
Running code with state preserved
The commands.run method starts a process, and once it finishes nothing survives except files. If an agent is meant to carry out an analysis in several steps, where step two uses variables loaded in step one, you need something else. That is what the separate package wrapping a Jupyter kernel is for.
import base64
from e2b_code_interpreter import Sandbox
with Sandbox.create(timeout=300) as sandbox:
sandbox.run_code("import pandas as pd")
sandbox.run_code("df = pd.DataFrame({'x': [1, 2, 3], 'y': [2, 4, 8]})")
execution = sandbox.run_code(
"df['z'] = df.x * df.y\ndf.z.sum()",
on_stdout=lambda message: print("OUT", message.line),
timeout=60,
)
if execution.error:
print(execution.error.name, execution.error.value)
print(execution.error.traceback)
else:
print(execution.text) # text form of the last expression
print(execution.logs.stdout) # list of lines printed to standard output
for result in execution.results:
if result.png:
# the png field is a base64 encoded image, not raw bytes
with open("chart.png", "wb") as file:
file.write(base64.b64decode(result.png))The Execution object has four fields you need to know. results is a list of results where each element may carry a text, HTML, Markdown, SVG, and PNG form in parallel, because that is exactly how a Jupyter kernel behaves when drawing charts. logs separates standard output from the error stream, both as lists of lines. error is set only when the code raised an exception, and holds the name, the message, and the raw stack trace. The fourth field is the cell number, spelled execution_count in Python and executionCount in JavaScript.
The default language is Python, but run_code accepts a language parameter. Contexts are managed explicitly through create_code_context, list_code_contexts, remove_code_context, and restart_code_context, which lets you keep separate namespaces for different conversation threads inside one machine.
An important difference for the bill: code that raises an exception does not end the sandbox. The exception comes back in the error field, the machine keeps running and keeps being charged, and the agent can use that to fix the code and try again. This is the advantage of the approach over starting a process, but it needs to be remembered when planning cost.
Billing, or what exactly you pay for
The billing model here is simpler than at most providers, and that is its strength, because it can be computed before deployment.
You pay per second of a running sandbox, according to the formula given in the documentation: cost is the number of cores times the core rate plus memory in gibibytes times the memory rate, all multiplied by the number of seconds. The rates are 0.000014 dollars per second per virtual core and 0.0000045 dollars per second per gibibyte of memory. The default configuration is two cores and 512 mebibytes of memory, which works out to roughly 0.109 dollars per hour and roughly 0.009 dollars for a five minute run.
On top of that sits a plan layer that carries no usage allowance, only technical limits.
| Plan | Fixed cost | Session length | Concurrent sandboxes | Included disk |
|---|---|---|---|---|
| Hobby | 0 dollars, a one-off 100 dollars in credits | up to 1 hour | up to 20 | 10 GiB |
| Pro | 150 dollars monthly | up to 24 hours | up to 100, extendable to 1,100 | 20 GiB |
| Ultimate | quoted individually | set individually | set individually | set individually |
The consequence of that construction is that one hundred and fifty dollars a month on Pro buys limits rather than usage. Usage is added separately. With a hundred concurrent machines in the default configuration running without a break, the usage bill alone runs into thousands of dollars a month, so the calculation has to be done on your own numbers rather than on a single machine example.
Charging stops the moment a sandbox is paused, killed, or exceeds its timeout. There is no fee for a paused machine existing, nor for its snapshot. That single property is why the pause mechanism described below matters more than it first appears.
Pause, resume, and the sandbox lifecycle
A sandbox has a timeout set at creation, and it can be extended mid-flight through setTimeout. Once exceeded, whatever you set in the lifecycle field happens, and by default the machine is killed.
import { Sandbox } from 'e2b'
const sandbox = await Sandbox.create({
timeoutMs: 600_000,
lifecycle: { onTimeout: 'pause', autoResume: true }
})
const id = sandbox.sandboxId
// save state on demand, including process memory
await sandbox.pause()
// back to the same machine, same variables, same open files
const resumed = await Sandbox.connect(id, { timeoutMs: 600_000 })
const info = await resumed.getInfo()
console.log(info.state)
// a full copy of the machine, for two parallel variants of a solution
const [variantA, variantB] = await Sandbox.fork(id, { count: 2 })
await resumed.kill()A paused sandbox keeps both the filesystem and the memory state, so after resuming the variables, loaded libraries, and running processes all come back. The documentation puts pausing at roughly four seconds per gigabyte of memory and resuming at around one second. The keepMemory option set to false yields a filesystem-only snapshot, cheaper in time, but the machine cold-boots on resume, losing processes and open connections.
Two things about this mechanism directly affect cost and system design. First, paused sandboxes do not count toward the concurrency limit, so you can hold many of them without blocking your ability to start new ones. Second, a paused sandbox is retained indefinitely and does not disappear on its own. If you never call kill, it stays there forever, and you will see the list of such orphans only once you call Sandbox.list with a state filter.
Custom templates and the command line
The default base template carries a basic environment, but installing dependencies on every sandbox start wastes the very seconds you are paying for. The answer is a custom template built once.
import { Template, waitForPort, defaultBuildLogger } from 'e2b'
const template = Template()
.fromPythonImage('3.12')
.aptInstall(['git', 'curl'])
.pipInstall(['pandas', 'matplotlib', 'scikit-learn'])
.setWorkdir('/home/user')
.setEnvs({ MPLBACKEND: 'Agg' })
.setStartCmd('python -m http.server 8000', waitForPort(8000))
await Template.build(template, 'data-analysis:v1', {
onBuildLogs: defaultBuildLogger({ minLevel: 'info' })
})The setStartCmd method takes a start command and a readiness check, and the helper functions waitForPort, waitForURL, waitForFile, and waitForProcess generate the usual variants of that check. Without a correct readiness check, the first request to the sandbox hits a service that has not come up yet.
The command line is useful mostly for cleanup and diagnostics.
e2b auth login
# templates
e2b template init # a new template in the SDK format
e2b template create my-image
e2b template list
e2b template migrate # moving from e2b.Dockerfile and e2b.toml to the Template SDK
# sandboxes
e2b sandbox list # running ones only by default
e2b sandbox info <id>
e2b sandbox metrics <id>
e2b sandbox exec <id> -- python --version
e2b sandbox pause <id>
e2b sandbox kill <id>The template migrate command is a hint that the way templates are built has changed. Older material describes an e2b.Dockerfile alongside an e2b.toml, while the current approach builds from code through the mechanism above. The old format still works, but examples found online from before that change will look nothing like what you see in the documentation.
The licence and what is actually open
The project is described as open source and that is true, but the answer to the licence question depends on where you look, so a dependency audit needs this settled once.
The LICENSE file at the root of the e2b-dev/E2B repository contains the text of the Apache License 2.0, and that is the licence the GitHub interface and its API report as well. The license field of the e2b package in the npm registry, however, says MIT, the PyPI package says the same, and the LICENSE file inside the published tarball, once downloaded and extracted, holds the full MIT text with a copyright note naming FOUNDRYLABS, Inc. The same applies to @e2b/code-interpreter and @e2b/cli. The source of the discrepancy is the packages/js-sdk subdirectory in the repository, which carries its own MIT licence file overriding the root licence for whatever ships to the registry.
In practice this means the repository as a whole is Apache 2.0, while the packages you actually install are MIT. Both licences are permissive and neither restricts commercial use, so the difference hurts only where somebody compiles a list of dependency licences and expects one answer to the question about E2B.
The infrastructure is open too, in a separate repository e2b-dev/infra, also Apache 2.0, with around thirteen hundred stars. Deployment goes through Terraform. Know the scope of support before treating self-hosting as a fallback plan: Google Cloud is supported, Amazon Web Services support is marked as a beta, and Azure along with a plain Linux machine appear on the list as unfinished. Standing up your own fleet of microVMs is a task for a team that maintains infrastructure rather than a weekend project, so realistically this option protects you against the service disappearing rather than serving as a day-to-day alternative.
E2B against the alternatives
The market for agent sandboxes got crowded over the past two years, and the choice rarely turns on startup time.
| Solution | Isolation | Infrastructure code | SDK package | Package licence |
|---|---|---|---|---|
| E2B | Firecracker microVM | open, e2b-dev/infra | e2b 2.44.0 | MIT |
| Vercel Sandbox | Firecracker microVM | closed | @vercel/sandbox 3.0.1 | Apache 2.0 |
| Modal | provider-side container | closed | modal 1.5.4 | Apache 2.0 |
| Cloudflare Sandbox | container driven from Workers | closed | @cloudflare/sandbox 0.12.7 | Apache 2.0 |
| Daytona | provider infrastructure | open, daytonaio/daytona | @daytonaio/sdk 0.205.1 | Apache 2.0 |
| Docker on your own machine | container, kernel shared with host | yours | none, your own layer | depends on tooling |
The choice usually turns on three questions. Whether you already sit inside one provider's ecosystem, since a sandbox billed through a contract you hold anyway saves you a separate agreement. Whether you need isolation at the virtual machine level, because if the code comes from unknown users, a container sharing a kernel with the host is a weaker boundary. And whether being able to run the whole thing yourself matters, because here E2B and Daytona publish infrastructure code and the others do not.
Running containers yourself stands apart from that comparison. That route is cheaper on the provider invoice and more expensive in everything else, so it pays off when the load is large, predictable, and steady. With the bursty traffic typical of agents, paying by the second usually beats keeping machines that sit idle for most of the day.
Common mistakes
The first concerns time units and comes back in every project spanning both languages. The JavaScript SDK takes timeoutMs in milliseconds, while the Python SDK takes timeout in seconds. Copying the value 300_000 from one example into the other yields a sandbox alive for over three months instead of five minutes, with a bill matching that difference.
The second is a missing kill call. A sandbox does not end together with your process or with an HTTP request, it lives to its own timeout and is charged for the whole of it. A kill inside a finally block, or a with statement in Python, costs one line and closes an entire class of problems.
The third is treating a sandbox as durable storage. By default, once the timeout is exceeded, the machine is killed along with its contents. If you want the state kept, set lifecycle.onTimeout to pause or call pause yourself, and fetch whatever results must survive through files.read or downloadUrl.
The fourth is mixing the two ways of running code. commands.run starts a process that leaves no in-memory state behind, while runCode executes code in a Jupyter kernel context where variables from previous calls remain available. Surprise that a variable defined in one step vanished in the next almost always means the first method was used instead of the second.
The fifth concerns secrets. Values passed through envs land in the environment the generated code runs in, so the code can read them. If the sandbox needs to query an external API on your behalf, the right mechanism is rules under network.rules, which attach an authorisation header at the egress proxy, combined with network.allowOut narrowing the list of reachable addresses.
The sixth is carrying limits from a paid plan over to a free one. Maximum sandbox lifetime is twenty four hours on Pro but only one hour on Hobby. Code setting timeoutMs above an hour behaves differently on a development account than on a test account, and it will look like a random failure.
FAQ
Does a paused sandbox cost anything, and does it consume the limit?
No on both counts. Charging stops the moment a sandbox is paused, killed, or times out, and only running sandboxes count toward the concurrency limit. Paused machines are retained indefinitely and have to be removed by hand through kill.
What does an hour of sandbox time actually cost?
For the default configuration of two cores and 512 mebibytes of memory it comes to roughly 0.109 dollars per hour, using the formula from the documentation. Moving to four cores and four gibibytes raises that figure roughly threefold, so matching the machine size to the task is the simplest saving lever available here.
Can I run E2B on my own infrastructure?
You can, the infrastructure code is public under Apache 2.0 and deployment goes through Terraform. Google Cloud is supported, Amazon Web Services support is marked as a beta, and Azure is not supported. The operational effort matches maintaining a fleet of virtual machines, so for most teams this is insurance against the service disappearing rather than a daily working mode.
How does the e2b package differ from @e2b/code-interpreter?
The e2b package controls the sandbox: it creates one, runs commands, and handles files and networking. The @e2b/code-interpreter package is a wrapper adding a runCode method that executes code in a Jupyter kernel context, preserving variables between calls and returning charts as images. The second depends on the first, so you install it instead of, not alongside.
What happens when a sandbox exceeds its timeout?
By default it is killed along with its contents. That can be changed with the lifecycle field at creation: onTimeout set to pause saves the state instead of destroying the machine, and an additional autoResume flag brings it back automatically on the next call. The autoResume flag works only together with a memory-preserving pause.
Is E2B tied to a particular model provider?
It is not. The sandbox receives code as text and runs it, so where that code came from makes no difference. Integrations with agent frameworks come down to declaring a tool whose implementation calls the SDK, and they look identical no matter which model generates the code.
Documentation lives on the documentation site, the rates on the pricing page, and the source code in the SDK repository and the infrastructure repository.