quarta-feira, 4 de novembro de 2026 Edição nº 228
Buscar
Avaliação · Infraestrutura · 28 min de leitura

E2B Sandboxes: Mão na Massa com o Ciclo de Vida Completo do Sandbox

Por Fabio Douek

Ir para seção
Explica (TLDR) como se eu fosse...
O que é isso?

Imagine uma caixa de brinquedos mágica que você pode emprestar para o seu robô. O robô pode construir qualquer coisa dentro dela, fazer bagunça, até quebrar coisas, e nada disso encosta no seu quarto de verdade. Quando a brincadeira acaba, a caixa pode sumir, ou pode congelar exatamente como está até você voltar.

A parte do congelamento é o truque esperto. Você pode parar a caixa no meio de um jogo, sair por dias, e quando abrir de novo os blocos ainda estão no meio da torre. Você pode até fazer cópias da caixa para que três robôs testem três ideias diferentes ao mesmo tempo.

A E2B é um ambiente de execução hospedado: o código que sua aplicação ou o AI agent dela gera roda em uma máquina virtual isolada na infraestrutura da E2B, cobrada por segundo, com o runtime open source do fornecedor disponível caso a organização prefira hospedar por conta própria.

Os pontos de diligência são ciclo de vida e retenção. Por padrão, um sandbox é destruído cinco minutos após a criação, mas sandboxes pausados são retidos indefinidamente, sem expiração automática, então os dados persistem até alguém apagá-los. Defina quem é responsável pela limpeza, configure um teto de gastos no console e trate o estado pausado como dado armazenado sob a sua política de retenção.

A condição que isso trata é a execução insegura de código escrito por máquina em infraestrutura com a qual você se importa. A intervenção é uma máquina virtual Firecracker descartável por tarefa, e nos meus testes ela iniciou em cerca de um terço de segundo e voltou da pausa em menos de um.

A evidência é boa para o mecanismo central: memória, arquivos e processos em execução sobreviveram intactos a três ciclos de pausa. Efeitos colaterais conhecidos ficam na mesma área, com bug reports em aberto e incidentes recentes do serviço envolvendo pause e resume. Bons candidatos são produtos de agents que precisam de estado por usuário; maus candidatos são workloads de GPU, que ela não suporta.

Repare em quanta preocupação silenciosa desaparece quando o código que um agent escreve só consegue quebrar uma caixa que foi feita para ser quebrada. Os engenheiros param de vigiar cada comando gerado, e a conversa muda de "deixamos o agent agir?" para "o que ele deve fazer em seguida?".

A nova tensão é sobre esquecimento. Pausar é tão barato e tão permanente que os sandboxes se acumulam sem ninguém ver, e os padrões destroem o trabalho silenciosamente após cinco minutos, a menos que alguém tenha escolhido outra coisa. O hábito saudável é nomear quem arruma a casa, e transformar isso num acordo compartilhado e explícito em vez de uma reflexão tardia.

Pense na E2B como um pedal de looper para o trabalho do seu agent. Você aperta gravar, o sandbox toca, e em qualquer compasso você pode congelar o loop exatamente onde está, sair, e voltar depois no mesmo tempo.

O fork é o overdub: um take vira três takes paralelos, cada um livre para improvisar a partir da mesma frase inicial. O andamento é rápido, com starts e resumes abaixo de um segundo nas minhas sessões, mas os padrões importam. Deixe o timeout como está e o loop simplesmente para depois de cinco minutos e desaparece.

O posicionamento é "um computador para o seu agent, com um botão de pausa." O time to value é de minutos: uma instalação, uma API key, e o primeiro sandbox inicia em bem menos de um segundo. O crédito único de $100 do plano gratuito cobriu todo este passo a passo por cerca de um centavo.

A história diferenciada é o estado. Os concorrentes também iniciam rápido, mas poucos mantêm memória e processos em execução durante uma pausa indefinidamente, ou clonam um sandbox ativo em cópias. Esse é o gancho para produtos que precisam de workspaces por usuário aos quais seus agents possam voltar dias depois.

GitHub Veja o código-fonte aqui: e2b-sandbox-lifecycle

Visão Geral

A E2B roda código gerado por IA em sandboxes na nuvem. Cada sandbox é uma microVM Firecracker com seu próprio kernel, filesystem e rede, que você cria, controla e destrói a partir de um SDK Python ou JavaScript. Para muitos times, ela é a resposta padrão para “onde o meu agent roda o código que ele escreve”, e o runtime é open source sob Apache-2.0 caso você prefira hospedá-lo por conta própria.

