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

Daytona Sandboxes: Hands-On 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 que você pode emprestar para o seu robô. O robô pode construir qualquer coisa dentro dela e fazer bagunça, e nada disso encosta no seu quarto de verdade. Quando a brincadeira acaba, você pode fechar a tampa, e os bloquinhos continuam lá na próxima vez, mas o robô tem que começar o jogo dele de novo.

A parte complicada é que a tampa fecha sozinha se ninguém de fora falar com a caixa por um tempo, mesmo que o robô ainda esteja ocupado lá dentro. E uma caixa fechada nunca abre sozinha: um adulto tem que abri-la de novo antes que alguém possa brincar.

O Daytona é um ambiente de execução hospedado: o código que uma aplicação ou o seu agente de IA gera roda em um container isolado na infraestrutura do Daytona, cobrado por segundo. O código-fonte da plataforma agora é fechado; apenas os SDKs de cliente são open source.

Os pontos de diligência são retenção e egress. Sandboxes parados mantêm seus arquivos, e são cobrados pelo disco, até serem arquivados após sete dias ou deletados, porque a deleção automática vem desligada por padrão. Contas novas começam em um tier com acesso restrito à internet e quotas apertadas. Confirme o tier, defina quem é responsável pelo cleanup e configure um time-to-live em todo sandbox.

A condição tratada aqui é a execução insegura de código escrito por máquina em infraestrutura que importa para você. A intervenção é um container descartável por tarefa. Nos meus testes, ele subiu em cerca de um segundo e fez stop e start de novo em um a dois segundos em cada direção.

A evidência é boa para arquivos, que sobreviveram a todo stop, start e archive. Os efeitos colaterais conhecidos são processos perdidos no stop, um timer de inatividade que ignora o trabalho rodando dentro do sandbox e nenhum wake-up automático. O pause que preserva a memória só existe em uma classe de máquina virtual. Depois que o suporte habilitou isso na minha conta, pause e resume levaram menos de um segundo cada, e um processo em execução continuou de onde parou.

Repare no alívio quando o código que um agent escreve só consegue quebrar uma caixa feita para ser quebrada. A conversa deixa de ser se o agent deve agir e passa a ser o que ele deve fazer em seguida.

O novo atrito é mais silencioso. Um sandbox pode parar sozinho no meio de um job longo porque ninguém de fora encostou nele, e ele não acorda até que alguém perceba. Nomear quem é dono dos timers, e quem arruma as caixas paradas que ainda custam dinheiro, evita que isso vire troca de culpas mais tarde.

Pense em um sandbox do Daytona como uma sala de ensaio que você reserva por segundo. Você pode deixar o equipamento montado entre as sessões, e ele continua lá quando você volta, mas a banda tem que contar a entrada da música de novo, porque nada que estava tocando continua tocando.

Fique de olho nas regras da casa sobre o andamento. A sala se tranca sozinha depois de quinze minutos sem ninguém bater na porta, mesmo no meio da música, e ninguém destranca até você pedir. A sala já vem com um rack completo de instrumentos, então a maioria das sessões começa sem desempacotar nada.

O posicionamento é "um workspace de longa duração para o seu agent". O time to value é de minutos: uma instalação, uma API key, um sandbox em cerca de um segundo e US$ 200 de crédito grátis, bem mais do que este passo a passo precisa.

A história é amplitude. O Daytona traz muitos controles de ciclo de vida, uma imagem padrão já carregada com ferramentas para agents e preço por segundo sem taxa de plano. As ressalvas que os compradores vão levantar são o bloqueio por tier em contas novas e a falta de wake-up automático para sandboxes inativos.

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

Visão Geral

O Daytona roda código gerado por IA em sandboxes na nuvem que você cria, controla e deleta a partir de SDKs em Python, TypeScript, Go, Ruby e Java. Ele começou como um gerenciador open source de ambientes de desenvolvimento e se reposicionou em torno de sandboxes para agents. Em junho de 2026, a plataforma passou para um codebase privado.

O que é preciso entender antes de qualquer coisa é que o ciclo de vida de um sandbox do Daytona depende da sua classe. A classe padrão é um container: ele pode fazer stop, start, archive e snapshot do seu filesystem, mas não pode fazer pause com a memória, não pode fazer fork, e nada o acorda automaticamente. A classe Linux VM adiciona pause com preservação de memória e fork. Você escolhe a classe pelo snapshot a partir do qual cria o sandbox, não por uma flag.

Este post, parte 1 de 2, acompanha um sandbox padrão pela vida inteira, de daytona.create() até o cleanup, com um script executável por etapa. Eu rodei tudo com o SDK 0.226.1 (lançado em 2026-10-23) a partir de Python, na região us. Minha organização começou no Tier 1 e passou para o Tier 2 no meio do caminho; nenhum dos números abaixo depende do tier. Todos os tempos são das minhas execuções. A classe Linux VM não estava disponível para a minha organização no início, em nenhum dos dois tiers. Depois de um pedido ao suporte, o Daytona habilitou uma quota de VM em uma região separada, us-west-3, e o Teste 5 cobre pause e fork lá. A Parte 2 cobre credenciais: dar API keys a um agent dentro do sandbox sem colocar as keys lá dentro.

Arquitetura

