Sessão 019 — Lambda: execution model, cold starts e provisioned concurrency
Duração estimada: 60 minutos
Pré-requisitos: session-004-cdk-v2-setup-bootstrap
Objetivo
Ao final, você conseguirá medir cold start de uma função com e sem provisioned concurrency, calcular o custo de provisioned concurrency vs on-demand para um perfil de tráfego específico, e identificar quais linguagens e tamanhos de package têm maior impacto em cold start.
Contexto
[FATO] Lambda é um serviço de computação serverless onde você paga apenas pelo tempo que seu código executa — não por capacidade alocada em standby. O corolário dessa garantia é que quando não há execuções ativas, não há execução environments mantidos aquecidos. A próxima invocação que chegar quando nenhum environment está disponível precisa passar pela fase de inicialização antes de executar o handler. Esse custo de latência é chamado de cold start.
[FATO] Cold starts não são um defeito do Lambda — são uma consequência direta do modelo de cobrança. O trade-off é: você economiza pagando zero quando sua função não é invocada, mas a primeira invocação após um período de inatividade (ou qualquer invocação que exige um novo execution environment por escala horizontal) tem latência adicional. Para a maioria dos workloads assíncronos, esse trade-off é aceitável. Para APIs síncronas de baixa latência com tráfego esparso, pode ser problemático.
[CONSENSO] A importância do cold start é frequentemente exagerada em discussões de comunidade. [FATO] Segundo dados da AWS, cold starts ocorrem em menos de 1% das invocações na maioria dos workloads de produção. O problema real aparece em três cenários específicos: APIs com tráfego muito esparso (uma invocação a cada 15+ minutos), funções com inicialização pesada (Spring Boot Java, modelos ML), e aplicações interativas onde P99 de latência importa mais que média.
Conceitos principais
1. O ciclo de vida do execution environment
[FATO] Um execution environment do Lambda passa por três fases:
┌─────────────────────────────────────────────────────────────────┐
│ FASE INIT (cold start — cobrada como duração) │
│ │
│ 1. Download do código/layer (da origem: S3, ECR) │
│ └─ Frequentemente chamado de "cold start" no senso estrito │
│ │
│ 2. Inicialização do runtime (Node.js, Python, JVM, etc.) │
│ │
│ 3. Execução do código de inicialização estática │
│ └─ Tudo FORA do handler: imports, conexões de DB, │
│ carregamento de modelos, inicialização de SDKs │
│ │
│ [sinaliza ready → Next API] │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ FASE INVOKE (execução do handler) │
│ │
│ handler(event, context) { ... } │
│ │
│ Cobrada por duração × memória │
└─────────────────────────────────────────────────────────────────┘
│
│ função retorna, environment fica em standby
▼
┌─────────────────────────────────────────────────────────────────┐
│ FASE SHUTDOWN (eventual) │
│ │
│ Lambda decide reciclar o environment (após inatividade) │
│ Extensions têm até 2s para finalizar │
└─────────────────────────────────────────────────────────────────┘
[FATO] A fase INIT tem um limite de 10 segundos. Se o código de inicialização (fora do handler) demorar mais de 10 segundos para completar, o Lambda tenta novamente na primeira invocação usando o timeout configurado da função. Funções com Spring Boot pesado ou carregamento de modelos ML de vários GB podem ultrapassar esse limite.
[FATO] O que acontece entre a fase INVOKE de uma invocação e a próxima é importante: o execution environment é congelado (CPU suspenso, memória mantida). Variáveis globais, conexões de banco e caches em memória persistem entre invocações quentes. Esse é o mecanismo que permite reutilização de conexões de banco de dados.
# Python: conexão de banco criada UMA VEZ na inicialização estática
# Persiste entre invocações do mesmo execution environment
import boto3
import psycopg2
# Código FORA do handler: executado apenas no cold start
db_connection = psycopg2.connect(host=os.environ['DB_HOST'], ...)
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])
def handler(event, context):
# Reutiliza db_connection e table (sem overhead de reconexão)
result = table.get_item(Key={'id': event['id']})
return result['Item']
2. Fatores que influenciam a duração do cold start
[FATO] Os principais fatores, em ordem de impacto:
Runtime/linguagem:
Tempo típico de cold start (inicialização do runtime, sem código de app):
Python 3.x: ~100-200ms
Node.js 20+: ~100-200ms
Go (custom): ~50-100ms (binário estático, sem VM)
.NET 8: ~300-600ms (com SnapStart: ~10ms)
Java 21: ~500-2000ms (com SnapStart: ~10ms)
Java Spring Boot: 3000-8000ms (sem otimizações)
Nota: esses valores são aproximações baseadas em benchmarks da comunidade
e variam com memória alocada, tamanho do package, e carga dos datacenter.
[FATO] Memória alocada tem impacto indireto no cold start: mais memória = mais CPU proporcional (Lambda aloca CPU proporcional à memória). Dobrar a memória de 512MB para 1024MB pode reduzir o tempo de inicialização estática em até 50% para funções com inicialização CPU-intensiva.
[FATO] Tamanho do deployment package impacta o tempo de download/extração durante o cold start. Referências práticas:
Package pequeno (< 5 MB): impacto mínimo (< 50ms adicional)
Package médio (5-50 MB): 50-200ms adicional
Package grande (> 50 MB): 200ms+ adicional
Container image (> 500 MB): pode adicionar 1-3s no primeiro pull
Mitigação: Lambda mantém cache do código por período de tempo não divulgado.
Pulls subsequentes ao mesmo código são muito mais rápidos.
[FATO] VPC historicamente era o maior multiplicador de cold start (adicionava 1-3 segundos para provisionar ENI). [FATO] Desde 2019, a AWS mudou a arquitetura de VPC para pre-provisionar ENIs. Cold starts em funções com VPC são agora comparáveis a funções sem VPC na maioria dos casos. O impacto residual ainda existe em contas com poucos execution environments de VPC criados recentemente.
3. Provisioned Concurrency
[FATO] Provisioned Concurrency (PC) mantém um número configurado de execution environments pré-inicializados e aquecidos, eliminando cold starts para esses environments. Quando uma invocação chega a um environment com PC, ela entra diretamente na fase INVOKE sem passar pelo INIT.
Sem PC:
Invocação 1: [INIT 800ms] + [INVOKE 50ms] = 850ms de latência
Invocação 2: [INVOKE 50ms] = 50ms (warm)
Com PC (2 environments provisionados):
Invocação 1: [INVOKE 50ms] (environment já inicializado) = 50ms
Invocação 2: [INVOKE 50ms] = 50ms
Invocação 3: [INIT 800ms] + [INVOKE 50ms] = 850ms (PC esgotado → on-demand)
[FATO] PC deve ser configurado em uma versão ou alias específico, não em $LATEST:
# ❌ Errado: $LATEST não suporta PC
aws lambda put-provisioned-concurrency-config \
--function-name my-function \
--qualifier '$LATEST' \
--provisioned-concurrent-executions 10
# ✅ Correto: versão numerada
aws lambda put-provisioned-concurrency-config \
--function-name my-function \
--qualifier 3 # versão 3
--provisioned-concurrent-executions 10
# ✅ Correto: alias
aws lambda put-provisioned-concurrency-config \
--function-name my-function \
--qualifier prod # alias 'prod'
--provisioned-concurrent-executions 10
[FATO] Custo do Provisioned Concurrency:
Cobrança de PC (us-east-1, referência):
$0.0000646234 por GB-segundo de PC alocado
$0.0000097656 por GB-segundo de invocação em PC
Cobrança on-demand (para comparação):
$0.0000200000 por GB-segundo de invocação
Exemplo: 10 PC × 1GB × 24h × 30 dias
PC alocado: 10 × 1 × 86400 × 30 × $0.0000646234 = $1,679.08/mês
Invocação em PC: depende do tráfego real
Total PC para 10 environments 1GB: ~$1,679/mês em alocação apenas!
→ PC tem custo fixo ALTO. Só é justificável se o custo do cold start
(perda de SLA, usuários impactados) for maior que esse valor.
[FATO] Uma alternativa de custo mais controlado é usar Application Auto Scaling para ajustar o PC dinamicamente com base no horário:
# Escalar PC para 10 das 8h às 18h (horário comercial), 2 fora do horário
aws application-autoscaling register-scalable-target \
--service-namespace lambda \
--resource-id function:my-function:prod \
--scalable-dimension lambda:function:ProvisionedConcurrency \
--min-capacity 2 \
--max-capacity 10
4. SnapStart: cold starts para Java e .NET
[FATO] SnapStart é uma feature do Lambda que captura um snapshot do execution environment após a fase INIT e o reutiliza para novas invocações, eliminando o tempo de inicialização do runtime e do código estático. Em vez de inicializar a JVM e carregar todas as classes a cada cold start, o Lambda restaura o estado de memória e disco do snapshot em milissegundos.
Sem SnapStart (Java Spring Boot):
Deploy nova versão → [JVM init: 1s] + [Spring init: 5s] + [handler: 100ms]
Cold start total: ~6.1 segundos
Com SnapStart:
Deploy nova versão → Lambda faz INIT e tira snapshot
Invocação cold → [restaura snapshot: ~10ms] + [handler: 100ms]
Cold start total: ~110ms
[FATO] Limitações do SnapStart (maio 2026):
- Disponível apenas para Java (managed runtimes) e .NET 8+.
- Não suporta funções em container image (apenas deployment package).
- Não suporta funções com ephemeralStorage > 512MB.
- O snapshot é tirado por versão — cada PublishVersion gera um novo snapshot.
- Código que usa timestamps ou randoms no INIT pode ter comportamento inesperado ao ser restaurado (o tempo "congela" no snapshot).
[FATO] SnapStart tem custo zero de configuração. A cobrança é pelo tempo de invocação normal (não há custo de "alocação" como no PC).
Exemplo prático
Cenário: Você tem uma API REST com Lambda + API Gateway. A função usa Node.js e faz conexão com RDS PostgreSQL. O tráfego é irregular: picos de 500 req/s durante horário comercial e quase zero à noite. O P99 de latência deve ser < 200ms.
Medindo cold start no CloudWatch
# Filtrar invocações com Init Duration nos logs do Lambda
# (Init Duration aparece apenas em cold starts)
aws logs filter-log-events \
--log-group-name /aws/lambda/my-api-function \
--filter-pattern '"Init Duration"' \
--start-time $(date -d '1 hour ago' +%s)000 \
| jq '.events[].message' \
| grep -o 'Init Duration: [0-9.]*'
# Saída típica:
# Init Duration: 823.45
# Init Duration: 791.12
# Init Duration: 1203.78
CloudWatch Insights para análise de cold starts em escala:
-- Query CloudWatch Insights: distribuição de cold starts por hora
filter @message like /Init Duration/
| parse @message "Init Duration: * ms" as initDuration
| stats
count(*) as coldStarts,
avg(initDuration) as avgInit,
pct(initDuration, 95) as p95Init,
max(initDuration) as maxInit
by bin(1h)
| sort by bin(1h) desc
Calculando custo: PC vs on-demand para um perfil de tráfego
Perfil de tráfego:
8h-18h (10h/dia × 22 dias úteis = 220h/mês): 100 invocações/s
Resto (530h/mês): 2 invocações/s
Função: 1GB memória, 100ms de execução média
On-demand (sem PC):
Total invocações/mês:
100 req/s × 220h × 3600s = 79,200,000 invocações em pico
2 req/s × 530h × 3600s = 3,816,000 invocações fora do pico
Total: 83,016,000 invocações
Custo GB-segundos: 83,016,000 × 0.1s × 1GB = 8,301,600 GB-s
Custo invocações: $0.20/M × 83.016M = $16.60
Custo GB-s: $0.0000200000 × 8,301,600 = $166.03
Total on-demand: ~$182.63/mês
(+ cold starts em ~1% das invocações = ~830,000 cold starts)
Com PC de 40 environments (pico de 100 req/s × 100ms = 10 concurrent + margem):
PC allocation: 40 × 1GB × 720h = 28,800 GB-h = 103,680,000 GB-s
Custo PC alocado: 103,680,000 × $0.0000646234 = $6,698/mês
→ PC é MUITO mais caro para esse perfil.
Com PC de 5 environments (garante resposta rápida para tráfego baixo):
PC allocation: 5 × 1GB × 720h × 3600s/h = 12,960,000 GB-s
Custo PC: 12,960,000 × $0.0000646234 = $837/mês
→ Ainda mais caro que on-demand. PC só compensa se o SLA
exigir que TODOS os cold starts sejam eliminados, não apenas
os de baixo tráfego.
Conclusão para esse perfil:
Melhor estratégia: otimizar o código estático para reduzir cold start
(conexão lazy de DB, remover dependências não usadas)
+ aceitar cold starts ocasionais em troca de custo 10x menor.
CDK com Provisioned Concurrency via alias
import { Stack, Duration } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as appscaling from 'aws-cdk-lib/aws-applicationautoscaling';
// Função
const fn = new lambda.Function(this, 'ApiFunction', {
runtime: lambda.Runtime.NODEJS_22_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
memorySize: 1024,
timeout: Duration.seconds(30),
});
// Publica versão imutável (necessária para PC)
const version = fn.currentVersion;
// Alias 'prod' aponta para a versão atual
const prodAlias = new lambda.Alias(this, 'ProdAlias', {
aliasName: 'prod',
version,
provisionedConcurrentExecutions: 5, // PC configurado no alias
});
// Auto Scaling do PC baseado em agendamento
const target = prodAlias.addAutoScaling({
minCapacity: 2,
maxCapacity: 20,
});
// Escala para 10 às 8h, volta para 2 às 18h (UTC-3 = UTC+3 invertido)
target.scaleOnSchedule('ScaleUpMorning', {
schedule: appscaling.Schedule.cron({ hour: '11', minute: '0' }), // 8h BRT
minCapacity: 10,
});
target.scaleOnSchedule('ScaleDownEvening', {
schedule: appscaling.Schedule.cron({ hour: '21', minute: '0' }), // 18h BRT
minCapacity: 2,
});
Armadilhas comuns
Armadilha 1: Conexões de banco de dados criadas dentro do handler
O erro: O código cria uma nova conexão de banco a cada invocação:
def handler(event, context):
# ❌ Conexão criada DENTRO do handler
conn = psycopg2.connect(host=DB_HOST, ...)
result = conn.execute("SELECT ...")
conn.close()
return result
Por que acontece: O desenvolvedor não conhece o modelo de execution environment ou está preocupado com conexões "stale" em ambientes warm.
O custo: Uma conexão TCP + TLS para RDS leva 50-200ms. Para uma função que executa em 10ms, isso é um overhead de 500-2000%. Com 1000 invocações/minuto, isso cria e destrói 1000 conexões/minuto no banco, potencialmente esgotando o max_connections do RDS.
Como evitar: Crie conexões no escopo global (fora do handler). Use RDS Proxy para gerenciar o pool de conexões no lado do banco — ele agrupa as conexões de múltiplos execution environments em um pool menor para o RDS.
# ✅ Conexão criada FORA do handler (inicialização estática)
conn = None
def get_connection():
global conn
if conn is None or conn.closed:
conn = psycopg2.connect(host=DB_HOST, ...)
return conn
def handler(event, context):
c = get_connection()
result = c.execute("SELECT ...")
return result
Armadilha 2: Provisioned Concurrency em $LATEST
O erro: O pipeline configura PC na versão $LATEST da função:
aws lambda put-provisioned-concurrency-config \
--function-name my-function \
--qualifier '$LATEST' \ # ← isso falha!
--provisioned-concurrent-executions 5
Por que acontece: $LATEST é um alias especial que aponta para o código mais recente. É conveniente para desenvolvimento mas não suporta PC porque é mutável — o Lambda não pode manter um snapshot de algo que muda a cada deploy.
Como reconhecer: Erro InvalidParameterValueException: Provisioned Concurrency Configurations are not supported on unpublished versions. O cold start continua acontecendo mesmo depois de configurar PC.
Como evitar: Sempre publique uma versão (PublishVersion) antes de configurar PC, e aplique PC na versão numerada ou em um alias que aponte para ela. Em CDK, use fn.currentVersion que publica automaticamente uma versão imutável.
Armadilha 3: Imports globais desnecessários aumentando cold start
O erro: A função importa bibliotecas completas quando só usa uma função de cada:
# ❌ Importa o SDK inteiro na inicialização
import boto3
import pandas as pd
import numpy as np
from PIL import Image
def handler(event, context):
# Só usa S3 e uma operação de JSON
s3 = boto3.client('s3')
# pandas, numpy, PIL nunca são usados nesta função
Por que acontece: Copiar código de outro lugar sem revisar imports, ou manter dependências de uma versão anterior da função que foi simplificada.
O custo: pandas + numpy juntos podem adicionar 300-500ms ao cold start apenas pelo import. Uma função que executa em 50ms pode ter cold start de 800ms por causa de imports não utilizados.
Como evitar: Audite os imports regularmente. Use imports lazy (dentro do handler) para bibliotecas usadas raramente. Para Node.js, use bundlers (esbuild via NodejsFunction do CDK) que fazem tree-shaking e eliminam código não utilizado do bundle final.
Exercício de reflexão
Você tem três funções Lambda com perfis distintos:
-
auth-validator: Node.js, 128MB, executa em 5ms, invocada 10.000 vezes/minuto de forma constante 24h/dia. Cold start atual: 200ms.
-
report-generator: Java Spring Boot, 3GB, executa em 8s, invocada 2 vezes/hora, apenas durante horário comercial. Cold start atual: 6s.
-
image-resizer: Python, 512MB, executa em 300ms, invocada em bursts de 0 para 500 req/s em menos de 1 minuto (campanhas de marketing imprevisíveis). Cold start atual: 400ms.
Para cada função, decida: o cold start é um problema real no contexto de uso? Se sim, qual estratégia você usaria — otimização de código, Provisioned Concurrency, SnapStart, ou outra abordagem? Calcule (aproximadamente) o custo mensal de cada estratégia escolhida e compare com o custo de não fazer nada (quantos usuários afetados, qual impacto no SLA).
Recursos para aprofundar
1. Understanding the Lambda execution environment lifecycle
URL: https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html
O que encontrar: Descrição detalhada das fases Init, Invoke e Shutdown, o limite de 10 segundos da fase Init, o comportamento de reuse de execution environments, e como extensões interagem com o ciclo de vida.
Por que é a fonte certa: É a documentação primária do modelo de execução — base para entender todos os outros conceitos desta sessão.
2. Configuring provisioned concurrency for a function
URL: https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html
O que encontrar: Como configurar PC em versões e aliases, a diferença de cobrança entre PC alocado e invocação em PC, como o comportamento muda quando o PC é esgotado (fallback para on-demand), e como usar Application Auto Scaling para ajustar PC dinamicamente.
Por que é a fonte certa: É a referência oficial do feature, com todos os detalhes de configuração e custo.
3. Optimizing static initialization
URL: https://docs.aws.amazon.com/lambda/latest/dg/static-initialization.html
O que encontrar: Boas práticas para reduzir o tempo de inicialização estática (fora do handler): lazy initialization, cache de objetos pesados, medição com Init Duration nos logs, e exemplos por linguagem.
Por que é a fonte certa: É o guia prático de otimização de cold start sem necessidade de PC — a primeira linha de defesa antes de considerar provisioned concurrency.