Wednesday, November 4, 2026 Edition No. 228
Search
Review · Infrastructure · 26 min read

E2B Sandboxes: Hands-On with the Full Sandbox Lifecycle

By Fabio Douek

Jump to section
Explain (TLDR) like I am...
What is this?

Imagine a magic toy box you can borrow for your robot. The robot can build anything inside it, make a mess, even break things, and none of it touches your real room. When playtime is over, the box can vanish, or it can freeze exactly as it is until you come back.

The freezing part is the clever trick. You can stop the box in the middle of a game, walk away for days, and when you open it again the blocks are still mid-tower. You can even make copies of the box so three robots can try three different ideas at once.

E2B is a hosted execution environment: code your application or its AI agent generates runs in an isolated virtual machine on E2B's infrastructure, billed per second, with the vendor's open source runtime available if the organization prefers to self-host.

The diligence points are lifecycle and retention. By default a sandbox is destroyed five minutes after creation, but paused sandboxes are retained indefinitely with no automatic expiry, so data persists until someone deletes it. Map who owns cleanup, set a spending cap in the console, and treat paused state as stored data under your retention policy.

The condition this treats is unsafe execution of machine-written code on infrastructure you care about. The intervention is a disposable Firecracker virtual machine per task, and in my testing it started in about a third of a second and resumed from pause in under one.

The evidence is good for the core mechanism: memory, files and running processes survived three pause cycles intact. Known side effects sit in the same area, with open bug reports and recent service incidents around pause and resume. Good candidates are agent products needing per-user state; poor candidates are GPU workloads, which it does not support.

Notice how much quiet worry disappears when the code an agent writes can only ever break a box that was built to be broken. Engineers stop hovering over every generated command, and the conversation shifts from whether to let the agent act to what it should do next.

The new tension is about forgetting. Pause is so cheap and so permanent that sandboxes pile up unseen, and the defaults quietly destroy work after five minutes unless someone chose otherwise. The healthy habit is naming who tidies up, and making that a shared, explicit agreement rather than an afterthought.

Think of E2B as a looper pedal for your agent's work. You hit record, the sandbox plays, and at any bar you can freeze the loop exactly where it is, walk away, and drop back in on the same beat later.

Forking is the overdub: one take becomes three parallel takes, each free to improvise from the same starting phrase. The tempo is quick, with sub-second starts and resumes in my sessions, but the defaults matter. Leave the timeout alone and the loop simply stops after five minutes and is gone.

The positioning is "a computer for your agent, with a pause button." Time to value is minutes: one install, one API key, and the first sandbox starts in well under a second. The free tier's one-time $100 credit covered this entire walkthrough for about a cent.

The differentiated story is state. Competitors start fast too, but few keep memory and running processes through a pause indefinitely, or clone a live sandbox into copies. That is the hook for products that need per-user workspaces their agents can return to days later.

GitHub See the source code here: e2b-sandbox-lifecycle

Overview

E2B runs AI-generated code in cloud sandboxes. Each sandbox is a Firecracker microVM with its own kernel, filesystem and network, which you create, drive and destroy from a Python or JavaScript SDK. It is the default answer to “where does my agent run the code it writes” for a lot of teams, and the runtime is open source under Apache-2.0 if you would rather host it yourself.

An E2B sandbox can be paused with its memory, files and running processes intact, kept paused indefinitely at no compute cost, woken automatically by the next API call or HTTP request, and forked into copies that inherit the live state.

This post, part 1 of 2, walks one sandbox through its whole life, from Sandbox.create() to cleanup, with a runnable script per stage. I ran everything against SDK 2.59.0 (released 2026-10-22) on the free Hobby plan in us-west-1, using Python, and every timing below is from my runs. I first wrote this post on 2.52.1; the scripts ran unchanged on 2.59.0, and the numbers held except for the template test (see Test 8). The single most important thing to know before you start: the default timeout is 5 minutes and the default action on timeout is kill. If you don’t opt into pausing, your sandbox and everything in it is gone five minutes after creation. Part 2 covers credentials: giving an agent in the sandbox API keys without putting the keys inside it.