A Daytona API cuida do ciclo de vida (create, start, stop, archive, snapshot, delete). Dentro de cada sandbox, um daemon toolbox atende execução de processos, arquivos e git. Um proxy expõe qualquer porta como https://<port>-<sandbox-id>.daytonaproxy01.net, protegida por um token por padrão.

TOPOLOGIA Seu app daytona SDK Daytona API create · start · stop · archive snapshot · list · delete Sandbox container padrão · 1 vCPU · 1 GiB · 3 GiB toolbox exec · sessions · files · git seus processos ex.: http.server :3000 (perdidos no stop) ciclo de vida exec / files Preview URL + token 3000-<id>.daytonaproxy01.net novo token a cada start CICLO DE VIDA DO CONTAINER Started Stopped Archived Deleted novo(s) sandbox(es) de um snapshot stop() / auto-stop (15 min, ignora processos) só start(), sem auto-wake archive() / auto-archive start() delete() / ttl_minutes delete() / auto-delete / ttl create_snapshot() Só na classe Linux VM: Paused (memória mantida), fork(), auto-pause. Em um container, retornam HTTP 422.

Quanto custa cada estado, segundo a documentação de billing:

Estado Cobrado por Conta na quota de disco? Expira?
Started vCPU + RAM + disco, por segundo Sim Auto-stop após 15 minutos de inatividade por padrão; sem limite de runtime na plataforma
Stopped Apenas disco Sim Arquivado automaticamente após 7 dias por padrão; auto-delete vem desligado por padrão
Archived Nada Não Nunca, a menos que você defina um TTL ou o delete
Deleted Nada (snapshots tirados dele continuam sendo cobrados) Não Some de vez

As features que dependem da classe, segundo a documentação de sandboxes:

FeatureContainer (padrão)Linux VM / Windows
Stop / start (arquivos mantidos, processos perdidos)simsim
Archive para object storagesimnão
Pause / resume com memórianãosim
Fork de um sandbox em execuçãonãosim
Snapshot do filesystemsimsim
Auto-pause por inatividadenãosim

Setup

Passo 1: Conta, API key e tier

Cadastre-se em app.daytona.io e crie uma key em API Keys. Organizações novas ganham US$ 200 de compute grátis (não utilizável em GPUs), bem mais do que este passo a passo precisa.

Verifique o seu tier antes de começar, porque ele decide o que você consegue testar. Uma organização nova fica no Tier 1: 10 vCPU, 10 GiB de RAM e 30 GiB de disco para a organização inteira, e acesso restrito à internet que um sandbox não consegue sobrescrever. Adicionar um cartão e US$ 25 de crédito leva você ao Tier 2 (100 vCPU / 200 GiB / 300 GiB, ainda com egress restrito). Internet aberta e allow lists por sandbox começam no Tier 3, que exige uma recarga de US$ 500 (documentação de limites).

O tier não decidiu se eu podia usar Linux VMs. O endpoint que lista as classes de sandbox que a sua organização pode criar não incluía linux-vm no Tier 1 nem no Tier 2, e o build de um snapshot de VM customizado falhou com “No runners are configured in region ‘us’ for sandbox class ‘linux-vm’”. Eu pedi ao suporte do Daytona, que habilitou uma quota de Linux VM. A mesma chamada então retornou uma nova entrada, em uma região própria:

curl -s -H "Authorization: Bearer $DAYTONA_API_KEY" \
  https://app.daytona.io/api/organizations/$ORG_ID/available-sandbox-classes
[{"regionId":"us","sandboxClass":"container","gpuAvailable":false},
 {"regionId":"us","sandboxClass":"windows","gpuAvailable":false},
 {"regionId":"us-west-3","sandboxClass":"linux-vm","gpuAvailable":false},
 {"regionId":"us-west-4","sandboxClass":"windows","gpuAvailable":false},
 {"regionId":"earth","sandboxClass":"container","gpuAvailable":true}]

As Linux VMs ficam em us-west-3, não em us, onde rodam os meus containers, então uma VM precisa de um client apontado para essa região (o Teste 5 mostra como). A página Limits do dashboard ganhou uma aba Linux VM, com uma quota própria de 10 vCPU, 10 GiB de memória e 30 GiB de storage:

Página Limits do dashboard do Daytona para uma organização Tier 2 na aba Linux VM: região us-west-3, recursos 0 de 10 vCPU, 0 de 10 GiB de memória e 0 de 30 GiB de storage, GPU em breve, limites por sandbox de 4 vCPU, 8 GiB de memória e 10 GiB de storage, e rate limits de 50.000 requests gerais, 600 criações de sandbox e 50.000 requests de ciclo de vida por minuto

O anúncio de julho do Daytona dizia que as VMs estavam “available to all Daytona organizations”, mas a minha precisou de um pedido ao suporte. Se VMs importam para você, rode essa chamada no primeiro dia e peça ao suporte caso linux-vm não apareça.

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

git clone https://github.com/fabiodouek/my2centsai-blog-samples.git
cd my2centsai-blog-samples/daytona-sandbox-lifecycle
cp .env.example .env            # paste DAYTONA_API_KEY=dtn_...
uv venv && uv pip install -e .  # installs daytona==0.226.1 + python-dotenv + requests
source .venv/bin/activate

Eu fixei daytona==0.226.1. O Daytona lança releases do SDK cerca de duas vezes por semana, e o changelog da documentação estava atrás do PyPI enquanto eu escrevia (a entrada mais recente era a 0.225.0). Atualize a versão fixada de forma deliberada.

