O nome "serverless" é marketing bem-sucedido: servidores existem, você simplesmente não os vê. O que muda é o modelo de responsabilidade — você entrega código de uma função, o provider executa esse código em resposta a eventos, e você paga exatamente pelo tempo de execução. A unidade de deploy deixa de ser um serviço persistente e passa a ser uma função efêmera que vive apenas durante a execução.
AWS Lambda foi lançado em 2014 e popularizou o modelo. Em 2024, Lambda executa trilhões de requisições por mês para milhões de clientes. O modelo claramente escala — mas o que os casos de sucesso não mostram com clareza são os trade-offs que aparecem quando você ultrapassa os casos de uso ideais.
O modelo de execução serverless
O ciclo de vida de uma invocação Lambda é:
- Init: o provider baixa o código (ou container image), inicializa o ambiente de execução (runtime, JVM, Python interpreter), e executa o código de inicialização fora do handler
- Invoke: o handler recebe o evento e produz uma resposta
- Shutdown: após um período de inatividade (tipicamente 5-15 minutos), o ambiente é destruído
O estado sobrevive apenas durante a fase de Invoke e no período em que o ambiente permanece "aquecido" (entre invocações do mesmo ambiente). Variáveis globais e conexões de banco criadas fora do handler persistem entre invocações do mesmo ambiente — mas não entre ambientes diferentes ou após reinicialização. Isso cria uma peculiaridade: você pode ter estado acidental em Lambda, mas não pode depender dele.
Cold start — o problema que ninguém quer ter
Cold start é a latência de inicialização de um novo ambiente de execução. Acontece quando:
- Primeira invocação após deploy
- Invocação após o ambiente ter sido destruído por inatividade
- Aumento de concorrência além dos ambientes já aquecidos (burst de tráfego)
O cold start tem três componentes:
| Componente | Duração típica | O que causa |
|---|---|---|
| Infrastructure init | 10-100ms | Alocação de container/VM e download do código |
| Runtime init | 10ms - 500ms | Python: 10ms; Node.js: 50ms; JVM: 200-500ms |
| Function init | 0ms - vários segundos | Código fora do handler: imports, conexões, warmup de cache |
Uma função Python simples tem cold start de 50-100ms — irrelevante. Uma função Java com Spring Boot pode ter cold start de 5-10 segundos — catastrófico para APIs síncronas onde SLA p99 é de 200ms.
Mitigando cold start
Provisioned Concurrency: mantém N ambientes pré-inicializados e aquecidos, eliminando cold start para aqueles N ambientes. Custo: você paga pelas instâncias aquecidas mesmo quando não há invocações — o mesmo modelo de custo de containers.
resource "aws_lambda_provisioned_concurrency_config" "api" {
function_name = aws_lambda_function.api.function_name
qualifier = aws_lambda_alias.api_live.name
provisioned_concurrent_executions = 10 # 10 ambientes sempre aquecidos
}
SnapStart (Lambda para Java): tira um snapshot do estado do ambiente após a inicialização, e restaura do snapshot em vez de reinicializar. Reduz cold start de JVM de segundos para centenas de milissegundos. Não é gratuito: o snapshot tem restrições (não pode usar state que muda entre invocações, como random seeds ou IDs).
Keep-warm com EventBridge: acionar a função a cada 5 minutos com um evento de ping. Evita destruição do ambiente por inatividade. Gambiarra funcional para baixo volume — para alto volume, provisioned concurrency é mais correto.
Otimizar o código de inicialização: mover imports pesados para dentro do handler (lazy loading), usar connection pooling para banco (RDS Proxy para Lambda), e preferir runtimes com cold start menor (Node.js, Python, Go > Java, .NET).
Limites que importam
| Limite | Valor | Impacto |
|---|---|---|
| Timeout máximo | 15 minutos | Impossível usar Lambda para processamento longo (ETL de horas) |
| Memória máxima | 10.240 MB | Suficiente para maioria dos casos; CPU escala proporcionalmente à memória |
| Payload de evento (sincrônico) | 6 MB | Impossível processar arquivos grandes diretamente — use S3 + presigned URL |
| Payload de resposta (sincrônico) | 6 MB | APIs que retornam dados grandes precisam de outra abordagem |
| Concorrência padrão por região | 1.000 | Burst de tráfego pode bater no limite e causar throttling (429) |
| Duração de /tmp | 512 MB - 10 GB | Storage efêmero dentro da invocação — não persiste entre invocações |
O limite de concorrência de 1.000 por região é o que pega mais times desprevenidos: se você tem 10 Lambdas e um deles recebe um burst de 900 invocações simultâneas, os outros 9 podem ficar sem concorrência disponível e retornar throttling. Reserve concorrência explicitamente por função crítica:
resource "aws_lambda_function_event_invoke_config" "api" {
function_name = aws_lambda_function.api.function_name
maximum_retry_attempts = 0 # para APIs síncronas — não retentar em erro
}
# Reservar concorrência para função crítica
resource "aws_lambda_provisioned_concurrency_config" "api_reserved" {
function_name = aws_lambda_function.api.function_name
qualifier = "$LATEST"
# reserved_concurrent_executions — bloqueia X para essa função
}
Vendor lock-in real em serverless
O código de uma função Lambda é portável: é uma função que recebe um evento e retorna uma resposta. Mas o ecossistema ao redor não é:
- Triggers: SQS, SNS, EventBridge, DynamoDB Streams, S3 Events — cada um tem formato de evento específico da AWS. O código que processa um DynamoDB Stream Event tem que conhecer a estrutura do evento AWS.
- Permissões: IAM roles e políticas são específicas da AWS
- Observabilidade: X-Ray para tracing, CloudWatch Logs Insights para consulta — não portável
- State: Step Functions para orquestração de workflows — proprietário
- API Gateway: autorizers, rate limiting, CORS — específico da AWS
Uma arquitetura serverless madura na AWS é tão locked-in quanto possível — cada serviço integrado adiciona camada de integração proprietária. Isso não é necessariamente ruim: a produtividade de integrar SQS + Lambda + DynamoDB é genuinamente alta. Mas é uma decisão consciente, não uma consequência ignorada.
A forma mais eficaz de reduzir lock-in sem perder produtividade é isolar a lógica de negócio do handler AWS. O handler faz apenas o parse do evento e chama código puro — o código puro pode ser testado sem AWS e potencialmente portado:
# Python — handler isolado do negócio
def lambda_handler(event, context):
# Parsing específico do AWS event — lock-in aqui
records = [parse_sqs_record(r) for r in event["Records"]]
# Lógica de negócio — portável, testável sem AWS
results = process_orders(records)
return {"statusCode": 200, "body": json.dumps(results)}
Análise econômica — quando serverless compensa
Lambda cobra por número de invocações ($0,20 por 1M no us-east-1) e duração de execução ($0,0000166667 por GB-segundo). Para carga constante, containers geralmente ganham na comparação de custo:
Exemplo: API que processa 1.000 req/hora de forma constante, Lambda de 128MB usando 100ms por invocação:
- Lambda: 720.000 invocações/mês × $0,20/M + 720.000 × 0,1s × 0,128 GB × $0,0000166667/GB-s ≈ $0,29/mês
- Fargate (0,25 vCPU, 0,5 GB): ~$32/mês
Lambda vence por enorme margem em baixo volume. Mas com 100.000 req/hora:
- Lambda: $14 + $15 = $29/mês
- ECS com EC2 spot + autoscaling: $15-40/mês dependendo de instâncias
No alto volume, Lambda e containers ficam comparáveis — e containers ganham em previsibilidade de custo e ausência de cold start. A regra prática: Lambda vence economicamente para tráfego com picos extremos ou volume baixo-médio (<10M req/mês); containers vencem para carga constante e alta.
Event-driven patterns com Lambda
Os patterns mais comuns onde Lambda brilha:
Fan-out com SNS → Lambda: um evento publicado no SNS dispara múltiplas funções Lambda em paralelo. Processamento de um pedido: uma função atualiza o inventário, outra envia confirmação por email, outra notifica o sistema de logística.
Queue worker com SQS → Lambda: Lambda consome mensagens de uma fila SQS. Escalamento automático baseado no tamanho da fila. Ideal para processamento de imagens, envio de emails em batch, ou qualquer workload que pode ser processado de forma assíncrona.
# SAM template — Lambda consumindo SQS com falha parcial
ProcessorFunction:
Type: AWS::Serverless::Function
Properties:
Handler: processor.handler
Runtime: python3.12
Events:
SQSTrigger:
Type: SQS
Properties:
Queue: !GetAtt MinhaFila.Arn
BatchSize: 10 # processa até 10 mensagens por invocação
FunctionResponseTypes:
- ReportBatchItemFailures # falha parcial — não reprocessa itens que já funcionaram
Scheduled jobs com EventBridge: substituição de cron jobs. Limpar banco de dados às 2h, gerar relatório diário, verificar certificados que vencem. Sem servidor dedicado para cron.
Orquestração com Step Functions: para workflows com múltiplos passos, lógica de retry, e ramificações condicionais, Step Functions orquestra Lambdas com estado persistido entre passos — elimina o problema de coordenação entre funções.
APIs síncronas com SLA de latência agressiva (p99 < 50ms): cold start é incompatível. Workloads com estado complexo: funções sem estado são uma restrição arquitetural real. Processamento de longa duração (>15 minutos): use ECS Task ou Step Functions com atividades. Carga constante e alta: o custo por request constante favorece containers. Migração de monolito: lambdificar uma aplicação monolítica existente cria distributed systems complexity sem ganho proporcional.
Decisões de engenharia
Perguntas de entrevista
O que é cold start em Lambda, quais são as causas e como cada estratégia de mitigação funciona?
Cold start é a latência adicional incorrida quando Lambda precisa inicializar um novo ambiente de execução — baixar o código, inicializar o runtime (Python, JVM, Node.js), e executar o código fora do handler. Acontece em três situações: primeiro deploy, inatividade (o ambiente é destruído após ~15min sem invocações), e burst de concorrência além dos ambientes já aquecidos.
Os três componentes têm magnitudes diferentes: infrastructure init (10-100ms, controlado pelo provider), runtime init (Python 10ms, Node.js 50ms, JVM 200-500ms — escolha do runtime impacta aqui), e function init (código fora do handler — imports pesados, conexões de banco, warmup de caches).
Estratégias de mitigação: (1) Provisioned Concurrency — mantém N ambientes pré-aquecidos, zero cold start para esses N slots, custo proporcional ao N configurado; (2) SnapStart para Java — snapshot do estado pós-init, restauração em <1s em vez de 5-10s, sem custo adicional mas com restrições; (3) Keep-warm com EventBridge — ping a cada 5min evita shutdown por inatividade, gambiarra que não escala com burst; (4) otimização do código de init — lazy imports, RDS Proxy para connection pooling, runtime mais leve.
Como o limite de concorrência de 1.000 por região pode causar throttling e como evitar que uma função prejudique as outras?
O limite de concorrência de 1.000 é um limite de conta por região — compartilhado entre todas as funções Lambda na conta. Se você tem 20 funções e uma delas recebe um burst de 950 invocações simultâneas, as outras 19 ficam com apenas 50 slots de concorrência disponíveis. Se uma segunda função também receber tráfego nesse momento, começa a retornar 429 TooManyRequests — throttling causado por outra função, não pelo seu próprio volume.
Dois mecanismos para evitar isso: (1) Reserved Concurrency — reserva um número máximo de slots para uma função específica. Uma função com reserved_concurrent_executions = 100 nunca usa mais que 100 slots, mas também nunca usa os 900 restantes se ficar ociosa. (2) Provisioned Concurrency — reserva slots pré-aquecidos para funções críticas, garantindo que elas sempre terão capacidade.
A prática recomendada é reservar concorrência para as funções críticas de negócio (APIs de pagamento, autenticação) e deixar funções de processamento de background sem reserva — se o batch throttle, ele espera na fila; se a API de pagamento throttle, usuários veem erro.
Por que o vendor lock-in em serverless é maior do que parece, e como isolá-lo na arquitetura?
O código da função em si é portável — é uma função Python ou Go com um handler. O lock-in real está no ecossistema ao redor: os eventos têm formatos proprietários (um SQS event tem estrutura AWS-específica com Records[].body, messageId, etc.), os triggers são serviços AWS (SQS, SNS, EventBridge, DynamoDB Streams), as permissões são IAM, o tracing é X-Ray, e a orquestração é Step Functions. Uma função que processa DynamoDB Streams diretamente é tão locked-in quanto possível — ela entende o formato de mudança do DynamoDB, as IAM policies para ler o stream, e os eventos de erro do Lambda.
A estratégia de isolamento é separar o adapter (que conhece AWS) do domínio (que não conhece): o handler desserializa o evento AWS-específico, extrai os dados relevantes, e chama código puro de domínio. O código de domínio não importa nada da AWS SDK, recebe structs simples, e pode ser testado com go test ou pytest sem mocks da AWS. Se um dia você migrar para GCP Cloud Functions, você reescreve apenas o adapter — não o domínio.
Essa separação também melhora testabilidade: 90% do código pode ser testado com testes unitários rápidos sem LocalStack ou AWS real. O handler em si fica tão fino que um teste de integração é suficiente para cobri-lo.
Como o modelo econômico de Lambda funciona e em que ponto containers se tornam mais baratos?
Lambda cobra em duas dimensões: número de invocações ($0,20 por 1 milhão) e duração em GB-segundos ($0,0000166667 por GB-s). Uma função de 128MB que executa em 100ms custa $0,0000000021 por invocação — praticamente gratuito em baixo volume. O free tier inclui 1 milhão de invocações e 400.000 GB-segundos por mês.
A equação muda com volume alto e execução contínua. Uma função que processa 10 milhões de requisições por mês com 500ms de duração em 1GB custa: $2 (invocações) + 10M × 0,5s × 1GB × $0,0000166667 = $2 + $83 = $85/mês. Um container Fargate com 1 vCPU e 2GB processando o mesmo volume (supondo que baste um container com uso de 60%) custaria: $0,04048 × 720h + $0,004445 × 720h × 2 = $29 + $6 = $35/mês.
O break-even depende da utilização do container: Lambda vence quando o tráfego é muito variável (Lambda não cobra quando idle, Fargate cobra 24h/dia). Containers vencem quando a função está sempre sendo invocada — o custo de Lambda cresce linearmente com invocações, enquanto o container tem custo fixo mais alto mas por request menor. A regra prática: acima de ~5-10 req/segundo constante com duração >100ms, containers começam a ser comparáveis ou mais baratos.
Qual é o padrão correto para processar mensagens SQS com Lambda e como implementar falha parcial de batch?
Quando Lambda processa um batch de SQS, o comportamento padrão é binário: ou todas as mensagens são processadas com sucesso, ou o batch inteiro falha e todas as mensagens voltam para a fila (e eventualmente vão para a DLQ após MaxReceiveCount tentativas). Isso cria um problema: se 9 de 10 mensagens processam com sucesso mas a décima falha, as 9 são reprocessadas desnecessariamente.
O padrão ReportBatchItemFailures resolve isso: a função retorna uma lista de itemIdentifier (messageIds) que falharam, e apenas essas mensagens voltam para a fila. As demais são deletadas. A implementação exige que a função capture erros por mensagem individualmente (não deixe uma exceção propagar para o nível do handler, pois isso faria todo o batch falhar) e construa a lista de falhas:
def lambda_handler(event, context):
failures = []
for record in event["Records"]:
try:
process_message(record["body"])
except Exception as e:
failures.append({"itemIdentifier": record["messageId"]})
return {"batchItemFailures": failures}
Combine com uma DLQ na fila SQS: após MaxReceiveCount falhas, a mensagem problemática vai para a DLQ onde pode ser inspecionada. Nunca configure MaxReceiveCount = 1 — uma falha transitória vai para a DLQ imediatamente, sem chance de retry.
Exercícios práticos
Implemente um processador de mensagens SQS com Lambda usando SAM ou Terraform. Configure: BatchSize de 10 mensagens, ReportBatchItemFailures habilitado, DLQ com MaxReceiveCount de 3 tentativas. Implemente o handler para processar cada mensagem individualmente, capturando exceções por item e retornando apenas os messageIds que falharam. Simule falha em 2 das 10 mensagens e verifique que apenas as 2 voltam para a fila e as outras 8 são deletadas.
Implemente uma Lambda simples em Java (ou qualquer runtime com cold start mensurável) com logging do timestamp de início do init e do handler. Use X-Ray ou CloudWatch Logs para capturar a duração do init. Invoque a função depois de um período de inatividade para capturar cold starts. Configure Provisioned Concurrency de 2 instâncias, aguarde a inicialização, e repita as invocações. Compare as distribuições de latência (init duration e total duration) com e sem PC usando CloudWatch Insights.
Critério: Gráfico ou tabela com p50/p95/p99 de latência total em dois cenários: sem PC (cold starts visíveis) e com PC (init duration = 0 para as instâncias aquecidas); o relatório quantifica a economia de latência e o custo adicional do PC.Implemente um sistema de processamento de pedidos usando fan-out: uma função order-received publica um evento no SNS quando um pedido é criado. Configure três subscriptions no SNS para três Lambdas diferentes: update-inventory (mock: decrementa quantidade em DynamoDB), send-confirmation (mock: loga o email de confirmação), e notify-logistics (mock: loga a notificação para sistema de entrega). Cada Lambda deve processar de forma independente e falhar de forma isolada.
notify-logistics não impede update-inventory de completar; cada função tem sua própria DLQ para mensagens que falharam após retries.
Crie um scheduled job usando EventBridge que roda toda segunda-feira às 8h UTC. A função Lambda deve: listar objetos no S3 criados há mais de 30 dias em um bucket de logs temporários, filtrar os que têm prefixo temp/, e deletar os encontrados. Implemente idempotência — a função deve ser segura para rodar múltiplas vezes (se deletar um arquivo que não existe, não deve falhar). Configure dead letter queue para o EventBridge rule e alarme no CloudWatch se a função falhar.
temp/ são deletados; arquivos recentes são preservados; a execução é registrada em CloudWatch Logs com contagem de arquivos deletados.
Refatore uma Lambda existente (ou escreva uma nova) que processa eventos SQS separando claramente o adapter (handler que parseia o evento AWS) do domínio (lógica de negócio). O código de domínio não deve importar nada de SDKs AWS. Escreva testes unitários para o domínio sem mocks da AWS (usando structs simples como input). Escreva um teste de integração para o handler usando events.SQSEvent com um evento simulado. Meça a cobertura de teste: o objetivo é >80% de cobertura no código de domínio com testes unitários puros.
Referências
- book Yan Cui — Production-Ready Serverless
- docs AWS Lambda — Operator Guide
- article Corey Quinn — The Hidden Costs of Serverless
- docs AWS — Lambda SnapStart for Java
- docs AWS Lambda Powertools — Python, TypeScript, Java, Go
- docs AWS SAM — Serverless Application Model
- docs AWS Step Functions — Developer Guide
- article Yan Cui — You are wrong about serverless vendor lock-in
- article Lumigo — The Complete Guide to AWS Lambda Cold Starts
- article AWS — Reporting batch item failures for Lambda with SQS
- video AWS re:Invent 2023 — Lambda internals: under the hood
- article CNCF — Serverless Landscape and Best Practices