Um sandbox da E2B pode ser pausado com sua memória, arquivos e processos em execução intactos, mantido pausado indefinidamente sem custo de computação, acordado automaticamente pela próxima chamada de API ou requisição HTTP, e “forkado” em cópias que herdam o estado ativo.

Este post, parte 1 de 2, acompanha um sandbox por toda a sua vida, de Sandbox.create() até a limpeza, com um script executável por etapa. Rodei tudo contra o SDK 2.59.0 (lançado em 2026-10-22) no plano gratuito Hobby em us-west-1, usando Python, e todos os tempos abaixo vêm das minhas execuções. Escrevi este post originalmente sobre a 2.52.1; os scripts rodaram sem alterações na 2.59.0, e os números se mantiveram, exceto no teste de template (veja o Teste 8). A coisa mais importante a saber antes de começar: o timeout padrão é de 5 minutos e a ação padrão no timeout é kill. Se você não optar pela pausa, seu sandbox e tudo o que há nele some cinco minutos após a criação. A Parte 2 trata de credenciais: como dar API keys a um agent no sandbox sem colocar as chaves dentro dele.

Arquitetura

Existem dois planos. A E2B API cuida das operações de ciclo de vida (create, connect, list, pause, fork, snapshot, kill). Dentro de cada VM, um pequeno daemon chamado envd atende comandos, o filesystem e os streams de processos, e um proxy expõe qualquer porta como https://<port>-<sandbox-id>.e2b.app.

TOPOLOGIA Sua aplicação e2b SDK (Py / JS) E2B API create · connect · list pause · fork · snapshot · kill Firecracker microVM kernel próprio · 2 vCPU · 512 MiB envd comandos · arquivos · streams seus processos ex.: http.server :3000 ciclo de vida exec / arquivos URL pública https://3000-<id>.e2b.app ESTADOS DO CICLO DE VIDA Running Paused Killed novos sandboxes mesmo estado pause() ou timeout (on_timeout=pause) connect() / auto-resume kill() / timeout (padrão) kill() fork() / snapshot

Três coisas nessa imagem importam para o resto do post:

Estado Cobrado? Conta para a concorrência? Expira?
Running Sim, por segundo (vCPU + RAM) Sim No seu timeout (kill ou pause) e no limite de runtime do plano (1 h no Hobby, 24 h no Pro)
Paused Não Não Nunca. A E2B não apaga sandboxes pausados por conta própria
Killed Não Não Some para sempre

Não existe uma chamada resume() separada no SDK: você retoma chamando connect(). E pausar zera o relógio de runtime contínuo, então um sandbox que pausa e retoma pode ultrapassar o limite de 1 h / 24 h indefinidamente.

Setup

Passo 1: Conta e API key

Cadastre-se em e2b.dev e crie uma chave em API Keys no console. O plano Hobby é gratuito e vem com $100 de crédito de uso único, o que equivale a aproximadamente 900 horas de um sandbox padrão. Todo este passo a passo me custou cerca de um centavo (mais sobre isso em Preços).

Passo 2: Instalar o SDK e o código de apoio

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

O SDK Python exige Python 3.11+ (a 2.56.0 removeu o suporte ao 3.10). Fixei e2b==2.59.0 de propósito: a E2B lança releases do SDK a cada poucos dias (cerca de 40 releases do SDK JS entre meados de junho e o início de outubro de 2026), e versões minor já mudaram padrões. Por exemplo, a 2.51.0 moveu create e connect para novos endpoints v2 e deixou de definir padrões no lado do cliente para timeout e recursos. Atualize a versão fixada de forma deliberada.

A CLI é opcional, mas útil para inspeção e limpeza: npx @e2b/cli sandbox list (ou npm i -g @e2b/cli / brew install e2b).

Passo 3: Marcar tudo com tags

Todo script marca seus sandboxes com metadata, para que a limpeza no final só toque no que a demo criou:

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

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

Recomendo o mesmo hábito em produção: marque por usuário, ambiente e job, porque a metadata é como você encontra os sandboxes de novo depois.

Testes

Teste 1: Criar um sandbox e inspecionar os padrões

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

Isso confirma os padrões documentados: 2 vCPU, 512 MiB, timeout de 300 segundos e kill no timeout. Cinco creates sequenciais tiveram mediana de 0.33 s, medidos a partir do meu laptop, de ponta a ponta, incluindo o round trip do SDK.