Architecture

There are two planes. The E2B API handles lifecycle operations (create, connect, list, pause, fork, snapshot, kill). Inside every VM, a small daemon called envd serves commands, the filesystem and process streams, and a proxy exposes any port as https://<port>-<sandbox-id>.e2b.app.

TOPOLOGY Your app e2b SDK (Py / JS) E2B API create · connect · list pause · fork · snapshot · kill Firecracker microVM own kernel · 2 vCPU · 512 MiB envd commands · files · streams your processes e.g. http.server :3000 lifecycle exec / files Public URL https://3000-<id>.e2b.app LIFECYCLE STATES Running Paused Killed new sandbox(es) same state pause() or timeout (on_timeout=pause) connect() / auto-resume kill() / timeout (default) kill() fork() / snapshot

Three things in that picture matter for the rest of the post:

State Billed? Counts toward concurrency? Expires?
Running Yes, per second (vCPU + RAM) Yes At its timeout (kill or pause) and at the plan's runtime cap (1 h Hobby, 24 h Pro)
Paused No No Never. E2B does not delete paused sandboxes on its own
Killed No No Gone for good

There is no separate resume() call in the SDK: you resume by calling connect(). And pausing resets the continuous-runtime clock, so a sandbox that pauses and resumes can outlive the 1 h / 24 h cap indefinitely.

Setup

Step 1: Account and API key

Sign up at e2b.dev and create a key under API Keys in the console. The Hobby plan is free and comes with $100 of one-time usage credit, which is roughly 900 hours of a default sandbox. This whole walkthrough cost me about a cent (more on that in Pricing).

Step 2: Install the SDK and the companion code

git clone https://github.com/fabiodouek/my2centsai-blog-samples.git
cd my2centsai-blog-samples/e2b-sandbox-lifecycle
cp .env.example .env            # paste E2B_API_KEY=e2b_...
uv venv && uv pip install -e .  # installs e2b==2.59.0 + python-dotenv
source .venv/bin/activate

The Python SDK needs Python 3.11+ (2.56.0 dropped 3.10). I pinned e2b==2.59.0 on purpose: E2B ships SDK releases every few days (about 40 JS SDK releases between mid-June and early October 2026), and minor versions have changed defaults. For example, 2.51.0 moved create and connect to new v2 endpoints and stopped setting client-side defaults for timeout and resources. Bump the pin deliberately.

The CLI is optional but handy for inspection and cleanup: npx @e2b/cli sandbox list (or npm i -g @e2b/cli / brew install e2b).

Step 3: Tag everything

Every script tags its sandboxes with metadata, so cleanup at the end only touches what the demo created:

# common.py
DEMO_TAG = {"demo": "my2cents-lifecycle"}

def tagged(**extra: str) -> dict:
    return {**DEMO_TAG, **extra}

I’d recommend the same habit in production: tag by user, environment and job, because metadata is how you find sandboxes again later.

Testing

Test 1: Create a sandbox and inspect the defaults

python 01_create.py      # optional arg: number of timed creates (default 5)
from e2b import Sandbox

sbx = Sandbox.create(metadata=tagged(test="01"))
info = sbx.get_info()
print(info.cpu_count, info.memory_mb, info.end_at - info.started_at, info.lifecycle)
[  0.85s] Sandbox.create()
template   : base (rki5dems9wqfm4r03t7g)
state      : running
cpu / mem  : 2 vCPU / 512 MiB
lifetime   : 300s
lifecycle  : {'on_timeout': <SandboxOnTimeout.KILL: 'kill'>, 'auto_resume': False}
envd       : 0.10.0