Passo 3: Coloque labels em tudo e defina um TTL

Todo script coloca labels nos seus sandboxes e define um TTL de uma hora, então o cleanup só mexe no que a demo criou, e qualquer coisa que um script que travou deixar para trás é destruída dentro de uma hora:

# common.py
DEMO_LABELS = {"demo": "my2cents-lifecycle"}
SAFETY_TTL = 60  # minutes; destroys the sandbox in any state

def labels(**extra: str) -> dict:
    return {**DEMO_LABELS, **extra}

O TTL importa porque o auto-delete vem desligado por padrão, então um sandbox parado mantém o seu disco, e continua sendo cobrado por ele, até que algo o delete.

Testes

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

python 01_create.py      # optional arg: number of timed creates (default 5)
from daytona import CreateSandboxFromSnapshotParams, Daytona

daytona = Daytona()
sbx = daytona.create(CreateSandboxFromSnapshotParams(labels=labels(test="01"), ttl_minutes=SAFETY_TTL))
print(sbx.snapshot, sbx.sandbox_class, sbx.cpu, sbx.memory, sbx.disk, sbx.auto_stop_interval)
=== Create with defaults ========================================
[  2.17s] daytona.create()
snapshot   : daytonaio/sandbox:0.8.0 (container)
region     : us
state      : SandboxState.STARTED
cpu/mem/disk: 1 vCPU / 1 GiB / 3 GiB
auto-stop  : 15 min | auto-archive: 10080 min | auto-delete: -1 | auto-pause: 0
6.8.0-85-generic
daytona
64                        # nproc
100000 100000             # /sys/fs/cgroup/cpu.max  -> 1 vCPU
Mem:  773337  124150  420041  1418  242299  649187   # free -m
1073741824                # /sys/fs/cgroup/memory.max -> 1 GiB
overlay  3.0G  24K  3.0G  1% /

=== 5 sequential creates ========================================
[  0.74s] create #1
[  0.88s] create #2
[  1.23s] create #3
[  1.94s] create #4
[  0.87s] create #5
median 0.88s | min 0.74s | max 1.94s

Os padrões documentados se confirmam: um container a partir da imagem daytonaio/sandbox:0.8.0, 1 vCPU, 1 GiB, 3 GiB, auto-stop após 15 minutos, auto-archive após 7 dias (10.080 minutos) e auto-delete desabilitado (-1). Cinco creates sequenciais tiveram mediana de 0,88 s nesta execução e 1,28 s em uma anterior (0,74–2,72 s somando as duas), medidos do meu laptop incluindo o round trip da API. Isso está bem longe do “Sub 90ms” da homepage do Daytona, que mede algo mais estreito do que uma chamada de SDK a partir de um laptop.

O sandbox reporta o tamanho do host, não o seu próprio. Em um sandbox de 1 vCPU / 1 GiB, nproc disse 64 e free -m mostrou cerca de 755 GiB. Os limites reais continuam sendo aplicados, mas ferramentas que se dimensionam a partir desses números (uma JVM, uma ferramenta de build, um pool de workers) vão iniciar trabalho demais para o sandbox.

O dashboard mostra os mesmos padrões por sandbox, incluindo a classe, os timers de ciclo de vida e o horário de destruição do TTL:

Painel Sandbox Details do dashboard do Daytona para my2cents-shot-alice: estado Started, região us, classe Container, snapshot daytonaio/sandbox:0.8.0, preview access Private, recursos 1 vCPU, 1 GiB e 3 GiB, ciclo de vida com auto-stop 15m, auto-archive 7d, auto-delete Disabled, auto-destroy-at em 1h, e labels demo my2cents-lifecycle, test shot, userId alice

Teste 2: Comandos, arquivos, preview URLs e o timeout do exec

python 02_commands_files.py

Há duas formas de rodar coisas. process.exec() é uma chamada bloqueante que retorna um exit code e a saída. Sessions são shells de longa duração em que os comandos podem rodar de forma assíncrona, e você busca os logs depois:

r = sbx.process.exec("echo out; echo err 1>&2; exit 3")

sbx.process.create_session("srv")
sbx.process.execute_session_command(
    "srv", SessionExecuteRequest(command=f"cd {home}/notes && python3 -m http.server 3000", run_async=True)
)
link = sbx.get_preview_link(3000)
requests.get(f"{link.url}/hello.txt", headers={"x-daytona-preview-token": link.token})
=== Blocking command ============================================
exit=0
Python 3.14.4
daytona
/home/daytona

=== stdout, stderr and exit code ================================
exit=3 result='out\nerr\n'

=== Session command, streamed logs ==============================
tick 1
tick 2
tick 3

=== Files: upload, download, list ===============================
hello from my laptop
  file a.txt (1 B)
  file b.txt (1 B)
  file hello.txt (21 B)

=== Background server + preview URL =============================
preview URL: https://3000-f1a09699-7d86-4d8c-bb81-52232269a93e.daytonaproxy01.net
no token     -> HTTP 401
token header -> HTTP 200 'hello from my laptop'
token + skip -> HTTP 200 'hello from my laptop'
signed URL   -> HTTP 200 'hello from my laptop'

=== Exec timeout ================================================
  exit=0
[ 75.30s] exec('sleep 75') with no timeout
  DaytonaProcessExecutionTimeoutError: Failed to execute command: command execution timeout
[  3.31s] exec('sleep 10', timeout=3)