Duas outras observações. free -m dentro da VM reporta 473 MiB no total, um pouco abaixo dos 512 nominais. E o filesystem raiz mostrou 22 GB (22.8 GB no console), embora a documentação de cobrança liste 10 GiB de disco para o Hobby. Não sei dizer se isso é uma franquia temporária ou uma defasagem da documentação, então não planeje em cima disso.

O console mostra cada sandbox ao vivo, com gráficos de CPU, RAM e disco, o timeout restante e os botões Pause e Kill:

Página de detalhes de um sandbox no console da E2B, na aba Monitoring: status RUNNING, template base, cerca de 29 minutos restantes antes do timeout, 2 cores, 512 MB de memória, 22.8 GB de disco, com gráficos de CPU e RAM em torno de 50% por causa de um processo em busy loop, e os botões Pause e Kill

Teste 2: Comandos, arquivos e o timeout de comando

python 02_commands_files.py

Por padrão, os comandos são bloqueantes; eles podem fazer stream de stdout e stderr através de callbacks, ou rodar em background e te devolver um 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: ...

A pegadinha está nessa última linha. Os comandos têm seu próprio timeout, de 60 segundos por padrão, e ele mata o processo mesmo quando background=True. Um dev server ou um processo de agent de longa duração precisa de timeout=0. Duas notas menores sobre a API: o Python usa write_files() para múltiplos arquivos, enquanto o JavaScript sobrecarrega write(), e o código roda como user em /home/user por padrão. Um relato de produção da Codecloud acrescenta outra pegadinha: um commands.run() bloqueante também espera pelos processos filhos, então qualquer coisa que vire daemon deve ser iniciada com background=True.

O console tem um terminal no navegador para cada sandbox em execução, que é a forma mais rápida de fuçar durante o debug. Não espere encontrar o sandbox do script ali: 02_commands_files.py o mata na última linha. Para este screenshot, iniciei manualmente um sandbox separado com um processo queimando CPU e o mantive vivo (ele não faz parte do código de apoio):

Aba Terminal do console da E2B conectada a um sandbox em execução, mostrando a saída de ps aux com um busy loop em python3 a 99.9% de CPU, o daemon envd rodando como root e um segundo processo python ocupando 200 MB de memória

Repare no processo /usr/bin/envd rodando como root: é o daemon dentro da VM do diagrama de arquitetura. Um efeito colateral que notei: abrir a aba Terminal empurrou o timeout restante daquele sandbox de cerca de 24 minutos para cerca de 30. A conexão de terminal do console parece estender o timeout da mesma forma que um connect() do SDK faria, então um sandbox que você está observando no navegador pode viver mais do que o seu código pediu.

Teste 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

Esse é o comportamento que pega as pessoas desprevenidas. set_timeout() define a expiração para exatamente agora + N. connect(timeout=N) a define como max(expiração atual, agora + N), então um timeout curto no connect é ignorado silenciosamente. E quando o timer acaba com o ciclo de vida padrão, o sandbox não pausa: ele é apagado, com arquivos e tudo.

Atenção também às unidades. O Python recebe segundos (timeout=60), o JavaScript recebe milissegundos (timeoutMs: 60_000). Portar um snippet de um para o outro sem converter te dá um sandbox que morre depois de cerca de um segundo (o SDK JS arredonda para cima, para segundos inteiros), ou um que pede 16 horas e roda até o limite de 1 h ou 24 h do seu plano.

⚠️ Fixe a versão do seu SDK e escreva os valores de ciclo de vida exatamente como documentado. Segundo o histórico de releases do SDK, antes da 2.48.0 um erro de digitação como on_timeout="Pause" não era rejeitado, então o sandbox caía no padrão e era morto no timeout em vez de pausado.

Teste 4: Pause e resume, três ciclos

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

Essa é a feature que diferencia a E2B, então testei com mais rigor. O script inicia um processo Python cujo contador vive apenas na memória do processo (espelhado em um arquivo para que eu possa lê-lo), escreve um arquivo a cada ciclo, depois pausa, espera 10 segundos e retoma, três vezes. No final, ele se reconecta ao stream de stdout do processo.

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