=== 5 sequential creates ===
[  0.79s] create #1
[  0.83s] create #2
[  0.33s] create #3
[  0.23s] create #4
[  0.33s] create #5
median 0.33s | min 0.23s | max 0.83s

That confirms the documented defaults: 2 vCPU, 512 MiB, a 300-second timeout, and kill on timeout. Five sequential creates had a median of 0.33 s, measured from my laptop, end to end including the SDK round trip.

Two other observations. free -m inside the VM reports 473 MiB total, a bit under the nominal 512. And the root filesystem showed 22 GB (22.8 GB in the console), even though the billing docs list 10 GiB of disk for Hobby. I can’t tell you whether that’s a temporary allowance or a docs lag, so don’t plan around it.

The console shows each sandbox live, with CPU, RAM and disk charts, the remaining timeout, and Pause and Kill buttons:

E2B console sandbox detail page in the Monitoring tab: status RUNNING, template base, about 29 minutes left before timeout, 2 cores, 512 MB memory, 22.8 GB disk, with CPU and RAM charts around 50% from a busy-loop process, and Pause and Kill buttons

Test 2: Commands, files and the command timeout

python 02_commands_files.py

Commands are blocking by default, can stream stdout and stderr through callbacks, or can run in the background and hand you a handle:

sbx.commands.run(
    "for i in 1 2 3; do echo tick $i; sleep 1; done",
    on_stdout=lambda line: print(f"  stream> {line.strip()}"),
)

sbx.files.write("/home/user/notes/hello.txt", "hello from my laptop\n")
sbx.files.write_files([
    {"path": "/home/user/notes/a.txt", "data": "a"},
    {"path": "/home/user/notes/b.txt", "data": "b"},
])

server = sbx.commands.run(
    "cd /home/user/notes && python3 -m http.server 3000",
    background=True,
    timeout=0,  # without this the default 60s command timeout kills it
)
print(f"https://{sbx.get_host(3000)}/hello.txt")
exit=0
Python 3.11.6
user
/home/user

  stream> tick 1
  stream> tick 2
  stream> tick 3
hello from my laptop
  file  a.txt
  file  b.txt
  file  hello.txt
server pid: 1529
  running: pid=1529 cmd=/bin/bash -l -c cd /home/user/notes && python3 -m http.server 3000
preview URL: https://3000-iwmtv6w8e12wb0ceo7b5i.e2b.app/hello.txt

[  3.00s] sleep 10 with timeout=3
  TimeoutException: Request timed out: ...

The gotcha is in that last line. Commands have their own timeout, 60 seconds by default, and it kills the process even when background=True. A dev server or a long-running agent process needs timeout=0. Two smaller API notes: Python uses write_files() for multiple files where JavaScript overloads write(), and code runs as user in /home/user by default. A production write-up from Codecloud adds another gotcha: a blocking commands.run() waits for child processes too, so anything that daemonizes should be started with background=True.

The console has a browser terminal for every running sandbox, which is the fastest way to poke around while debugging. Don’t expect to find the script’s sandbox there: 02_commands_files.py kills it on its last line. For this screenshot I started a separate sandbox by hand with a CPU-burning process and kept it alive (it isn’t part of the companion code):

E2B console Terminal tab attached to a running sandbox, showing ps aux output with a python3 busy loop at 99.9% CPU, the envd daemon running as root, and a second python process holding 200 MB of memory

Notice the /usr/bin/envd process running as root: that’s the in-VM daemon from the architecture diagram. One side effect I noticed: opening the Terminal tab pushed that sandbox’s remaining timeout from about 24 minutes to about 30. The console’s terminal connection appears to extend the timeout the same way an SDK connect() would, so a sandbox you’re watching in the browser may live longer than your code asked for.

Test 3: Timeouts, set_timeout vs connect

