Perto do final da minha última postagem sobre como mover o FFmpeg do meu servidor Fly.io primário, escrevi:
Eu realmente gostaria de não ter que fazer a dança dos batimentos cardíacos. Seria um ótimo recurso ter apenas amigos Cloudflare integrados!
Bem. Vinte e quatro horas depois, a dança dos batimentos cardíacos desapareceu.
Recapitulação rápida
Essa postagem explica a história completa, mas aqui está a versão resumida: eu estava executando o FFmpeg inline em meu servidor de aplicativo principal e ele saturou a CPU sempre que publiquei um episódio mais longo. A solução foi mover o trabalho para Cloudflare: uma fila entrega o trabalho para um Worker, que o encaminha para um Cloudflare Container, que faz a costura do FFmpeg e carrega as saídas para R2.
Isso resolveu o problema de produção. Mas o design do contêiner tinha algumas arestas que não me agradaram.
O contêiner não sabia que estava pronto. Quando o trabalho do FFmpeg terminou, o contêiner não conseguiu dizer ao Cloudflare “você pode me impedir agora”. Esse controle reside no wrapper Cloudflare Worker e Durable Object, então tive que conectar pings de pulsação de dentro do processo do contêiner enquanto o FFmpeg estava em execução, além de um endpoint “desligamento se inativo” que o contêiner chamava na conclusão do trabalho para verificar se algum outro trabalho estava ativo e, se não, instruísse o controlador a parar o contêiner. Todo esse encanamento de coordenação existia apenas para gerenciar um ciclo de vida que não deveria exigir tanta cerimônia.
Cloudflare Sandboxes têm um modelo diferente. Você chama sandbox.exec() para executar um comando, espera que ele termine e o sandbox está pronto. Sem batimentos cardíacos. Sem sinais de desligamento. Sem verificações ociosas.
A primeira tentativa de sandbox
No dia em que enviei a migração do contêiner, entreguei a um agente do Cursor as chaves para um pico: migrar o pipeline de áudio dos Cloudflare Containers para Cloudflare Sandboxes.
Esse pico se tornou PR #726 . Funcionou e excluiu totalmente o encanamento de pulsação/desligamento. Mas quando olhei para o código (sim, às vezes ainda leio o código), ainda parecia um sistema de contêiner vestindo uma fantasia de sandbox. Um lobo em pele de cordeiro! 😆
O design desse PR era: um serviço call-kent-audio-sandbox dedicado com sua própria configuração Wrangler, seu próprio fluxo de trabalho de implantação e seu próprio endpoint HTTP em /jobs/episode-audio. O trabalhador da fila existente POST um trabalho para esse terminal. O serviço sandbox iniciaria um processo do Node dentro do sandbox, esperaria por uma porta, faria proxy da solicitação para ela e executaria o trabalho. O próprio serviço sandbox possuía a lógica de retorno de chamada e mantinha as credenciais R2.
A pulsação desapareceu, mas a forma geral era a mesma: um serviço de longa duração situado entre o trabalhador da fila e o trabalho real.
Qual é a aparência real da versão mesclada
Então fechei a primeira tentativa e recomecei com um novo agente que se transformou em PR #729 . Foi necessária uma abordagem diferente. Em vez de construir um novo serviço em torno do sandbox, ele tornou o sandbox um detalhe de implementação do trabalhador existente.
Aqui está o fluxo completo:
enfileirar trabalho (draftId, chaves de áudio R2) entregar mensagem POST audio_generation_started pré-assinar URLs de download e upload URLs assinados exec(call-kent-audio-cli, env={URLs assinados}) baixar chamada + áudio de resposta executar pipeline FFmpeg enviar episódio.mp3 + segmentos JSON stdout (tamanhos de arquivo) POST audio_Generation_completed
O trabalhador da fila agora é o orquestrador. Ele recebe a mensagem, envia um retorno de chamada started, cria URLs R2 pré-assinados de curta duração para as entradas e as saídas, executa uma única chamada exec() em uma nova sandbox, e então envia completed ou failed. A sandbox executa um script de shell, sai e é destruída em um bloco finally. É isso.
A principal diferença do PR #726 é onde as coisas vivem:
| PR #726 (abandonado) | PR #729 (mesclado) | |
|---|---|---|
| Invocação de sandbox | Worker POSTs para endpoint de serviço de sandbox | Chamadas de trabalho sandbox.exec() diretamente |
| Propriedade de retorno de chamada | O serviço sandbox envia retornos de chamada | Worker envia retornos de chamada |
| Credenciais R2 | Passado para a sandbox | Mantido no trabalhador; sandbox recebe apenas URLs assinados |
| Ciclo de vida do sandbox | Processo de serviço de longa duração, verificação de porta pronta | One-shot exec, destroy() em finally |
| Implantar superfície | Serviço separado + fluxo de trabalho separado | Incorporado no pacote de trabalho |
A imagem da sandbox é correspondentemente pequena:
FROM docker.io/cloudflare/sandbox:0.7.16 RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* WORKDIR /opt/call-kent-audio COPY assets ./assets COPY sandbox/call-kent-audio-cli.sh /usr/local/bin/call-kent-audio-cli RUN chmod +x /usr/local/bin/call-kent-audio-cli
A imagem base do Cloudflare Sandbox fornece o tempo de execução. Eu adiciono FFmpeg, copio os recursos de áudio bumper e copio um script de shell. Esse script de shell baixa os arquivos de áudio de entrada de URLs pré-assinados, executa o pipeline de costura FFmpeg, carrega os três arquivos de saída para URLs de upload pré-assinados e imprime JSON em stdout com os tamanhos dos arquivos de saída. Então ele sai. Nada dentro da sandbox precisa de credenciais, segredos ou qualquer conhecimento do sistema mais amplo.
O lado do trabalhador é igualmente legível:
const completed = await runCallKentAudioSandboxJob({ binding: env.Sandbox, sandboxId: createSandboxId(parsed.draftId), request: { draftId: parsed.draftId, attempt, callAudioUrl: signedUrls.callAudioUrl, responseAudioUrl: signedUrls.responseAudioUrl, episodeUploadUrl: signedUrls.episodeUploadUrl, callerSegmentUploadUrl: signedUrls.callerSegmentUploadUrl, responseSegmentUploadUrl: signedUrls.responseSegmentUploadUrl, }, })
E FROM docker.io/cloudflare/sandbox:0.7.16 RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* WORKDIR /opt/call-kent-audio COPY assets ./assets COPY sandbox/call-kent-audio-cli.sh /usr/local/bin/call-kent-audio-cli RUN chmod +x /usr/local/bin/call-kent-audio-cli se resume a:
const sandbox = getSandbox(binding, sandboxId) try { const result = await sandbox.exec('/usr/local/bin/call-kent-audio-cli', { env: createSandboxCommandEnvironment(request), timeout: sandboxExecTimeoutMs, }) return getSandboxOutput(result.stdout) } finally { await sandbox.destroy() }
Inicie, execute, destrua. Nenhuma camada de coordenação é necessária.
Como justifiquei mudar tão rápido
Adicionei Cloudflare Containers no dia anterior e tive exatamente uma produção real para avaliar. Isso não é muitos dados.
Mas a justificativa aqui não são os números de desempenho. É que o sistema final tem menos peças móveis e menos locais onde as coisas podem dar errado. O plano de controle que excluí (o loop de pulsação, a verificação de inatividade, o sinal de desligamento, o serviço separado com seu próprio pipeline de implantação) era uma complexidade que eu estava adicionando a um problema que já tinha uma solução mais simples. Um sandbox que executa um trabalho e sai não precisa de nada disso. O ciclo de vida certo para o trabalho em formato de trabalho é uma sandbox em formato de trabalho.
A migração do container ainda valeu a pena. Ele resolveu o problema imediato de produção e rodar na versão containers por um dia sequer tornou óbvio que a cerimônia de pulsação/desligamento era a parte que não precisava existir. Eu simplesmente não conhecia a API do sandbox o suficiente para ver isso até experimentar a primeira versão.
Quanto tempo isso realmente levou
A implementação do contêiner, o pico do sandbox (PR #726), a abordagem do sandbox redesenhado (PR #729), a comparação entre os dois e a validação final, tudo aconteceu em menos de uma hora do meu tempo.
Descrevi o problema para um agente Cursor. Ele construiu a primeira direção sandbox. Eu olhei para o que ele construiu, pensei “isso ainda tem o formato de um serviço de contêiner”, descrevi o formato mais simples que eu queria e ele o reconstruiu. Revisei o resultado, mesclei-o e segui em frente.
O agente cuidou dos custos de exploração. Essa é a parte que geralmente torna a iteração arquitetônica lenta: você precisa construir a coisa antes de poder ter uma opinião informada sobre se ela é a coisa certa. Quando esse custo estiver próximo de zero (ou o custo da quantidade de tokens 😅), você pode simplesmente experimentar os dois e escolher o melhor (ou como gosto de dizer, “escolher o que menos odeio”). A história das relações públicas aqui tem uma direção totalmente abandonada que eu realmente usei para informar o design final, e me custou muito pouco tempo para produzir.
O que ainda senti falta
Os agentes não pegaram tudo. Duas coisas só surgiram quando o sistema real foi executado.
Comprimento do ID da sandbox. O trabalhador original gerou IDs de sandbox como este:
const sandboxId = `call-kent-audio-${draftId}-${crypto.randomUUID()}`
Um UUID tem 36 caracteres, então totalizou aproximadamente 89 caracteres. Os IDs do Cloudflare Sandbox devem ter de 1 a 63 caracteres. A primeira execução de produção real falhou imediatamente com const completed = await runCallKentAudioSandboxJob({ binding: env.Sandbox, sandboxId: createSandboxId(parsed.draftId), request: { draftId: parsed.draftId, attempt, callAudioUrl: signedUrls.callAudioUrl, responseAudioUrl: signedUrls.responseAudioUrl, episodeUploadUrl: signedUrls.episodeUploadUrl, callerSegmentUploadUrl: signedUrls.callerSegmentUploadUrl, responseSegmentUploadUrl: signedUrls.responseSegmentUploadUrl, }, })
A correção foi manter o ID rastreável, mas compacto: retire os traços do ID de rascunho e do sufixo aleatório, pegue os primeiros 12 caracteres de cada um e combine-os:
function createSandboxId(draftId: string) { const compactDraftId = draftId.replaceAll('-', '').slice(0, 12) const randomSuffix = crypto.randomUUID().replaceAll('-', '').slice(0, 12) return `call-kent-${compactDraftId}-${randomSuffix}` }
runCallKentAudioSandboxJob tem 10 caracteres, cada segmento compacto tem 12, o separador é 1, dando um total de 35. Bem abaixo do limite, ainda rastreável ao rascunho, ainda único o suficiente.
Eu poderia ter percebido isso executando-o sozinho em um ambiente de teste/visualização… ou dando ao agente as chaves para isso para mim.
A imagem do sandbox não era na verdade uma imagem do sandbox. Esta é uma história melhor.
Durante a revisão do PR, um dos bots automatizados observou que o Dockerfile era executado como root e sugeriu adicionar um usuário não root. O agente que implementa essa mudança também configurou um servidor HTTP mínimo (const sandbox = getSandbox(binding, sandboxId) try { const result = await sandbox.exec('/usr/local/bin/call-kent-audio-cli', { env: createSandboxCommandEnvironment(request), timeout: sandboxExecTimeoutMs, }) return getSandboxOutput(result.stdout) } finally { await sandbox.destroy() } ) como ponto de entrada do contêiner, provavelmente a partir de algum padrão sobre contêineres que precisam de um processo em execução. O problema é que os Cloudflare Sandboxes não são contêineres nesse sentido. O const sandboxId = `call-kent-audio-${draftId}-${crypto.randomUUID()}` SDK espera se comunicar com o tempo de execução do sandbox Cloudflare que está incorporado à imagem base. Quando baseei a imagem no Debian simples e defini meu próprio Sandbox ID must be 1-63 characters long., a configuração da sessão exec do SDK obteve 501 erros porque o tempo de execução não estava lá.
Não percebi isso nos testes porque o caminho simulado local não passa por uma imagem real do sandbox.
Aqui está a parte legal: entreguei essa tarefa de depuração a um agente. Ele se conectou ao ambiente de produção ao vivo usando env vars reais, enfileirou trabalhos descartáveis com IDs de rascunho falsos (para que nada pudesse ser publicado acidentalmente) e percorreu o caminho real da fila até o sandbox na produção. Em poucos minutos, ele isolou a falha: a entrega da fila e o roteamento de retorno de chamada estavam bons, a lógica do trabalhador estava boa e o executivo do sandbox estava falhando com 501s. Ele rastreou isso até a configuração da imagem, identificou o requisito de imagem base ausente e escreveu a correção.
Grite para o servidor Cloudflare MCP 🔥 .
Recebi um resumo descrevendo exatamente o que estava errado e o que foi alterado. Olhei para a diferença, a explicação fazia sentido e mesclei. A próxima sonda de produção foi bem-sucedida e produziu as saídas MP3 esperadas em R2.
Isso é realmente legal. Não é legal “AI escreveu código”, o que neste momento é uma aposta de mesa. Quero dizer “Delegei uma investigação real de depuração de produção, o agente a executou com segurança sem minha supervisão e recebi um diagnóstico e correção corretos” legal. Não passei uma noite mexendo em troncos. Não precisei reconstruir o caminho da falha manualmente. Acabei de revisar o resultado e segui em frente.
O Dockerfile fixo agora tem seis linhas:
FROM docker.io/cloudflare/sandbox:0.7.16 RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* WORKDIR /opt/call-kent-audio COPY assets ./assets COPY sandbox/call-kent-audio-cli.sh /usr/local/bin/call-kent-audio-cli RUN chmod +x /usr/local/bin/call-kent-audio-cli
A imagem base oficial lida com o tempo de execução do sandbox. Eu adiciono FFmpeg e os ativos. Nada mais.
A ruga monorepo
Uma coisa que não estou abordando em detalhes aqui: todo esse trabalho de sandbox aconteceu no mesmo dia em que também migrei o repositório para espaços de trabalho npm e Nx, que moveu tudo em function createSandboxId(draftId: string) { const compactDraftId = draftId.replaceAll('-', '').slice(0, 12) const randomSuffix = crypto.randomUUID().replaceAll('-', '').slice(0, 12) return `call-kent-${compactDraftId}-${randomSuffix}` } . Essa migração teve seu próprio incidente de produção envolvendo caminhos de conteúdo codificados e um estágio Docker quebrado.
Escrevi sobre tudo isso separadamente em Migrating to Workspaces and Nx . A versão resumida é: refatoradores estruturais quebram suposições que você não sabia que tinha, e “o agente estava confiante de que funcionaria” não é o mesmo que “vai funcionar”.
Eu só mencionei isso para dizer que nunca poderia ter feito tanta coisa de uma vez antes dos agentes. Adoro construir software em 2026!
O que eu tiraria disso
Novas primitivas de infraestrutura só ajudam se você permitir que elas mudem a forma do que você está construindo. Os sandboxes da Cloudflare me permitem excluir um plano de controle do ciclo de vida exigido pela abordagem do contêiner, mas os sandboxes simplesmente não precisam. A vitória não foi “sandboxes são mais rápidos” ou “sandboxes são mais baratos” (não tenho dados suficientes para fazer essas afirmações depois de dois dias). A vantagem foi que o design certo para um trabalho único é um modelo de execução único, e a API sandbox torna isso simples.
A migração do contêiner corrigiu o problema de produção. A migração do sandbox corrigiu a forma arquitetônica que foi deixada para trás. Valeu a pena fazer ambos e, juntos, me custaram cerca de uma hora do meu tempo.