Os três ciclos passaram:

  • A memória sobrevive. O contador foi de 5 para 6 em uma pausa de 10 segundos, não de 5 para 15 e nem de volta para 1. O processo foi congelado, não reiniciado.
  • Os processos sobrevivem. Mesmo PID do início ao fim.
  • Os arquivos sobrevivem. Todos os arquivos de ciclo estavam presentes após cada resume.
  • O pause levou 0.69–0.73 s, o resume 0.23–0.25 s. A documentação cita cerca de 4 segundos por GiB de RAM para o pause e cerca de 1 s para o resume. Meus sandboxes usavam só algumas dezenas de MiB, o que provavelmente explica por que fiquei bem abaixo disso. Conte com o número documentado se seus sandboxes guardam memória de verdade: um sandbox de 8 GiB pode levar cerca de 32 segundos para pausar.
  • A reconexão ao stdout funcionou. Testei isso especificamente porque há reports em aberto de alta prioridade sobre streams de stdout travando quando você se reconecta após um resume (E2B#1587, entre outros). Nas minhas execuções na 2.52.1, e de novo na 2.59.0, o stream entregou todas as linhas. São duas execuções, não uma prova de que o bug foi corrigido.

Pause e resume é a área com mais issues em aberto e mais incidentes recentes do serviço (a pesquisa que fiz na comunidade contou cinco incidentes de pause, resume ou connect no histórico da status page entre julho e o início de outubro de 2026, o mais recente em 2026-10-02). Um bug de persistência do filesystem em resumes repetidos (E2B#884) foi fechado como corrigido e depois reportado de novo por um usuário. Nada disso apareceu nos meus testes, mas se o estado pausado é o seu produto, inclua verificação: checksums ou um arquivo canário que você confere após cada resume.

No console, um sandbox pausado mostra Resume em vez de Pause, além de quando ele foi pausado e por quanto tempo rodou:

Página de detalhes no console da E2B de um sandbox pausado criado a partir do template my2cents-datasci: status PAUSED, pausado às 12:05:16 AM UTC, rodou por 1 segundo, 2 cores, 2,048 MB de memória, 12.8 GB de disco, com os botões Resume e Kill

A aba Events registra cada transição de ciclo de vida. São os mesmos eventos que você pode assinar com lifecycle webhooks, que trazem tempo de execução e recursos caso você queira fazer sua própria contabilidade de custos:

Aba Events do console da E2B para um sandbox pausado listando dois eventos de ciclo de vida: CREATED às 00:05:14.91 com a metadata do sandbox na coluna de dados, e PAUSED às 00:05:16.24 com dados de tempo de execução

Se você não precisa da memória restaurada, sbx.pause(mode="filesystem") tira um snapshot só do filesystem (ele substituiu keep_memory=False no SDK 2.55.0): os arquivos sobrevivem, os processos não, e o próximo resume é um reboot (docs).

Teste 5: Auto-pause no timeout e auto-resume com atividade

python 05_auto_resume.py

Pause manual serve para scripts. Para um produto, você quer que o sandbox se pause sozinho quando ocioso e acorde quando algo encostar nele. São duas flags de ciclo de vida:

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

Os dois caminhos de despertar funcionaram. Um files.read() contra o sandbox pausado retornou o conteúdo em 0.25 s sem nenhuma chamada explícita de resume. Um HTTP GET na preview URL pública acordou o sandbox, restaurou o http.server que ainda estava rodando e retornou HTTP 200 em 0.33 s. Esse segundo caminho é o interessante: um ambiente de preview ou o web app de um agent pode dormir com custo zero de computação e acordar na primeira requisição.

Dois detalhes da execução. Fazer polling de Sandbox.get_info() a cada 5 segundos não o acordou, então checagens de status são seguras. Depois de acordar, o sandbox ganha pelo menos 5 minutos de runtime (ou seu timeout original, se for maior) antes de se auto-pausar de novo, e a configuração de ciclo de vida persiste entre os ciclos. auto_resume exige o pause que preserva memória; não dá para combiná-lo com mode="filesystem".

Teste 6: List, filtro e connect a partir de outro processo

python 06_list_connect.py

Em um sistema real, o processo que cria um sandbox raramente é o mesmo que o usa depois. Metadata mais Sandbox.list() é como você o encontra, e Sandbox.connect(id) é como você o retoma:

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

Filtrar por metadata e estado funciona como documentado, e o paginator limita as páginas a 100. Um segundo processo, sem relação com o primeiro, se conectou ao sandbox pausado pelo ID em 0.76 s, o que o retomou, e escreveu um arquivo que o processo pai depois leu. Dois padrões diferem entre as ferramentas: Sandbox.list() no SDK retorna sandboxes em execução e pausados, enquanto e2b sandbox list na CLI mostra apenas os em execução, a menos que você passe --state running,paused. É um jeito fácil de se convencer de que não tem sandboxes sobrando quando tem.

A lista de sandboxes do console mostra o mesmo quadro. Repare no contador no canto superior direito: três sandboxes estão listados, mas só dois contam como concorrentes, porque sandboxes pausados não contam para o limite de concorrência do plano (20 no Hobby):

Lista de sandboxes no console da E2B com três sandboxes: um PAUSED no template my2cents-datasci com 2.0 GB de memória, e dois RUNNING no template base, um deles a 51% de CPU; o cabeçalho mostra 2 sandboxes concorrentes

A metadata fica visível por sandbox, o que ajuda quando você está rastreando o sandbox de um usuário a partir de um ticket de suporte:

Popover de metadata de sandbox no console da E2B mostrando JSON com demo my2cents-lifecycle, test showcase e userId bob

Teste 7: Fork e snapshot

python 07_fork_snapshot.py

O fork foi lançado em julho de 2026 (SDK 2.34.0 para Python), com um comando na CLI vindo em setembro. Um fork é um novo sandbox que parte dos arquivos, processos e memória do original. Snapshots são a versão persistente: capture uma vez, crie vários sandboxes a partir dele depois.

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

Três forks em 0.57 s, cada um com o arquivo do pai e o processo contador do pai ainda rodando com o mesmo PID. As escritas nos forks não vazaram de volta para o original. Um snapshot levou 0.44 s e um novo sandbox a partir dele, 0.38 s. Essa é a primitiva para “testar três abordagens em paralelo a partir do mesmo estado” em workflows de agents, ou para fazer checkpoint antes de um passo arriscado.

O que saber antes de depender disso:

  • fork() retorna uma lista que mistura objetos Sandbox e exceções em falhas parciais em vez de lançar erro, então filtre os resultados como acima.
  • O SDK limita uma chamada de fork a 20 cópias (a API permite até 100), e a quantidade precisa caber no seu limite de concorrência.
  • Forks têm timeout padrão de 15 segundos no SDK e na API, enquanto a documentação da CLI fala em 5 minutos. Passe timeout explicitamente, ou seus forks desaparecem no meio da demo.
  • O fork pausa brevemente o original para tirar o snapshot, então os streams abertos dele caem. Planeje reconectar.
  • Fazer fork de um sandbox pausado atualmente retorna HTTP 409; o suporte a isso ainda é uma proposta em aberto no repositório do runtime. Retome antes.

A documentação de snapshots da E2B faz uma distinção útil: para um setup que você repete (os mesmos packages, os mesmos arquivos toda vez), um template é mais rápido que um snapshot, porque templates são capturados após um boot limpo com memória compacta. Snapshots e forks servem para capturar um estado ativo que você não consegue recriar facilmente.

Teste 8: Um template customizado vs instalar em runtime

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

Templates são o equivalente da E2B a uma imagem Docker, construídos com um builder code-first (Dockerfiles também funcionam). Aqui está um pequeno template de data science:

# 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())

O build levou 53 segundos. CPU e RAM são definidos em tempo de build, por template; não dá para redimensionar um sandbox em Sandbox.create(). Depois comparei o tempo até o primeiro import pandas no template base (instalando em runtime) contra o template customizado:

=== 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

Só cerca de 1.6x de ganho, e tudo vem de pular os 11.9 s do pip install. O primeiro import no template comeu a maior parte do ganho: 7–8.5 s, contra 0.6–1.1 s de um segundo import no mesmo sandbox, e 1.3 s logo após uma instalação nova no template base. Meu palpite é que o filesystem do template é carregado sob demanda, então a primeira leitura dos arquivos do pandas paga para buscá-los, e a execução no template base não pagou porque o pip tinha acabado de escrever esses arquivos localmente. Trate isso como hipótese. Se a latência do caminho a frio importa para você, um set_start_cmd que pré-importa suas libraries pesadas durante o build deve mover esse custo para dentro do snapshot. O time do lakeFS relatou que o tempo até o sandbox ficar pronto caiu de cerca de 19 s para cerca de 1 s depois de embutir as dependências em um template, que é a mesma ideia em escala maior.

Esse custo do primeiro import também não é estável. Quando rodei este teste pela primeira vez, três semanas antes, no SDK 2.52.1, o primeiro import no template levou só 3–4 s, um ganho de 3.7x. Na 2.59.0, com um template recém-reconstruído, levou 6.4–8.5 s em quatro execuções (1.5–2.0x), enquanto o caminho de instalação em runtime mal se mexeu. Não sei dizer se o responsável é o SDK, o rebuild ou a plataforma da E2B, o que é mais um motivo para pré-importar durante o build.

A página Templates do console lista seus templates privados ao lado dos públicos da E2B, que agora incluem imagens prontas para coding agents (claude, codex, opencode, amp e outros):

Lista de Templates no console da E2B mostrando o template privado my2cents-datasci marcado como INTERNAL, seguido dos templates públicos da E2B, incluindo pi, opencode, openclaw, k3s, grok, droid, codex, claude, amp, desktop, omarchy, code-interpreter-v1 e base

Resumo dos resultados

OperaçãoMedido (SDK 2.59.0, Hobby, us-west-1, a partir do meu laptop)
Sandbox.create() (base, sequencial)mediana 0.33 s (0.23–0.83 s, n=5)
pause() (sandbox com pouca memória)0.69–0.73 s
Resume com connect()0.23–0.25 s
Auto-resume via files.read()0.25 s
Auto-resume via HTTP na preview URL0.33 s
Sandbox.connect(id) a partir de um segundo processo (paused → running)0.76 s
fork(count=3)0.57 s
create_snapshot() / create a partir do snapshot0.44 s / 0.38 s
Build do template (pandas, numpy, matplotlib)53 s
Tempo até o primeiro import pandas: instalação em runtime vs template13.58 s vs 7.94–9.06 s

São medições em uma única região, a partir de um laptop numa conexão residencial, com sandboxes que usavam muito pouca memória. Use-as como noção de escala, não como benchmark.

Preços

A E2B cobra por segundo enquanto um sandbox está em execução, pela vCPU e RAM alocadas, não pelo que ele usa (documentação de cobrança):

RecursoTarifaPor hora
vCPU$0.000014 / vCPU-segundo$0.0504
RAM$0.0000045 / GiB-segundo$0.0162
Sandbox padrão (2 vCPU, 512 MiB)≈ $0.109
Sandbox pausado ou mortográtis$0
PlanoMensalInclusoRuntime contínuo máximoSandboxes concorrentes
Hobby$0$100 de crédito único1 hora20
Pro$150 + usoSem crédito extra24 horas100 (600 por +$500/mês, 1,100 por +$1,000/mês)
Enterprisea partir de $3,000PersonalizadoPersonalizadoPersonalizado

A taxa do Pro compra limites, não computação: sessões mais longas, mais concorrência, tetos maiores de CPU e RAM por template (o Hobby já permite até 8 vCPU e 8 GiB) e mais chamadas de create por segundo (5/s vs 1/s no Hobby). O armazenamento de templates e snapshots é gratuito hoje, embora a documentação diga que a cobrança pelo armazenamento de templates “pode ser introduzida no futuro”.

Para dar uma escala: minha execução de testes original no SDK 2.52.1 (29 sandboxes iniciados ou retomados, um build de template) saiu por $0.01, segundo a página Usage do console:

Página Usage do console da E2B para outubro de 2026: 29 sandboxes iniciados e retomados, $0.01 de custo de uso, 0.09 horas de vCPU e 0.03 horas de RAM no período

Limpeza

Sandboxes pausados nunca expiram, não são cobrados e não contam para a concorrência, o que os torna fáceis de esquecer. O cookbook da E2B chegou a adicionar um script de limpeza em 2026-09-30 que o suporte, segundo relatos, entrega aos clientes que pedem um TTL para sandboxes pausados. O último script do repositório de apoio mata tudo o que carrega a tag da demo:

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

Um Sandbox.list() posterior abrangendo os estados running e paused retornou zero sandboxes, e Sandbox.list_snapshots() retornou zero snapshots (o snapshot da demo do Teste 7 já tinha sido apagado lá).

O equivalente na CLI, que também precisa do filtro de estado explícito:

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

Por fim, defina um teto. A página Limits do console tem um limite rígido de uso, que bloqueia todas as requisições de API quando você o atinge em um período de cobrança, além de um alerta de uso mais brando. E-mails automáticos são enviados em 50%, 80%, 90% e 100%:

Página Limits do console da E2B com o campo de limite de uso não definido, informando que todas as requisições de API são bloqueadas após atingir o limite e que alertas são enviados em 50, 80, 90 e 100 por cento, além de um campo de alerta de uso não definido

No Hobby, ficar sem créditos bloqueia a conta, então um limite de uso importa mais no Pro. Eu definiria um no primeiro dia mesmo assim.

Comments