Três coisas nessa saída:

  • stdout e stderr voltam misturados. Para streams separados, rode o comando em uma session e leia os logs dela.
  • Preview URLs são privadas. Sem um token você recebe um 401. Envie-o no header x-daytona-preview-token, ou crie uma signed URL de curta duração com create_signed_preview_url().
  • Não há timeout padrão no exec. A documentação diz 10 segundos, mas sleep 75 rodou até o fim. Passe timeout= você mesmo em qualquer coisa que possa travar.

O código roda como o usuário daytona em /home/daytona, com sudo sem senha. A imagem padrão é Debian 13 com Python 3.14.4 e Node 25.9.0, e é pesada: ela traz Claude Code, Codex, OpenCode, OpenClaw, Bun e 291 packages Python, incluindo pandas, numpy, scikit-learn, matplotlib e torch. Isso muda o Teste 8.

Teste 3: Auto-stop, refresh_activity, TTL e auto-delete

python 03_timeouts.py   # about 4 minutes

O Daytona tem vários timers de ciclo de vida, e nem todos significam o que o nome sugere. O script cria quatro sandboxes e os observa:

# A: auto-stop after 1 minute, with "sleep 3600" running in a session
# B: same, but refresh_activity() every 15 seconds
a = daytona.create(CreateSandboxFromSnapshotParams(auto_stop_interval=1, labels=..., ttl_minutes=SAFETY_TTL))
# C: ttl_minutes=2, then stopped immediately
c = daytona.create(CreateSandboxFromSnapshotParams(ttl_minutes=2, labels=...)); c.stop()
# D: auto_delete_interval=0, then stopped
d = daytona.create(CreateSandboxFromSnapshotParams(auto_delete_interval=0, labels=..., ttl_minutes=SAFETY_TTL)); d.stop()
C b9294341-... auto_destroy_at=2026-10-29T03:27:33.653Z
C stopped: stopped
D after stop(): gone (DaytonaNotFoundError)

=== Poll every 15s for 3.5 min (B gets refresh_activity each tick)
t=    0s  A=started    B=started    C=stopped
t=   47s  A=started    B=started    C=stopped
t=   62s  A=stopping   B=started    C=stopped
t=   78s  A=stopped    B=started    C=stopped
t=  109s  A=stopped    B=started    C=stopped
t=  124s  A=stopped    B=started    C=destroying
t=  140s  A=stopped    B=started    C=gone (DaytonaNotFoundError)
t=  201s  A=stopped    B=started    C=gone (DaytonaNotFoundError)

(Linhas cortadas. Os tempos contam a partir do início do loop de polling, que começou alguns segundos depois de os sandboxes estarem configurados.)

  • O auto-stop ignora os seus processos. O sandbox A tinha sleep 3600 rodando e já estava em stopping no poll dos 62 segundos. A documentação é explícita sobre isso: só mudanças de ciclo de vida, tráfego de preview, SSH e chamadas à API do toolbox contam como atividade. Um agent rodando um job de 20 minutos dentro do sandbox, sem nada chamando de fora, é parado no padrão de 15 minutos, e o stop mata os seus processos (Teste 4).
  • refresh_activity() o mantém vivo. O sandbox B, idêntico exceto por um refresh a cada 15 segundos, ficou de pé o tempo todo. Para execuções longas de agents, chame isso a partir do seu orchestrator ou crie o sandbox com auto_stop_interval=0.
  • ttl_minutes é um relógio de parede que destrói em qualquer estado. O sandbox C estava parado, estava sendo destruído no poll dos 124 segundos e tinha sumido aos 140 segundos. Isso faz do TTL a rede de segurança certa, e eu definiria um em todo sandbox.
  • auto_delete_interval=0 significa “deletar assim que parar”. O D sumiu em até cinco segundos depois do stop(). Não significa “nunca deletar”: isso é -1, o padrão.

O Daytona oferece auto-stop, auto-archive, auto-delete, TTL e, em VMs, auto-pause: bastante controle, e bastante maneira de errar.

Teste 4: Stop e start, três ciclos

python 04_stop_start.py

Um container não consegue fazer pause mantendo a memória. O mais próximo disso é stop e start. O script escreve um arquivo, inicia um processo contador e um web server, depois faz stop e start do sandbox três vezes:

for cycle in range(1, 4):
    before = sbx.process.exec("cat /tmp/n").result.strip()
    sbx.stop()
    sbx.start()
    n1 = sbx.process.exec("cat /tmp/n").result.strip(); time.sleep(2)
    n2 = sbx.process.exec("cat /tmp/n").result.strip()
=== Cycle 1 =====================================================
[  0.80s] stop()
[  0.71s] start()
file   : written before the first stop
counter: 4 at stop, 4 -> 4 two seconds after start (process gone, /tmp kept)
preview token changed: True

=== Cycle 2 =====================================================
[  0.83s] stop()
[  0.70s] start()
file   : written before the first stop
counter: 3 at stop, 3 -> 3 two seconds after start (process gone, /tmp kept)
preview token changed: True

=== Cycle 3 =====================================================
[  0.82s] stop()
[  0.86s] start()
file   : written before the first stop
counter: 3 at stop, 3 -> 3 two seconds after start (process gone, /tmp kept)
preview token changed: True