python 03_timeouts.py
sbx = Sandbox.create(timeout=120, metadata=tagged(test="03"))
sbx.set_timeout(20)       # exact: 20 s from now, can shorten
sbx.connect(timeout=5)    # only extends, never shortens
time.sleep(remaining(sbx) + 10)
Sandbox.get_info(sandbox_id)
remaining: 119s (asked for 120)
set_timeout(20): exact, can shorten          -> remaining: 19s
connect(timeout=5): only extends, never shortens -> remaining: 19s
gone: SandboxNotFoundException: Sandbox ikxrlwxsu5gsoiu2ba98y not found
listed: False

This is the behavior that bites people. set_timeout() sets the expiry to exactly now + N. connect(timeout=N) sets it to max(current expiry, now + N), so a short connect timeout is silently ignored. And when the timer runs out with the default lifecycle, the sandbox doesn’t pause: it’s deleted, files and all.

Also watch the units. Python takes seconds (timeout=60), JavaScript takes milliseconds (timeoutMs: 60_000). Porting a snippet between the two without converting gives you a sandbox that dies after about a second (the JS SDK rounds up to whole seconds), or one that asks for 16 hours and runs until your plan’s 1 h or 24 h cap.

⚠️ Pin your SDK version and spell lifecycle values exactly. According to the SDK release history, before 2.48.0 a typo such as on_timeout="Pause" was not rejected, so the sandbox fell back to the default and was killed on timeout instead of paused.

Test 4: Pause and resume, three cycles

python 04_pause_resume.py 3   # number of pause/resume cycles

This is the feature that sets E2B apart, so I tested it harder. The script starts a Python process whose counter lives only in process memory (mirrored to a file so I can read it), writes a file each cycle, then pauses, waits 10 seconds and resumes, three times. At the end it reattaches to the process’s stdout stream.

counter = sbx.commands.run(
    "python3 -u -c \"import time\n"
    "i = 0\n"
    "while True:\n"
    "    i += 1\n"
    "    print(i)\n"
    "    open('/tmp/counter', 'w').write(str(i))\n"
    "    time.sleep(1)\"",
    background=True,
    timeout=0,
)

for cycle in range(1, 4):
    before = sbx.files.read("/tmp/counter")
    sbx.pause()
    time.sleep(10)       # wall clock moves, the sandbox doesn't
    sbx.connect()        # this is "resume"
    after = sbx.files.read("/tmp/counter")
=== Cycle 1 ===
[  0.73s] pause()
state: paused
[  0.24s] connect() / resume
counter before=5 after=6 (continues, not reset => memory restored)
pid 1520: alive
files: ['cycle-1.txt', 'state.txt']

=== Cycle 2 ===
[  0.69s] pause()
[  0.23s] connect() / resume
counter before=6 after=7
pid 1520: alive
files: ['cycle-1.txt', 'cycle-2.txt', 'state.txt']

=== Cycle 3 ===
[  0.70s] pause()
[  0.25s] connect() / resume
counter before=8 after=9
pid 1520: alive
files: ['cycle-1.txt', 'cycle-2.txt', 'cycle-3.txt', 'state.txt']

=== Reattach to the counter's stdout after resume ===
lines received in 6s: 6 ['13', '14', '15']
stream OK

