MÓDULO 12 · CONCEITO 07 DE 12

Serverless — Lambda, Cloud Functions e os trade-offs que ninguém conta

Modelo de execução serverless. Cold start: causas, mitigações (provisioned concurrency, SnapStart). Limites que importam. Vendor lock-in real. Quando serverless é economicamente vantajoso. Event-driven patterns.

Tempo de leitura ~20 min Pré-requisito 01 · Modelos de Cloud · 06 · FinOps Próximo 08 · Multi-cloud →

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 é:

  1. 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
  2. Invoke: o handler recebe o evento e produz uma resposta
  3. 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:

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 é:

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 vence por enorme margem em baixo volume. Mas com 100.000 req/hora:

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.

quando não usar serverless

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

Lambda vs Fargate vs EC2 — quando cada um vence
Lambda para: workloads event-driven (SQS consumer, S3 trigger, scheduled jobs), APIs com tráfego muito variável ou baixo volume (<10M req/mês), automações e glue code entre serviços AWS. Fargate para: APIs síncronas com SLA agressivo onde cold start é inaceitável, workloads com conexões de banco persistentes que não toleram re-init frequente, serviços que precisam de armazenamento local temporário >10GB. EC2 com autoscaling para: cargas previsíveis e altas onde Savings Plans garantem custo mínimo, workloads que precisam de acesso a hardware específico (GPU, storage NVMe local), e quando o overhead de Fargate ou Lambda é o gargalo.
Provisioned Concurrency vs SnapStart vs keep-warm
Provisioned Concurrency para funções que atendem tráfego síncrono com SLA de latência — paga por N ambientes pré-aquecidos continuamente, mas garante zero cold start para aqueles N slots. Use Application Auto Scaling para ajustar a quantidade de PC por horário (reduzir à noite). SnapStart apenas para Java — reduz cold start de 5-10s para <1s sem custo adicional além do init; não resolve todos os casos (restrições em estado com efeitos colaterais). Keep-warm com EventBridge ping a cada 5 min é uma gambiarra razoável para funções de baixo custo onde PC seria desproporcional — não escala com burst.
Lambda + API Gateway vs Lambda Function URL vs ALB
API Gateway quando precisar de: authorizers customizados (JWT, Cognito), rate limiting por usuário/API key, transformação de request/response, WAF integrado, e modelos de uso com throttling granular. Lambda Function URL para: protótipos, webhooks simples, e APIs internas onde a funcionalidade do API Gateway seria overhead — é mais barato e mais simples. ALB como trigger quando a Lambda já está atrás de um ALB existente (evita duplicar custos de API Gateway), ou quando precisa de routing baseado em path/host entre Lambda e containers no mesmo ALB.
Step Functions vs Lambda encadeadas manualmente
Step Functions para workflows com mais de 2-3 passos sequenciais, lógica de retry com backoff, branches condicionais, e necessidade de auditoria de estado entre passos. O mapa de execução visual é genuinamente valioso para debug de workflows complexos. Lambda encadeadas (Lambda A invoca Lambda B diretamente) para: chamadas simples sem lógica de orchestration, quando a latência adicional do Step Functions é inaceitável (<50ms por step), e quando o workflow é linear sem branches. Nunca use Lambda síncrona para encadear mais de 3 passos — a cadeia se torna impossível de debugar e o tratamento de erro fica distribuído por todas as funções.

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

01 · Lambda + SQS com ReportBatchItemFailures e DLQ

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.

Critério: Com 10 mensagens no batch onde 2 falham, apenas as 2 mensagens com falha aparecem novamente na fila após processamento; as 8 mensagens bem-sucedidas são deletadas; após 3 tentativas, as mensagens problemáticas chegam na DLQ.
02 · Medir cold start com e sem Provisioned Concurrency

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.
03 · Fan-out event-driven com SNS e múltiplos Lambdas

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.

Critério: Um evento publicado no SNS é processado pelas três funções em paralelo; uma falha em notify-logistics não impede update-inventory de completar; cada função tem sua própria DLQ para mensagens que falharam após retries.
04 · Scheduled job com EventBridge para cleanup periódico

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.

Critério: A função executa no horário configurado; arquivos com mais de 30 dias no prefixo temp/ são deletados; arquivos recentes são preservados; a execução é registrada em CloudWatch Logs com contagem de arquivos deletados.
05 · Isolar lógica de negócio do handler AWS para testabilidade

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.

Critério: Os testes de domínio rodam sem credenciais AWS, sem LocalStack, e sem internet; a cobertura de domínio é superior a 80% com testes unitários; o handler tem apenas um teste de integração que valida o parsing do evento e o retorno correto.

Referências

  1. book Yan Cui — Production-Ready Serverless Manning · 2022 · patterns e anti-patterns de Lambda em produção por especialista da comunidade
  2. docs AWS Lambda — Operator Guide docs.aws.amazon.com/lambda/latest/operatorguide · guia de operação de Lambda em produção
  3. article Corey Quinn — The Hidden Costs of Serverless lastweekinaws.com · análise de custo real de serverless em diferentes padrões de uso
  4. docs AWS — Lambda SnapStart for Java docs.aws.amazon.com · mecanismo de snapshot para redução de cold start em JVM
  5. docs AWS Lambda Powertools — Python, TypeScript, Java, Go docs.powertools.aws.dev · biblioteca oficial para tracing, logging estruturado, idempotência e validação em Lambda
  6. docs AWS SAM — Serverless Application Model docs.aws.amazon.com/serverless-application-model · framework oficial AWS para definir e fazer deploy de aplicações serverless
  7. docs AWS Step Functions — Developer Guide docs.aws.amazon.com/step-functions · orquestração de workflows com Lambda, retry, branching e estado persistido
  8. article Yan Cui — You are wrong about serverless vendor lock-in theburningmonk.com · análise do lock-in real vs percebido e estratégias de isolamento de domínio
  9. article Lumigo — The Complete Guide to AWS Lambda Cold Starts lumigo.io/blog · benchmark de cold start por runtime, tamanho de função e estratégias de mitigação
  10. article AWS — Reporting batch item failures for Lambda with SQS docs.aws.amazon.com · implementação de ReportBatchItemFailures para processamento parcial de batch
  11. video AWS re:Invent 2023 — Lambda internals: under the hood youtube.com · deep dive em como Lambda gerencia ambientes de execução, concorrência e SnapStart
  12. article CNCF — Serverless Landscape and Best Practices cncf.io/blog · visão geral do ecossistema serverless além da AWS, incluindo Knative e OpenFaaS