=== Touching a stopped sandbox ==================================
exec on stopped: DaytonaBadRequestError: Failed to execute command: bad request: failed to resolve container IP after 3 attempts: no IP address found. Is the San
preview on stopped: HTTP 400 '{"statusCode":400,"message":"bad request: failed to resolve container IP after 3'
state after both: stopped

=== Archive and restore =========================================
[ 18.00s] archive()
state: archived
[  2.25s] start() from archived
file after restore: written before the first stop

=== pause() on a container ======================================
DaytonaUnprocessableEntityError: Failed to pause sandboxPausing is not supported for this sandbox
  • Stop e start são rápidos: 0,80–0,83 s para o stop e 0,70–0,86 s para o start nesta execução. Uma execução posterior foi mais lenta, com 0,9–1,4 s, depois de um primeiro ciclo que levou 4,9 s para o stop e 10,6 s para o start.
  • Arquivos sobrevivem, processos não. O arquivo estava lá depois de todo ciclo, incluindo /tmp, mas o contador parou de contar: o valor em /tmp/n era o que ele tinha escrito antes do stop. O web server também tinha sumido. Um stop é um restart do container, não um congelamento.
  • O preview token muda a cada start, então qualquer URL que você tenha distribuído para de funcionar depois de um restart.
  • Nada acorda um sandbox parado. Um exec() contra ele falhou com um 400 (failed to resolve container IP), e o mesmo aconteceu com uma request HTTP para a preview URL dele. O estado continuou stopped. Nada o retoma até você chamar start().
  • O archive move o filesystem para object storage, o que encerra a cobrança de disco e libera quota de disco. Levou 18 s aqui (29 s e 42 s em outras execuções), e o start a partir de archived levou 2,25–4,79 s com o arquivo intacto.
  • pause() em um container é um 422. Repare no espaço faltando na mensagem do SDK.

Teste 5: Pause e fork em uma Linux VM

python 05_vm_pause_fork.py

Os snapshots de VM ficam na sua própria região, então o script pergunta ao snapshot onde ele está e aponta um client para lá:

regions = Daytona().snapshot.get("daytona-vm-small").region_ids   # ['us-west-3']
daytona = Daytona(DaytonaConfig(target=regions[0]))
vm = daytona.create(CreateSandboxFromSnapshotParams(snapshot="daytona-vm-small", ...))

Com o client padrão us, o mesmo create falha com “Snapshot daytona-vm-small is not available in region us”. Esse é o erro que eu recebia antes de a quota existir, então é fácil interpretá-lo errado como “sem VMs”. No dashboard, os snapshots daytona-vm-* mostram us-west-3 na coluna de região:

Página Snapshots do dashboard do Daytona listando os snapshots de sistema: daytona-vm-small, daytona-vm-medium e daytona-vm-large na região us-west-3, os snapshots de container daytona-small, daytona-medium e daytona-large na região us, snapshots Windows em us e em mais uma região, daytona-gpu na região earth, e o build declarativo customizado my2cents-claude-creds em us

O script então inicia um contador em memória (um loop Python que imprime um número a cada segundo e não escreve nada em disco), faz pause e resume da VM três vezes e faz fork dela:

=== Create a Linux VM sandbox ===================================
daytona-vm-small: regions ['us-west-3']
[  2.10s] create(snapshot='daytona-vm-small')
vm: 71284db2-... target=us-west-3 class=linux-vm auto-pause=60 auto-stop=0
inside: 1 vCPU, 982 MiB

=== Pause cycle 1 ===============================================
[  0.79s] pause()
exec while paused: DaytonaServiceUnavailableError: Failed to execute command: sandbox is not running (state SANDBOX_STATE_SUSPENDED)
[  0.37s] start() from paused
counter: 4 before pause, 7 two seconds after resume

=== Pause cycle 2 ===============================================
[  0.69s] pause()
[  0.35s] start() from paused
counter: 8 before pause, 11 two seconds after resume

=== Pause cycle 3 ===============================================
[  0.66s] pause()
[  0.35s] start() from paused
counter: 11 before pause, 14 two seconds after resume

=== fork() with a running process ===============================
[  0.94s] fork()
fork: 7c151a85-... class=linux-vm, parent counter was 14 at fork time
parent counter: 19, fork counter: 19
parent counter: 23, fork counter: 23
the fork is 8s old; its counter process has been running for 24s

=== Delete the parent while it has a fork =======================
DaytonaBadRequestError: Failed to remove sandbox: Cannot delete sandbox which has active fork children. The forks must be deleted first.
deleted the fork, then the parent
  • Pause e resume são rápidos. Em cinco execuções, pause() levou 0,66–0,96 s e start() a partir de paused levou 0,35–0,60 s. Criar a VM levou 2,05–2,28 s.
  • A memória sobrevive, e o processo é congelado, não reiniciado. Cada ciclo cobre cerca de 8 s de tempo de relógio (pause, uma espera de 5 s, resume, uma espera de 2 s), mas o contador só avançou 3. Ele parou de contar enquanto estava em pause e continuou do mesmo valor, em vez de recomeçar em 1 como aconteceria depois do stop de um container.
  • Uma VM em pause também não acorda sozinha. Um exec() contra ela falha com um 503 até você chamar start(), a mesma regra de um container parado.
  • O fork copia o processo em execução. O contador do fork continuou a partir do valor do pai, e ps dentro do fork encontrou um processo contador mais velho do que o próprio fork (24 s contra 8 s). fork() levou 0,72–0,94 s. Isso resolve uma lacuna na documentação: a documentação de sandboxes diz que o fork duplica o “filesystem and memory”, enquanto a docstring do fork() em Python só menciona um filesystem copy-on-write. A documentação de sandboxes está certa.
  • Um pai não pode ser deletado enquanto tem forks, como documentado. Delete os forks primeiro.
  • Uma VM reporta o seu próprio tamanho. nproc e free mostraram 1 vCPU e 982 MiB, a alocação real, enquanto um container mostra a do host (Teste 1). Ela também roda o seu próprio kernel (6.19) sobre Debian 13.