All three cycles passed:

  • Memory survives. The counter went from 5 to 6 across a 10-second pause, not from 5 to 15 and not back to 1. The process was frozen, not restarted.
  • Processes survive. Same PID throughout.
  • Files survive. All cycle files were present after every resume.
  • Pause took 0.69–0.73 s, resume 0.23–0.25 s. The docs quote about 4 seconds per GiB of RAM for pause and about 1 s for resume. My sandboxes were only using a few tens of MiB, which is the likely reason I came in far under that. Budget for the documented figure if your sandboxes hold real memory: an 8 GiB sandbox could take around 32 seconds to pause.
  • Reattaching to stdout worked. I specifically tested this because there are open high-priority reports of stdout streams stalling when you reattach after a resume (E2B#1587 among others). In my runs on 2.52.1, and again on 2.59.0, the stream delivered every line. That’s two runs, not proof the bug is fixed.

Pausing and resuming is the area with the most open issues and the most recent service incidents (the community research I did counted five pause, resume or connect incidents on the status page history between July and early October 2026, the latest on 2026-10-02). A filesystem-persistence bug across repeated resumes (E2B#884) was closed as fixed and then re-reported by a user. None of that showed up in my testing, but if paused state is your product, build in verification: checksums or a canary file you check after every resume.

In the console, a paused sandbox shows Resume instead of Pause, plus when it was paused and how long it ran:

E2B console detail page for a paused sandbox built from the my2cents-datasci template: status PAUSED, paused at 12:05:16 AM UTC, ran for 1 second, 2 cores, 2,048 MB memory, 12.8 GB disk, with Resume and Kill buttons

The Events tab logs every lifecycle transition. These are the same events you can subscribe to with lifecycle webhooks, which carry execution time and resources if you want to do your own cost accounting:

E2B console Events tab for a paused sandbox listing two lifecycle events: CREATED at 00:05:14.91 with sandbox metadata in the data column, and PAUSED at 00:05:16.24 with execution time data

If you don’t need memory restored, sbx.pause(mode="filesystem") takes a filesystem-only snapshot (it replaced keep_memory=False in SDK 2.55.0): files survive, processes don’t, and the next resume is a reboot (docs).

Test 5: Auto-pause on timeout and auto-resume on activity

python 05_auto_resume.py

Manual pause is fine for scripts. For a product, you want the sandbox to pause itself when idle and wake when something touches it. That’s two lifecycle flags:

sbx = Sandbox.create(
    timeout=20,
    metadata=tagged(test="05"),
    lifecycle={"on_timeout": "pause", "auto_resume": True},
)
sbx.files.write("/home/user/hello.txt", "hello from a paused sandbox\n")
sbx.commands.run("cd /home/user && python3 -m http.server 3000", background=True, timeout=0)

# ...poll Sandbox.get_info(id).state until "paused"...
content = sbx.files.read("/home/user/hello.txt")   # wakes it
lifecycle: {'on_timeout': <SandboxOnTimeout.PAUSE: 'pause'>, 'auto_resume': True}
preview URL: https://3000-in85jhl0d77kt9gq2bwtk.e2b.app/hello.txt

=== Wait for timeout -> auto-pause ===
paused after ~21s of polling

=== Wake with files.read() ===
[  0.25s] files.read on paused sandbox
hello from a paused sandbox
state after read: running, new end_at: 2026-10-25 09:12:35+00:00

=== Pause again, wake with HTTP ===
state: paused
HTTP 200: hello from a paused sandbox
[  0.33s] GET preview URL on paused sandbox
state after HTTP: running

Both wake paths worked. A files.read() against the paused sandbox returned the content in 0.25 s with no explicit resume call. An HTTP GET to the public preview URL woke the sandbox, restored the still-running http.server and returned HTTP 200 in 0.33 s. That second path is the interesting one: a preview environment or an agent’s web app can sleep at zero compute cost and wake on its first request.

Two details from the run. Polling Sandbox.get_info() every 5 seconds did not wake it, so status checks are safe. After waking, the sandbox gets at least 5 minutes of runtime (or its original timeout, if longer) before it auto-pauses again, and the lifecycle setting persists across cycles. auto_resume requires memory-preserving pause; you can’t combine it with mode="filesystem".

Test 6: List, filter, and connect from another process

python 06_list_connect.py

In a real system, the process that creates a sandbox is rarely the one that uses it later. Metadata plus Sandbox.list() is how you find it, and Sandbox.connect(id) is how you pick it up:

from e2b import Sandbox, SandboxQuery, SandboxState

paginator = Sandbox.list(
    query=SandboxQuery(metadata={"userId": "alice"},
                       state=[SandboxState.RUNNING, SandboxState.PAUSED]),
    limit=100,
)
items = paginator.next_items()
while paginator.has_next:
    items.extend(paginator.next_items())

# ...in a different Python process, with only the ID:
sbx = Sandbox.connect(sandbox_id)
Create three tagged sandboxes
  i9vnw3wxcwi7wg2plqbd6 userId=alice
  ib9sorwzv31movgjuqfhv userId=alice
  iv0ti2xxvalw7yiw4u2s1 userId=bob
  paused ib9sorwzv31movgjuqfhv

default (no query): 3
state=running: 2
state=paused: 1
userId=alice, any state: 2
    ib9sorwzv31movgjuqfhv paused userId=alice
    i9vnw3wxcwi7wg2plqbd6 running userId=alice

Connect from another process (resumes the paused one)
[  0.76s] Sandbox.connect(id) from second process
  [worker] read: owned by alice
back in parent, worker file: the worker was here
state now: running

Filtering by metadata and state works as documented, and the paginator caps pages at 100. A second, unrelated process connected to the paused sandbox by ID in 0.76 s, which resumed it, and wrote a file the parent then read. Two defaults differ between tools: Sandbox.list() in the SDK returns running and paused sandboxes, while e2b sandbox list in the CLI shows running only unless you pass --state running,paused. That’s an easy way to convince yourself you have no leftover sandboxes when you do.

The console’s sandbox list shows the same picture. Note the counter in the top right: three sandboxes are listed, but only two count as concurrent, because paused sandboxes don’t count toward the plan’s concurrency limit (20 on Hobby):

E2B console sandbox list with three sandboxes: one PAUSED on the my2cents-datasci template with 2.0 GB memory, and two RUNNING on the base template, one at 51% CPU; the header shows 2 concurrent sandboxes

Metadata is visible per sandbox, which helps when you’re tracing a user’s sandbox from a support ticket:

E2B console sandbox metadata popover showing JSON with demo my2cents-lifecycle, test showcase and userId bob

Test 7: Fork and snapshot

python 07_fork_snapshot.py

Forking shipped in July 2026 (SDK 2.34.0 for Python), with a CLI command following in September. A fork is a new sandbox that starts from the original’s files, processes and memory. Snapshots are the persistent version: capture once, create many sandboxes from it later.

results = sbx.fork(count=3, timeout=120)  # always pass timeout: the SDK default is 15s
forks = [r for r in results if isinstance(r, Sandbox)]
errors = [r for r in results if not isinstance(r, Sandbox)]

snap = sbx.create_snapshot(name="my2cents-lifecycle-demo")
restored = Sandbox.create(snap.snapshot_id, timeout=120)
=== Fork x3 ===
[  0.57s] fork(count=3, timeout=120)
forks: 3, errors: 0 []
  fork 1 i85nwpvq3ryakayuvdh54: state.txt='shared state' counter=6 pid 1520 alive
  fork 2 iajm0ldx33wti61hts3qr: state.txt='shared state' counter=7 pid 1520 alive
  fork 3 it8v5xxsvk7nfgsynbfjp: state.txt='shared state' counter=8 pid 1520 alive
original sees fork writes? no  (forks diverge)
original counter still running: 8

=== Snapshot -> new sandbox ===
[  0.44s] create_snapshot()
snapshot: <project>/my2cents-lifecycle-demo:default
[  0.38s] Sandbox.create(snapshot_id)
restored ih2d669g297p9phu40tjb: 'shared state'
deleted snapshot: True

Three forks in 0.57 s, each with the parent’s file and the parent’s counter process still running with the same PID. Writes in the forks didn’t leak back to the original. A snapshot took 0.44 s and a new sandbox from it 0.38 s. This is the primitive for “try three approaches in parallel from the same state” in agent workflows, or for checkpointing before a risky step.

Things to know before you rely on it:

  • fork() returns a list that mixes Sandbox objects and exceptions for partial failures instead of raising, so filter the results as above.
  • The SDK caps a fork call at 20 copies (the API allows up to 100), and the count has to fit in your concurrency limit.
  • Forks default to a 15-second timeout in the SDK and API, while the CLI docs say 5 minutes. Pass timeout explicitly, or your forks disappear mid-demo.
  • Forking briefly pauses the original to snapshot it, so its open streams drop. Plan to reconnect.
  • Forking a paused sandbox currently returns HTTP 409; support for it is still an open proposal in the runtime repo. Resume first.

E2B’s snapshot docs make a useful distinction: for a setup you repeat (same packages, same files every time), a template is faster than a snapshot, because templates are captured after a clean boot with compact memory. Snapshots and forks are for capturing a live state you can’t easily recreate.

Test 8: A custom template vs installing at runtime

python template/build.py   # one-off build, about a minute
python 08_template.py

Templates are E2B’s equivalent of a Docker image, built with a code-first builder (Dockerfiles also work). Here’s a small data-science template:

# template/template.py
from e2b import Template

template = (
    Template()
    .from_base_image()
    .pip_install(["pandas", "numpy", "matplotlib"])
    .set_envs({"MPLBACKEND": "Agg"})
)

# template/build.py
Template.build(template, "my2cents-datasci", cpu_count=2, memory_mb=2048,
               on_build_logs=default_build_logger())

The build took 53 seconds. CPU and RAM are set at build time, per template; you can’t resize a sandbox in Sandbox.create(). Then I compared time-to-first-import pandas on the base template (installing at runtime) against the custom template:

=== Base template + pip install at runtime ===
[  0.42s] create
[ 11.86s] pip install pandas numpy matplotlib
[  1.29s] first import
  time to first import: 13.58s

=== my2cents-datasci (run 1) ===
[  0.79s] create
[  7.15s] first import
[  1.06s] second import
  time to first import: 7.94s

=== my2cents-datasci (run 2) ===
[  0.56s] create
[  8.50s] first import
[  0.57s] second import
  time to first import: 9.06s

speedup vs base: run 1 1.7x, run 2 1.5x

Only about a 1.6x speedup, and it all comes from skipping the 11.9 s pip install. The first import on the template ate most of the gain: 7–8.5 s, against 0.6–1.1 s for a second import in the same sandbox, and 1.3 s right after a fresh install on the base template. My guess is that the template’s filesystem is loaded on demand, so the first read of pandas’ files pays to fetch them, and the base-template run didn’t because pip had just written those files locally. Treat that as a hypothesis. If cold-path latency matters to you, a set_start_cmd that pre-imports your heavy libraries during the build should move that cost into the snapshot. The lakeFS team reported sandbox readiness dropping from about 19 s to about 1 s after baking their dependencies into a template, which is the same idea at a larger scale.

That first-import cost also isn’t stable. When I first ran this test three weeks earlier on SDK 2.52.1, the first import on the template took only 3–4 s, for a 3.7x speedup. On 2.59.0, with a freshly rebuilt template, it took 6.4–8.5 s across four runs (1.5–2.0x), while the runtime-install path barely moved. I can’t tell whether the SDK, the rebuild or E2B’s platform is responsible, which is one more reason to pre-import during the build.

The console’s Templates page lists your private templates next to E2B’s public ones, which now include ready-made images for coding agents (claude, codex, opencode, amp and others):

E2B console Templates list showing the private my2cents-datasci template marked INTERNAL, followed by public E2B templates including pi, opencode, openclaw, k3s, grok, droid, codex, claude, amp, desktop, omarchy, code-interpreter-v1 and base

Results summary

OperationMeasured (SDK 2.59.0, Hobby, us-west-1, from my laptop)
Sandbox.create() (base, sequential)median 0.33 s (0.23–0.83 s, n=5)
pause() (small-memory sandbox)0.69–0.73 s
connect() resume0.23–0.25 s
Auto-resume via files.read()0.25 s
Auto-resume via HTTP to preview URL0.33 s
Sandbox.connect(id) from a second process (paused → running)0.76 s
fork(count=3)0.57 s
create_snapshot() / create from snapshot0.44 s / 0.38 s
Template build (pandas, numpy, matplotlib)53 s
Time to first import pandas: runtime install vs template13.58 s vs 7.94–9.06 s

These are single-region measurements from a laptop on a home connection, with sandboxes that used very little memory. Use them as a sense of scale, not as a benchmark.

Pricing

E2B bills per second while a sandbox is running, for the vCPU and RAM it’s allocated, not what it uses (billing docs):

ResourceRatePer hour
vCPU$0.000014 / vCPU-second$0.0504
RAM$0.0000045 / GiB-second$0.0162
Default sandbox (2 vCPU, 512 MiB)≈ $0.109
Paused or killed sandboxfree$0
PlanMonthlyIncludedMax continuous runtimeConcurrent sandboxes
Hobby$0$100 one-time credit1 hour20
Pro$150 + usageNo extra credit24 hours100 (600 for +$500/mo, 1,100 for +$1,000/mo)
Enterprisefrom $3,000CustomCustomCustom

The Pro fee buys limits, not compute: longer sessions, more concurrency, higher CPU and RAM ceilings per template (Hobby already allows up to 8 vCPU and 8 GiB), and more create calls per second (5/s vs 1/s on Hobby). Template and snapshot storage are free today, though the docs say pricing for template storage “may be introduced in the future”.

For scale: my original test run on SDK 2.52.1 (29 started or resumed sandboxes, one template build) came to $0.01, according to the console’s Usage page:

E2B console Usage page for October 2026: 29 started and resumed sandboxes, $0.01 usage cost, 0.09 vCPU hours and 0.03 RAM hours over the range

Cleanup

Paused sandboxes never expire, aren’t billed, and don’t count toward concurrency, which makes them easy to forget. The E2B cookbook even added a cleanup script on 2026-09-30 that support reportedly hands to customers asking for a paused-sandbox TTL. The companion repo’s last script kills everything carrying the demo tag:

from e2b import Sandbox, SandboxQuery, SandboxState

paginator = Sandbox.list(
    query=SandboxQuery(metadata={"demo": "my2cents-lifecycle"},
                       state=[SandboxState.RUNNING, SandboxState.PAUSED]),
    limit=100,
)
while True:
    for info in paginator.next_items():
        Sandbox.kill(info.sandbox_id)
    if not paginator.has_next:
        break

for s in Sandbox.list_snapshots(name="my2cents-lifecycle-demo").next_items():
    Sandbox.delete_snapshot(s.snapshot_id)
=== Sandboxes tagged {'demo': 'my2cents-lifecycle'} ===
  kill imkgfgv7dnriwz7lg2y12 (paused): True
  kill id1f7cwktljzuzg2qar77 (running): True
  kill ijbh3kjusqwblykk3hp8o (running): True
killed 3

=== Snapshots named my2cents-* ===
deleted 0

A follow-up Sandbox.list() across running and paused states returned zero sandboxes, and Sandbox.list_snapshots() returned zero snapshots (the demo snapshot from Test 7 had already been deleted there).

The CLI equivalent, which also needs the explicit state filter:

npx @e2b/cli sandbox kill --all --state=running,paused --metadata=demo=my2cents-lifecycle

Finally, set a ceiling. The console’s Limits page has a hard usage limit, which blocks all API requests once you reach it in a billing period, plus a softer usage alert. Automatic emails go out at 50%, 80%, 90% and 100%:

E2B console Limits page with an unset usage limit field, noting that all API requests are blocked after reaching the limit and that alerts are sent at 50, 80, 90 and 100 percent, plus an unset usage alert field

On Hobby, running out of credits blocks the account, so a usage limit is mostly about Pro. I’d set one on day one anyway.

Comments