SnapStart em container: o bake-off contra concorrência provisionada
Em 2 de setembro de 2026 a AWS estendeu o Lambda SnapStart para funções empacotadas como imagem de container, o formato que muita organização adotou por padrão corporativo de deploy, ou simplesmente porque precisava de mais que os 250 MB do .zip. A promessa é honesta: em vez de baixar camadas de imagem e inicializar o runtime a cada ambiente de execução novo, o Lambda tira um snapshot do ambiente já inicializado no momento em que você publica a versão e retoma dali. Só que trocar inicialização por snapshot não é uma flag de performance neutra. Muda quando você paga, muda o que quebra e muda onde o deploy falha. Este é o bake-off que eu faria antes de ligar isso em produção.
O que de fato foi destravado
O bloqueio nunca foi o mecanismo de snapshot, o Firecracker já congelava memória e disco desde o SnapStart original em Java. O bloqueio era o contrato de ciclo de vida: uma imagem de container pode carregar qualquer runtime, e o Lambda precisava de um jeito padronizado de saber quando a inicialização terminou e quando o ambiente voltou do congelamento.
Esse contrato agora é público, e vive na Runtime API. Ao fim da sua inicialização, se AWS_LAMBDA_INITIALIZATION_TYPE valer snap-start, você roda seus hooks de before-snapshot e chama GET /runtime/restore/next. A chamada bloqueia, literalmente, o processo fica parado ali, até que o Lambda restaure aquele ambiente a partir do snapshot e devolva HTTP 200. Aí você roda os hooks de after-restore e entra no loop normal de invocação. Erros vão para /runtime/init/error (falha o PublishVersion) ou /runtime/restore/error (falha a invocação em voo e derruba o ambiente).
Se você usa as imagens base da AWS para Java 11+, Python 3.12+ ou .NET 8+, o Lambda coordena tudo isso e a experiência é idêntica ao .zip. Se você traz sua própria imagem base, um Runtime Interface Client próprio, ou as imagens base de provided.al2023, Node.js e Ruby, existem dois caminhos: implementar o /restore/next ou declarar LABEL com.amazonaws.lambda.feature.snapstart="Allow" no Dockerfile. Sem uma das duas coisas, publicar a versão simplesmente falha.
O ciclo de vida do SnapStart em imagem de container
A inicialização sai do caminho da requisição e vai para o caminho do deploy. Os medidores de custo seguem o snapshot, não a invocação.
- Dockerfile · LABEL ...snapstart=Allow
- Amazon ECR · imagem ≤ 10 GB descomprimida
- PublishVersion · ApplyOn=PublishedVersions
- Init · runtime + pesos do modelo
- beforeCheckpoint · GET /runtime/restore/next
- Snapshot Firecracker · cifrado, cacheado, replicado
- Restore · retoma do snapshot
- afterRestore · re-semeia CSPRNG, reconecta
- Handler · resposta ao cliente
- INIT_REPORT · Init Duration
- REPORT · Restore + Billed Restore
- X-Ray · subsegmento Restore
- Cache do snapshot · US$0,0000015046/GB-s (mín. 3h)
- Restauração · US$0,0001398 por GB
A física: o que o snapshot remove e o que ele não remove
O SnapStart tira do caminho da requisição três coisas caras: o download e a otimização das camadas da imagem, a carga do runtime e a sua inicialização, imports, wiring de framework, compilação de esquema, carregamento de pesos de modelo em memória. Tudo isso passa a acontecer uma vez por versão publicada, no deploy.
O que ele não remove é o que é genuinamente por ambiente. Conexões de rede abertas na init não têm estado garantido depois da retomada; o SDK da AWS costuma se reconectar sozinho, o resto é responsabilidade sua no hook de after-restore. Entropia é o caso perigoso: se você gerou um UUID, um segredo ou uma seed na init, esse valor entra no snapshot e sai idêntico em todos os ambientes restaurados. O Lambda já ajuda num ponto sensível, com SnapStart ativo o runtime passa a usar as credenciais de container (AWS_CONTAINER_CREDENTIALS_FULL_URI) em vez das variáveis de access key, justamente para que credenciais não expirem congeladas dentro do snapshot.
E há uma inversão de design que quase ninguém nota na primeira leitura: com SnapStart, o /tmp fica limitado a 512 MB, contra os 10.240 MB configuráveis de uma função normal. O padrão clássico de inferência, baixar pesos do S3 para o /tmp na init, deixa de caber. O caminho que sobra é assar os pesos na própria imagem (até 10 GB descomprimidos) e carregá-los em memória, onde o snapshot os captura já materializados. É exatamente o cenário que a AWS cita, mas ele exige reescrever o carregamento, não só ligar a flag.
Quatro formas de matar a partida a frio de um artefato grande
| Container + SnapStart | Container + Conc. provisionada | .zip + SnapStart | ECS Fargate sempre ligado | |
|---|---|---|---|---|
| Partida a frio | Sub-segundo (Restore + Duration) | Dezenas de ms, sem partida a frio | Sub-segundo, sem pull de imagem | Zero em regime; minutos ao escalar |
| Custo fixo mensal (2 GB, us-east-1) | ~US$ 7,80 por versão publicada | ~US$ 21,60 por unidade provisionada | ~US$ 7,80 por versão publicada | vCPU + GB por segundo, 24×7 |
| Custo por partida | US$ 0,00028 por restauração (2 GB) | Nenhum; você já pagou o tempo todo | US$ 0,00028 por restauração (2 GB) | Nenhum, mas ociosidade é paga |
| Teto do artefato | 10 GB descomprimidos, via ECR | 10 GB descomprimidos, via ECR | 250 MB descompactados | Sem teto prático |
| Armazenamento efêmero (/tmp) | Máximo de 512 MB | Até 10.240 MB | Máximo de 512 MB | Volume da task, configurável |
| Runtimes cobertos | Java 11+, Python 3.12+, .NET 8+ e base própria com hooks | Qualquer runtime | Só Java 11+, Python 3.12+, .NET 8+ | Qualquer runtime |
| Alvo de invocação | Só versão publicada ou alias | Versão ou alias provisionado | Só versão publicada ou alias | Endpoint do balanceador |
| Modo de falha típico | PublishVersion falha; estado não-único vazado entre ambientes | Estouro de concorrência cai em partida a frio | Dependência não cabe em 250 MB | Capacidade ociosa vira a maior linha da fatura |
A conta, feita com os preços publicados
Os números de us-east-1, arquitetura x86: cache do snapshot a US$ 0,0000015046 por GB-segundo, restauração a US$ 0,0001397998 por GB, concorrência provisionada a US$ 0,0000041667 por GB-s e duração sob demanda a US$ 0,0000166667 por GB-s. Para runtimes gerenciados Java a documentação isenta o preço de SnapStart; a conta abaixo vale para Python e .NET.
Uma função de 2 GB com uma versão publicada mantida ativa um mês inteiro custa cerca de US$ 7,80 só de cache. Uma única unidade de concorrência provisionada nos mesmos 2 GB custa cerca de US$ 21,60 no mês. Cada restauração de 2 GB sai por US$ 0,00028, ou seja, você precisaria de aproximadamente 77 mil restaurações mensais para que o custo variável do SnapStart alcançasse uma unidade provisionada. Para a esmagadora maioria das APIs, o SnapStart ganha por larga margem.
O problema não é o preço unitário; é a multiplicação. A cobrança de cache é por versão publicada e continua enquanto a versão existir, com mínimo de 3 horas faturadas. Uma esteira que publica versão a cada merge e nunca poda deixa dezenas de snapshots pagos rodando para trás. Vinte versões de 2 GB esquecidas no ar custam mais de US$ 150 por mês para servir absolutamente nenhum tráfego. Some a isso o fato de que o Lambda regenera snapshots periodicamente para aplicar patches de runtime, e cada re-execução da init é cobrada. Uma política de retenção de versões deixa de ser higiene e passa a ser controle financeiro.
Como eu decidiria, por perfil de carga
Container + SnapStart
- Melhor relação latência/custo para tráfego intermitente com artefato grande
- Mantém o padrão corporativo de deploy em imagem e o pipeline de ECR intacto
- Pesos de modelo carregados em memória entram no snapshot já materializados
- Teto de 512 MB em /tmp derruba o padrão de baixar artefato do S3 na init
- Exige auditoria de unicidade: entropia, IDs e conexões abertas na init
- Incompatível com concorrência provisionada, não dá para hedgear
Padrão para API interativa e inferência com pesos assados na imagem.
Container + concorrência provisionada
- Latência de partida em dezenas de milissegundos, sem retomada nenhuma
- Funciona com qualquer runtime e com /tmp de até 10 GB
- Sem exigências de unicidade sobre o código de inicialização
- Cerca de 2,8× o custo do cache de snapshot por GB-hora equivalente
- Dimensionar mal é caro; o estouro cai em partida a frio completa
Só quando o SLO é estrito o bastante para que a retomada não caiba nele.
.zip + SnapStart
- Sem etapa de otimização de imagem nem estado Pending/Inactive do container
- Ciclo de vida coordenado pelo runtime gerenciado, zero código de hook
- 250 MB descompactados é pouco para dependências de ML
- Migrar de container para .zip exige uma função nova, o tipo de pacote é imutável
Continua o caminho mais simples quando a dependência cabe. Não é para onde migrar.
ECS Fargate sempre ligado
- Nenhuma restrição de runtime, de /tmp ou de unicidade de estado
- Conexões persistentes, cache local quente e processos de longa duração
- Você paga ociosidade 24×7 e herda escalonamento, patching e balanceador
- Escalar horizontalmente leva minutos, não milissegundos
Justifica-se por utilização alta e sustentada, não por medo de partida a frio.
O que passa a quebrar na esteira, não na requisição
Este é o ponto que mais me interessa como arquiteto: o SnapStart move a inicialização para o deploy, e com ela move os modos de falha.
A init e os hooks de before-snapshot compartilham um timeout combinado de max(timeout_da_função, 130 segundos). Estourou, o PublishVersion falha. Uma função de inferência que carrega 4 GB de pesos e antes só ficava lenta na primeira chamada agora quebra o pipeline de release, o que, sinceramente, é o lugar certo para quebrar, desde que a esteira trate isso como erro de deploy e não como flake.
Segundo: SnapStart só existe em versão publicada ou alias que aponte para versão. $LATEST nunca é SnapStart. Isso significa que o loop de desenvolvimento que invoca $LATEST exercita um caminho de código diferente do de produção; os testes de contrato precisam bater no alias. Terceiro: o tipo de pacote é imutável. Você não converte uma função de container em .zip, cria outra função, com outro ARN, outro alias, outros event source mappings. Planeje isso como migração, não como toggle.
E há os estados que só aparecem com tráfego raro. Uma função de container sem invocação por semanas tem a imagem otimizada recuperada, volta para Pending e rejeita a primeira invocação. Para runtimes Java, o snapshot é apagado após 14 dias sem invocação e você recebe SnapStartNotReadyException. Ambos são erros retentáveis pelo chamador, desde que o chamador tenha retry com backoff e a operação seja idempotente. Sem isso, a sua função de baixo tráfego tem um erro embutido no calendário.
Como medir isso sem se enganar
O primeiro efeito colateral do SnapStart é que os seus dashboards de partida a frio param de funcionar em silêncio. O campo Init Duration some do REPORT, porque a inicialização não acontece mais na invocação; ele passa a viver num registro separado, o INIT_REPORT, junto com a duração dos hooks de before-snapshot. No REPORT aparecem dois campos novos: Restore Duration e Billed Restore Duration. Eles não são a mesma coisa, o primeiro inclui trabalho feito fora da microVM, que o usuário espera mas você não paga; o segundo é só a carga do runtime e os hooks de after-restore.
A fórmula que importa é simples e precisa entrar no seu SLO: partida a frio = Restore Duration + Duration. Se o seu alarme só olha @duration, ele vai mostrar uma melhora que não existe. No X-Ray a mudança é análoga: não há mais subsegmento Initialization, e sim Restore. Pela Telemetry API chegam platform.restoreStart, platform.restoreRuntimeDone (com status success, failure ou timeout) e platform.restoreReport.
Dois detalhes práticos. As variáveis AWS_LAMBDA_LOG_GROUP_NAME e AWS_LAMBDA_LOG_STREAM_NAME não existem em função SnapStart, bibliotecas de log que dependem delas quebram de forma discreta. E a medida honesta de impacto não é a duração da função: é a IntegrationLatency do API Gateway ou a UrlRequestLatency da function URL, em p99 e p99,9. Só ali você vê o que o cliente sentiu.
Os cinco erros que eu esperaria ver nos primeiros três meses
- Ligar
ApplyOn=PublishedVersionssem auditar a init: seeds, UUIDs e segredos gerados antes do snapshot passam a ser idênticos em todos os ambientes restaurados. - Publicar uma versão por merge e nunca podar, o cache do snapshot é cobrado por versão ativa, com mínimo de 3 horas, para sempre.
- Manter o padrão de baixar pesos de modelo do S3 para o
/tmpna init, ignorando o teto de 512 MB que o SnapStart impõe. - Testar contra
$LATESTe concluir que 'não mudou nada':$LATESTnunca usa SnapStart. - Declarar vitória com base em
@duration, sem somarRestore Durationnem olhar a latência de integração na borda.
O verdadeiro trade-off não é latência contra custo
É determinismo contra unicidade. A concorrência provisionada mantém N ambientes independentes, cada um com sua própria entropia, suas próprias conexões e seu próprio ciclo de vida. O SnapStart mantém um estado inicial canônico e o clona. Tudo que era acidentalmente único por ambiente vira deliberadamente compartilhado. Em sistemas financeiros isso deixa de ser detalhe de performance e vira questão de correção: um gerador pseudoaleatório clonado, um token de idempotência pré-computado ou um pool de conexões congelado produzem bugs que não aparecem em teste de carga, aparecem em reconciliação.
Leitura pelos pilares
Segurança
O snapshot é cifrado, mas qualquer segredo materializado na init é clonado; busque credenciais no handler e re-semeie CSPRNG no after-restore.
Confiabilidade
SnapStartNotReadyException e o estado Pending do container exigem retry com backoff e operações idempotentes no chamador.
Eficiência de performance
Meça Restore Duration + Duration e valide contra IntegrationLatency na borda; a melhora só é real em p99,9.
Eu ligaria isso, mas na terceira semana, não na primeira. A ordem que uso é sempre a mesma: primeiro instrumento o Restore Duration num ambiente de homologação com o alias real, depois audito a init caçando entropia, credenciais e conexões, é sempre nessa auditoria que aparece o achado desconfortável. Só então mexo em produção. Já vi um sistema de pagamento em que a init pré-computava um sufixo de chave de idempotência 'para economizar tempo'; sob concorrência provisionada isso nunca colidiu, porque cada ambiente tinha o seu. Com snapshot clonado, teria colidido, e o incidente não apareceria em latência, apareceria em transação duplicada, dois dias depois, no fechamento. Performance que você não consegue reconciliar não é performance, é dívida.
Recomendação
Para APIs interativas e inferência empacotadas em imagem de container, com Java 11+, Python 3.12+ ou .NET 8+ sobre as imagens base da AWS, o SnapStart é agora a escolha padrão: entrega partida sub-segundo por cerca de um terço do custo mensal de uma unidade de concorrência provisionada equivalente, e o ponto de virada do custo de restauração está tão longe (~77 mil restaurações por mês, a 2 GB) que raramente importa. Reserve a concorrência provisionada para os endpoints cujo SLO não comporta a retomada, ou que dependem de /tmp acima de 512 MB. Não migre para .zip só para ganhar SnapStart, o tipo de pacote é imutável e o benefício não paga a troca de ARN. E antes de qualquer coisa, faça duas tarefas nada glamourosas: uma auditoria de unicidade na init e uma política automática de poda de versões publicadas. É nessas duas que o SnapStart é ganho ou perdido.
Referências
Continue lendo
Deep dives de arquitetura, AWS, IA e mercado: direto no seu email. Grátis.
Sem spam · cancele quando quiser
Pergunte ao Fernando sobre isto
Uma resposta focada sobre este artigo, do meu assistente de IA, baseada no meu trabalho.
Participe da conversa
Entre para comentar
Confirme seu e-mail para participar, você também recebe a newsletter. Sem senha.