Um teste separado cobriu o resto do ciclo de vida da VM. stop() e start() funcionam e mantêm os arquivos (1,5–1,7 s para o stop, 1,7–2,0 s para o start), mas a memória é apagada, como em um container. archive() é recusado com “Sandboxes in this region or class cannot be archived”; a documentação diz que uma VM parada já descarrega o seu filesystem. VMs têm por padrão auto-pause após 60 minutos de inatividade, com auto-stop desligado. Com set_auto_pause_interval(1), uma VM inativa entrou em pause depois de cerca de 72 s e ficou em pause até eu chamar start().

Teste 6: List, filtro e reconexão a partir de outro processo

python 06_list_connect.py

Labels mais daytona.list() encontram sandboxes; daytona.get(id_or_name) pega um deles. Sandboxes também podem ter um name único, o que significa que um segundo processo nem precisa do ID:

from daytona import ListSandboxesQuery

for s in daytona.list(ListSandboxesQuery(labels={**DEMO_LABELS, "userId": "alice"})):
    print(s.name, s.state)

# ...in a different Python process, with only the name:
s = Daytona().get("my2cents-06-bob")
if s.state.value != "started":
    s.start()
stopped bob, get() says: stopped

=== How long until list() agrees? ===============================
  list() still says stopping after 0.1s
  list() says stopped after 1.2s

=== List: all demo sandboxes ====================================
  my2cents-06-bob      stopped  bob
  my2cents-06-carol    started  carol
  my2cents-06-alice    started  alice

=== List: stopped only ==========================================
  my2cents-06-bob      stopped

=== Reconnect by name from another process ======================
  get('my2cents-06-alice') -> e6f5d24e-... state=started in 0.15s
  workspace of alice
[  1.13s] child process for my2cents-06-alice
  get('my2cents-06-bob') -> 8432db04-... state=stopped in 0.13s
  start() in 0.60s
  workspace of bob
[  1.59s] child process for my2cents-06-bob

(Saída cortada: as seções de create, filtro por alice e troca de labels ficaram de fora.)

A visão do dashboard de três sandboxes com labels em três estados diferentes (de uma execução separada, por isso se chamam my2cents-shot-*):

Lista Sandboxes do dashboard do Daytona com três sandboxes do snapshot padrão daytonaio/sandbox na região us, cada um com 1 vCPU, 1 GiB e 3 GiB: my2cents-shot-carol Archived, my2cents-shot-bob Stopped e my2cents-shot-alice Started

Filtrar por labels e estado funciona, e get() por name levou 0,13–0,15 s. Como nada acorda sozinho, reconectar a um sandbox parado é sempre get() e depois start() (0,60 s aqui).

O ponto de atenção é que list() fica atrás do get(). Logo depois que stop() retornou, get() dizia stopped enquanto list() ainda dizia stopping por 1,2 s (2,5 s em outra execução). Na minha primeira execução deste script, um filtro “stopped only” rodado imediatamente após o stop voltou vazio. Se você construir um reaper ou um dashboard em cima de list(), não trate a resposta dele como instantânea.

set_labels() substitui o conjunto inteiro de labels em vez de mesclar, então passe as labels existentes junto com as novas.

Teste 7: Snapshots, e fork em um container

python 07_snapshot_fork.py
sbx.process.exec(f"pip install -q cowsay && echo 'state from the parent' > {home}/state.txt")
sbx.fork(name="my2cents-07-fork")       # 422 on a container
sbx.create_snapshot("my2cents-lifecycle-07")
child = daytona.create(CreateSandboxFromSnapshotParams(snapshot="my2cents-lifecycle-07", labels=...))
=== fork() on a container =======================================
DaytonaUnprocessableEntityError: Failed to fork sandbox: Forking is not supported for this sandbox

=== create_snapshot() while running =============================
[  6.64s] create_snapshot (running)
snapshot my2cents-lifecycle-07: state=SnapshotState.ACTIVE size=15.265777064487338 GB

=== Three sandboxes from the snapshot ===========================
[  1.96s] create child 1
  state from the parent | cowsay 6.1
[  1.10s] create child 2
  state from the parent | cowsay 6.1
[  1.21s] create child 3
  state from the parent | cowsay 6.1

=== Children diverge ============================================
  child 1: 2 line(s)
  child 2: 1 line(s)
  child 3: 1 line(s)

