06 - AWS Lambda (Serverless)
video: https://youtu.be/... no frontmatter da nota.AWS Lambda (Serverless)
O AWS Lambda é o serviço de computação serverless da AWS: você envia código, define memória e timeout, e a plataforma executa a função em resposta a eventos, escalando automaticamente de zero a milhares de execuções paralelas sem qualquer administração de servidor. Em produção, o Lambda é a escolha natural para processamento orientado a eventos, APIs com tráfego variável, integrações e automações, com custo proporcional ao tempo efetivo de execução e zero conta em ociosidade. O modelo impõe restrições que precisam ser entendidas antes da adoção: tempo máximo de execução, modelo de concorrência, cold starts e o fato de que a memória configurada determina a CPU alocada. A maturidade do serviço em produção é comprovada pela presença dele em arquiteturas de todos os portes, mas a adoção bem-sucedida depende de desenhar para o modelo de execução, e não de tentar reproduzir nele uma aplicação de servidor tradicional. Este arquivo cobre o ciclo de vida do ambiente, o trade-off de custo, limites, integrações e os casos em que Lambda não é a resposta.
Modelo de execução e ciclo de vida do ambiente
Quando um evento chega, o serviço Lambda provisiona ou reaproveita um ambiente de execução, que é um microVM com runtime configurado, e invoca o handler. Após a execução, o ambiente permanece quente por tempo variável e pode servir chamadas subsequentes, reaproveitando conexões e objetos criados no escopo global. Esse ciclo explica as duas regras de ouro do desenvolvimento Lambda: tudo que é caro, como conexões de banco, clients de SDK e caches locais, deve ser inicializado fora do handler, no escopo global do runtime.
| Fase | O que acontece | Implicação de desenho |
|---|---|---|
| Inicialização fria | Download do pacote, boot do runtime, init do código | Minimize pacote e peso de dependências |
| Reuso quente | Ambiente reaproveitado, sem init | Cachear conexões e clients no escopo global |
| Execução | Handler processa o evento | Código curto, I/O paralelizado, timeout calibrado |
| Congelamento | Ambiente pausado entre invocações | Não confie em cronômetros locais ou threads |
O congelamento merece atenção: entre invocações, o ambiente fica pausado, e qualquer estado em memória pode sobreviver ou não entre chamadas. Caches in-memory funcionam como otimização oportunista, nunca como fonte de verdade, e tarefas de fundo iniciadas no handler podem ser cortadas quando a execução termina.
O empacotamento tem duas formas suportadas: pacote zip com runtime gerenciado, mais leve e mais rápido de inicializar, e imagem de contêiner até 10 GB, para dependências nativas pesadas, ao custo de cold start maior. A escolha segue o peso real das dependências, e a regra de maturidade é manter o runtime gerenciado enquanto possível e migrar para contêiner apenas quando a necessidade de artefatos nativos justificar.
Cold start e provisioned concurrency
O cold start é a latência extra da primeira execução após um ambiente novo nascer, composta por boot de microVM, inicialização do runtime e execução do código de init. Runtimes interpretados como Python e Node iniciam na casa de centenas de milissegundos; runtimes compilados e JVM pesada podem ultrapassar um segundo com pacotes grandes. Em APIs, esse custo aparece no p-99 e incomoda rotas de latência crítica.
As respostas disponíveis em produção são três e combináveis: manter o pacote magro e a inicialização preguiçosa; configurar provisioned concurrency, que mantém N ambientes pré-inicializados pagando por isso mesmo em ociosidade; e usar o SnapStart, que acelera a inicialização JVM a partir de snapshots do ambiente, com custo adicional reduzido em relação ao provisioned. O provisioned concurrency deve mirar a faixa de tráfego permanente ou de picos conhecidos, e não todo o teto de escala, sob pena de perder a vantagem econômica do modelo.
A medição honesta do cold start usa o campo de init duration nos logs estruturados do CloudWatch, que separa o tempo de inicialização do tempo de execução, e deve ser acompanhada por versão de deploy, pois novas dependências podem alterar o perfil silenciosamente.
# Provisioned concurrency para a versão publicada da função
Resources:
PedidosApiFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.handler
Runtime: python3.12
MemorySize: 1024 # 1 GB de memória e CPU proporcional
Timeout: 15
AutoPublishAlias: live
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: "10" # ambientes quentes permanentes
Memória como proxy de CPU e custo
O Lambda dimensiona CPU, rede e throughput de disco proporcionalmente à memória configurada, de aproximadamente 1 vCPU em 1.769 MB para cima. Essa equivalência transforma o dimensionamento em decisão econômica: mais memória custa mais por milissegundo, mas executa mais rápido, e o ponto ótimo frequentemente fica acima do que a intuição sugere. Dobrar a memória frequentemente reduz o tempo de execução a menos da metade em cargas de processamento, o que barateia a invocação total.
| Memória | CPU aproximada | Perfil típico |
|---|---|---|
| 128 MB | Fração de vCPU | Leitura de fila leve, scripts mínimos |
| 512 MB | Meio vCPU | APIs simples, integrações |
| 1769 MB | 1 vCPU | Equilíbrio padrão de APIs e workers |
| 3538 MB | 2 vCPU | Processamento de dados, parsing pesado |
| 10000 MB | Cerca de 6 vCPU | Workloads computacionais e Parquet grandes |
A prática consolidada é executar benchmark de custo por invocação com a carga real, varrendo a faixa de memória e escolhendo o mínimo de custo total, e não o mínimo de memória, com revisão periódica porque o perfil de código muda conforme o sistema evolui. Ferramentas de perfis de Lambda automatizam esse levantamento.
Limites: tempo, memória e concorrência
| Limite | Valor | Impacto prático |
|---|---|---|
| Timeout máximo | 15 minutos | Tarefas longas vão para Step Functions ou ECS |
| Memória | 128 MB a 10 GB | Define CPU e custo por milissegundo |
| Tamanho do pacote | 50 MB zipped, 250 MB descomprimido (250 MB em camadas por função) | Dependências pesadas em layers ou contêiner |
| Concurrency regional | 1000 por padrão (ajustável) | Pico de execução simultâneas; reserva e burst |
| Payload síncrono | 6 MB | Cargas grandes via S3 com referência |
| Tamanho da variável de ambiente | 4 KB no total | Segredos via Parameter Store ou Secrets Manager |
O limite de 15 minutos é o divisor de águas mais objetivo: qualquer processamento que possa exceder precisa ser fatiado, orquestrado com Step Functions ou movido para contêineres em ECS. O teto de concorrência compartilhado na região é o risco sistêmico clássico: uma função em loop pode esgotar a concorrência e derrubar todas as outras, o que se resolve com reserved concurrency por função crítica e monitoramento do Throttles.
Para picos planejados, a conta pode ter aumento de concorrência solicitado, e funções com provisioned concurrency consomem da cota reservada antes do pool compartilhado, o que garante previsibilidade para APIs essenciais mesmo em cenários de tempestade de execuções em outras funções da conta.
Eventos e integrações
A força prática do Lambda está nas integrações nativas: o S3 invoca funções em eventos de objeto, o SQS alimenta consumidores com batch e DLQ, o API Gateway expõe endpoints HTTP, o EventBridge roteia eventos por padrão e o DynamoDB Streams reage a mudanças de dados. Cada integração tem semântica própria de entrega, batching e retry, e desenhar para ela é parte do trabalho.
# Consumidor de fila SQS com inicialização fora do handler
import boto3
# Criado uma vez por ambiente e reaproveitado em invocações quentes
sqs = boto3.client("sqs")
DLQ = None # a DLQ é configurada na fila, não na função
def handler(event, context):
processados = []
for record in event["Records"]:
pedido = __import__("json").loads(record["body"])
# Idempotência por chave de mensagem garante segurança com retrys
if ja_processado(pedido["pedidoId"]):
continue
processa_pagamento(pedido)
registra_processado(pedido["pedidoId"])
processados.append(record["messageId"])
return {"processados": processados}
Para integração com banco relacional, o padrão RDS Proxy protege o primário de explosões de conexões, já que cada ambiente mantém pool próprio e picos de escala podem multiplicar conexões além da capacidade do banco.
Otimizações práticas de produção
- Inicialize no escopo global tudo que é caro: clients de SDK, conexões via RDS Proxy e caches de configuração.
- Minimize o pacote: dependências desnecessárias aumentam cold start e custo de deploy; layers compartilham artefatos entre funções.
- Ajuste batch size e batching window no SQS e Kinesis para equilibrar throughput e latência, com partial batch responses para não retriar lotes inteiros por uma falha isolada.
- Use structured logs e trace com X-Ray ou OpenTelemetry desde o primeiro handler, porque o diagnóstico serverless depende de tracing.
- Defina timeout realista por função e alarme de duração no p-99, pois timeout mal calibrado gera retrios em cascata.
- Controle reserved concurrency para funções críticas e monitore Throttles na conta para prever saturação regional.
- Prefira payloads pequenos com referência a S3 para cargas grandes, mantendo o limite de 6 MB fora do caminho.
Quando Lambda não é a resposta
| Situação | Por que Lambda falha | Alternativa recomendada |
|---|---|---|
| Processamento acima de 15 minutos | Limite de timeout | ECS Fargate ou Step Functions orquestrando |
| Cargas com GPU | Não há suporte a GPU | EC2 ou ECS Managed Instances |
| Tráfego constante alto 24x7 | Custo supera contêiner dedicado | ECS com EC2 reservado ou Savings Plan |
| Sistemas com estado local pesado | Ambiente efêmero e congelável | Contêiner com EFS ou armazenamento dedicado |
| Latência p99 sub-100ms com JVM fria | Cold start JVM | Provisioned concurrency ou contêiner aquecido |
| Aplicações web monolíticas tradicionais | Modelo de evento não combina | Contêineres em ECS ou App Runner |
A leitura da tabela orienta a fronteira prática: Lambda domina em eventos, integração e tráfego irregular; contêineres dominam em processamento longo, estado e tráfego constante. Arquiteturas maduras usam os dois lado a lado, e a pergunta certa por componente é sobre padrão de invocação, duração e estado, e não sobre preferência tecnológica.
Erros comuns e boas práticas
- Inicializar conexões e clients dentro do handler, pagando cold start em toda invocação; mova tudo ao escopo global.
- Ignorar idempotência em integrações com SQS e Kinesis, que entregam at-least-once; chave de idempotência é obrigatória no desenho.
- Usar Lambda para processamento que passa de minutos e descobrir o timeout em produção; esbelte e orquestre com Step Functions quando necessário.
- Deixar memória no mínimo "para economizar" e sofrer lentidão; benchmark de custo total escolhe o ponto ótimo.
- Não limitar concorrência de funções que atacam banco relacional e derrubar o primário por conexões; use RDS Proxy e reserved concurrency.
- Falta de alarme de Throttles e DLQ por invocação assíncrona; erro de entrega vira silêncio sem essas peças.
- Tratar cache in-memory como dado confiável entre invocações; é otimização, não estado.
- Versionar e empacotar sem teste de cold start em runtime de produção; meça antes e depois de cada mudança relevante de dependência.
- Misturar ambientes e reutilizar o mesmo pool de variáveis entre dev e produção; separe por função e por configuração versionada com Parameter Store.
Como prática consolidada, padronize handlers magros com infraestrutura como código, benchmark de memória por função com carga real, tracing distribuído obrigatório, alarmes de erro, throttle e duração, e um guia de decisão explícito no time para escolher entre Lambda, Cloud Run ou ECS por perfil de carga. O Lambda recompensa quem desenha para o modelo de execução e pune quem tenta escondê-lo.