=== Delete the parent: children keep running ====================
  child 1 still: started
  • Fork é só para VM, como documentado: 422 em um container (funcionou em uma VM no Teste 5).
  • Um container pode ter snapshot tirado enquanto roda. A documentação de snapshots se contradiz aqui: a introdução diz “running or stopped”, enquanto a tabela e a spec da API dizem que um snapshot de filesystem exige um sandbox parado. O meu funcionou em um sandbox em execução, em 6,3–18 s ao longo de três execuções. Ele captura apenas o filesystem: o package instalado via pip e o arquivo vieram junto, nenhum processo veio.
  • Os filhos subiram em 1,1–3,4 s com os arquivos e packages do pai, divergiram de forma independente e continuaram rodando depois que o pai foi deletado.
  • O snapshot reportou 15,27 GB para um sandbox com disco de 3 GiB. O tamanho inclui as camadas da imagem base. A documentação de billing diz que snapshots tirados de um sandbox são cobrados por storage, mas a página de preços não lista nenhuma tarifa de snapshot, então delete os snapshots de que você não precisa.

Em um container, portanto, um snapshot carrega o filesystem e os packages instalados, mas nenhum processo em execução nem memória.

Teste 8: Um snapshot customizado vs instalar em runtime

python 08_custom_snapshot.py

A imagem padrão do Daytona já tem pandas, numpy, matplotlib e torch, então este teste usa packages que ela não traz: polars, DuckDB e PyArrow. O builder declarativo Image do Daytona gera um Dockerfile e faz o build do lado deles:

from daytona import CreateSnapshotParams, Image

image = Image.debian_slim("3.12").pip_install(["polars==1.35.2", "duckdb==1.4.2", "pyarrow==22.0.0"]).workdir("/home/daytona")
daytona.snapshot.create(CreateSnapshotParams(name="my2cents-lifecycle-tpl", image=image), on_logs=lambda line: None, timeout=0)
=== Build the snapshot ==========================================
FROM python:3.12.10-slim-bookworm
RUN apt-get update
RUN apt-get install -y gcc gfortran build-essential
RUN pip install --upgrade pip
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
RUN python -m pip install duckdb==1.4.2 polars==1.35.2 pyarrow==22.0.0
WORKDIR /home/daytona

[ 61.78s] snapshot.create()

=== Path A: sandbox from the custom snapshot ====================
  1.35.2 1.4.2 22.0.0
[  1.71s] create + first import

=== Path B: default sandbox + pip install =======================
  1.35.2 1.4.2 22.0.0
[  7.45s] create + pip install + first import

O build levou 62 s (58 s em uma execução anterior). O tempo do create até o primeiro import foi de 1,71 s a partir do snapshot customizado vs 7,45 s instalando em runtime (2,29 s vs 7,97 s e 3,07 s vs 7,33 s em outras execuções), um ganho de 2,4x a 4,4x, sem um primeiro import caro na imagem customizada. Repare no que o debian_slim coloca na imagem: um toolchain de compilador e gfortran, que você talvez não queira em uma imagem de produção. Image.base("your/registry/image") ou Image.from_dockerfile() dão controle total.

Snapshots construídos dessa forma são sempre containers. Snapshots de VM não podem ser construídos a partir de um Dockerfile ou do builder Image; eles precisam de uma imagem de registry ou de um snapshot tirado de um sandbox VM existente.

Resumo dos resultados

Operação Medido (SDK 0.226.1, us, container, a partir do meu laptop)
daytona.create() (snapshot padrão, sequencial) mediana de 0,88 s (0,74–1,94 s, n=5); mediana de 1,28 s em uma execução anterior
stop() / start() 0,80–1,42 s / 0,70–1,33 s (um primeiro ciclo: 4,9 s / 10,6 s)
archive() / start() a partir de archived 18–42 s / 2,25–4,79 s
Auto-stop com um processo rodando disparou em ~62 s (intervalo de 1 min)
ttl_minutes=2 em um sandbox parado destruído em ~124 s
get(name) / start() a partir de um segundo processo 0,13–0,15 s / 0,60 s
Atraso do list() após stop() 1,2–2,5 s
create_snapshot() em um container em execução 6,3–18 s (15,27 GB reportados)
Create a partir de snapshot 1,1–3,4 s
Build de snapshot customizado (polars, duckdb, pyarrow) 62 s
Tempo até o primeiro import: instalação em runtime vs snapshot customizado 7,33–7,97 s vs 1,71–3,07 s
Pause, fork (container) 422, não suportado
Linux VM: create / pause() / start() a partir de paused 2,05–2,28 s / 0,66–0,96 s / 0,35–0,60 s, memória mantida
Linux VM: fork() com um processo rodando 0,72–0,94 s, processo levado junto

Estas são medições de uma única região, a partir de um laptop em uma conexão doméstica. Use-as como noção de escala, não como benchmark.

Preços

O Daytona cobra por segundo pelos recursos alocados a um sandbox (preços):

RecursoTarifaPor hora
vCPUUS$ 0,000014 / vCPU-segundoUS$ 0,0504
RAMUS$ 0,0000045 / GiB-segundoUS$ 0,0162
Discodepois dos primeiros 5 GiB grátisUS$ 0,000108 / GiB-hora
Sandbox padrão (1 vCPU, 1 GiB, 3 GiB)≈ US$ 0,067 enquanto started
Sandbox paradoapenas disco≈ US$ 0,0003 para um disco padrão de 3 GiB, ou US$ 0 se couber nos 5 GiB grátis
Archived ou deletedgrátisUS$ 0

Sandboxes Windows custam US$ 0,0858 por vCPU-hora. Nenhuma tarifa separada para Linux VM é publicada, e a documentação de billing lista as VMs sob os mesmos estados de cobrança. Uma VM em pause é cobrada apenas pelo disco: “The preserved memory state is not billed.” Nenhuma tarifa de storage de snapshot é publicada.

Duas coisas se destacam. Não há taxa mensal de plano: os limites vêm do tier, que você desbloqueia com recargas em vez de uma assinatura. E um container parado ainda custa disco e ocupa quota de disco até ser arquivado. Com os padrões, um sandbox parado é arquivado após 7 dias, mas nunca deletado.

Para ter uma escala: a organização que eu criei para estes testes tinha rodado 106 sandboxes (os scripts deste post, os da parte 2 e os meus testes avulsos) quando eu olhei a página Spending, com um total de US$ 0,08. O Daytona diz que as cobranças podem atrasar até 48 horas, então trate isso como um piso. O disco representou 64% do custo, mais do que CPU e RAM somados, que é o efeito do sandbox parado descrito acima em miniatura:

Página Spending do dashboard do Daytona para os últimos 30 dias: custo total de US$ 0,08 em 106 sandboxes, 1,1 CPU-horas, 1,1 GB-horas de RAM e 3,9 GB-horas de disco, com uma divisão por recurso de CPU 18 por cento, RAM 18 por cento e disco 64 por cento

Cleanup

Sandboxes parados são arquivados depois de uma semana, mas nunca deletados, porque o auto-delete vem desligado por padrão. Isso faz deles a coisa mais fácil de esquecer no Daytona. O último script do repositório de apoio deleta tudo que carrega a label da demo, mais os snapshots da demo:

from daytona import Daytona, ListSandboxesQuery

daytona = Daytona()
for s in daytona.list(ListSandboxesQuery(labels={"demo": "my2cents-lifecycle"})):
    s.delete(wait=True)   # retried on 409 "Sandbox state change in progress"

for snap in daytona.snapshot.list().items:
    if snap.name.startswith("my2cents-lifecycle"):
        daytona.snapshot.delete(snap)
=== Sandboxes labelled {'demo': 'my2cents-lifecycle'} ===========
processed 0

=== Snapshots named my2cents-lifecycle* =========================
  deleted my2cents-lifecycle-tpl
deleted 1

Zero sandboxes é o resultado esperado aqui: todo teste deleta os próprios sandboxes, e o script de cleanup é a rede de segurança. Uma pegadinha que encontrei em uma execução anterior: deletar um sandbox que ainda está no meio de uma mudança de estado (porque outro delete está em andamento) retorna 409 “Sandbox state change in progress”, então faça retry em vez de falhar.

Para produção, eu definiria três coisas em todo sandbox: um ttl_minutes como teto rígido, um auto_delete_interval para que sandboxes parados não fiquem largados, e labels de usuário e job para que um cleanup como este consiga encontrá-los.

Veredito

Os sandboxes container do Daytona sobem rápido, fazem stop e start rápido, são baratos e vêm com uma imagem padrão que já tem a maior parte do que um agent precisa. O ciclo de vida tem bastante botão para ajustar, e a maioria dos que eu consegui testar se comportou como documentado. As exceções: o timeout do exec e os snapshots de containers em execução foram mais permissivos do que a documentação, e as Linux VMs exigiram um pedido ao suporte e uma região diferente. Depois que eu as tive, pause e fork fizeram o que a documentação promete, em menos de um segundo.

O que eu teria em mente antes de construir em cima dele:

  • A classe padrão é um container, e se comporta como um. Stop é um restart: arquivos sobrevivem, processos e memória não. A contagem de CPUs e a memória do host aparecem para qualquer coisa que leia nproc ou /proc/meminfo.
  • Nada acorda sozinho. Em um sandbox parado, comandos e requests de preview falham com um 400 até você chamar start(); só chamadas de ciclo de vida como get() continuam funcionando. Somado a um timer de auto-stop que ignora processos rodando lá dentro, um agent fazendo um job longo pode ser parado no meio da tarefa e continuar parado. Defina auto_stop_interval=0 ou chame refresh_activity() para qualquer coisa de longa duração.
  • Faça o cleanup de forma deliberada. O auto-delete vem desligado por padrão, então sandboxes parados mantêm o disco até serem arquivados depois de uma semana, e snapshots têm o seu próprio storage. Um ttl_minutes em todo sandbox é a rede de segurança mais simples.
  • Verifique o que a sua organização realmente pode criar. Pause com preservação de memória e fork precisam da classe Linux VM. A minha só a conseguiu pelo suporte, em us-west-3 em vez de us, então as VMs precisam do seu próprio client e da sua própria quota.

Quem deve usar

O Daytona se encaixa em workspaces de agents de longa duração em que os arquivos importam mais do que o estado em memória: coding agents que clonam um repositório, trabalham nele por uma hora e voltam amanhã. A ausência de limite de runtime, o estado parado barato e a imagem que já vem com tudo ajudam nesse cenário. Com a classe Linux VM, agents que precisam manter um processo rodando ou memória aquecida entre requests também ficam cobertos, via pause e fork. Ele também oferece sandboxes Windows e GPUs.

Se o seu produto depende de congelar um processo em execução, a classe Linux VM faz isso bem: uma VM em pause custa apenas disco e é retomada com os seus processos em menos de um segundo. Verifique se a sua organização tem acesso a ela, e em qual região, e lembre-se de que uma VM em pause ainda espera um start() explícito. E se você quer que a própria plataforma seja inspecionável, note que, desde junho, o lado do servidor é closed source.

Comments