Serverless na AWS: Quando Vale a Pena e Quando é Cilada
Minha experiência real com Lambda, API Gateway e DynamoDB. Onde serverless brilha, onde complica, e quanto realmente custa em produção.
Todo mundo fala que serverless é o futuro. Depois de usar em produção por um bom tempo, posso dizer: depende. Tem cenário que é perfeito, tem cenário que vira pesadelo.
Onde Serverless Brilha
Vou direto ao ponto - serverless faz sentido quando:
- Tráfego imprevisível - Escala de 0 a milhares sem você fazer nada
- Workloads esporádicos - Paga só quando executa
- Eventos e integrações - Reagir a upload S3, mensagem SQS, etc
- APIs simples - CRUD sem muita lógica complexa
Já usei pra processar webhooks de pagamento. Tráfego zero a maior parte do tempo, picos quando tinha campanha. Custo total: menos de $5/mês.
Onde Vira Dor de Cabeça
Por outro lado, evito serverless quando:
- Processamento longo - Lambda tem limite de 15 minutos
- Conexões persistentes - WebSocket funciona, mas é gambiarrento
- Tráfego alto e constante - EC2/Fargate fica mais barato
- Debug complexo - Rastrear problema em 50 Lambdas encadeados não é divertido
Como Estruturo um Projeto Serverless
Uso o Serverless Framework porque simplifica muito o deploy. A estrutura que funciona pra mim:
project/
├── src/
│ ├── handlers/ # Cada Lambda é um arquivo
│ │ ├── createItem.ts
│ │ ├── getItem.ts
│ │ └── listItems.ts
│ ├── services/ # Lógica de negócio
│ └── utils/ # Helpers
├── serverless.yml
└── package.json
O serverless.yml básico:
service: minha-api
provider:
name: aws
runtime: nodejs20.x
region: sa-east-1
environment:
TABLE_NAME: ${self:service}-${sls:stage}
functions:
createItem:
handler: src/handlers/createItem.handler
events:
- http:
path: /items
method: post
cors: true
getItem:
handler: src/handlers/getItem.handler
events:
- http:
path: /items/{id}
method: get
cors: true
O Padrão de Handler que Uso
Depois de muito refatorar, cheguei nessa estrutura que deixa o código testável:
// src/handlers/createItem.ts
import { APIGatewayProxyHandler } from 'aws-lambda';
import { ItemService } from '../services/ItemService';
const itemService = new ItemService();
export const handler: APIGatewayProxyHandler = async (event) => {
try {
const body = JSON.parse(event.body || '{}');
// Validação simples - não precisa de lib pesada
if (!body.name) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'name é obrigatório' }),
};
}
const item = await itemService.create(body);
return {
statusCode: 201,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item),
};
} catch (error) {
console.error('Erro ao criar item:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Erro interno' }),
};
}
};
A lógica fica no service, separada do handler. Assim consigo testar sem precisar mockar o evento inteiro do API Gateway.
DynamoDB - Single Table Design
DynamoDB é diferente de SQL. No começo eu criava uma tabela por entidade, como faria no MySQL. Erro.
O lance é usar Single Table Design - tudo na mesma tabela, diferenciando pelo padrão da chave:
PK | SK | Dados
----------------|-----------------|------------------
USER#123 | PROFILE | nome, email, ...
USER#123 | ORDER#456 | total, status, ...
USER#123 | ORDER#789 | total, status, ...
ORDER#456 | METADATA | userId, items, ...
Parece estranho no início, mas a vantagem é buscar tudo de um usuário com uma única query:
// Busca usuário + todos os pedidos dele
const result = await docClient.query({
TableName: 'minha-tabela',
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: {
':pk': `USER#${userId}`,
},
});
Cold Start - O Elefante na Sala
Cold start é quando Lambda precisa inicializar do zero. Pode adicionar 100ms a 1s+ na resposta.
O que faço pra minimizar:
- Manter as Lambdas pequenas - Menos código = inicialização mais rápida
- Usar Node.js ou Python - Java e .NET têm cold start maior
- Provisioned Concurrency pra endpoints críticos:
functions:
checkout:
handler: src/handlers/checkout.handler
provisionedConcurrency: 5 # Mantém 5 instâncias "quentes"
Provisioned Concurrency custa mais, então uso só onde latência importa (checkout, login).
Monitoramento que Funciona
Não adianta ter tudo rodando se você não sabe quando quebra. O mínimo:
# CloudWatch Alarm pra erros
resources:
Resources:
ErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: ${self:service}-errors
MetricName: Errors
Namespace: AWS/Lambda
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 5
ComparisonOperator: GreaterThanThreshold
Também ativo X-Ray pra rastrear requests:
provider:
tracing:
lambda: true
apiGateway: true
Custos Reais
Vou ser específico porque “serverless é barato” é muito vago.
Pra uma API com 1 milhão de requests/mês:
| Recurso | Uso | Custo |
|---|---|---|
| API Gateway | 1M requests | ~$3.50 |
| Lambda | 1M invocações, 200ms média | ~$0.40 |
| DynamoDB | 1M writes, 5M reads | ~$2.50 |
| CloudWatch | Logs básicos | ~$5.00 |
| Total | ~$11/mês |
Compara com um EC2 t3.small rodando 24/7: ~$15/mês. Mas serverless escala automaticamente e você não gerencia servidor.
Agora, se você tem 100M requests/mês constantes, a conta muda. Aí EC2 ou Fargate provavelmente sai mais barato.
Quando Migrei de Volta pra Container
Tive um caso onde comecei serverless e migrei pra ECS Fargate depois.
O motivo: a aplicação precisava de conexão persistente com banco PostgreSQL. Lambda abre/fecha conexão a cada invocação. Com tráfego alto, o banco não aguentava tantas conexões.
Tinha a opção de usar RDS Proxy, mas adicionava mais um componente (e custo). No fim, Fargate com connection pooling resolveu melhor.
Minha Stack Serverless Atual
Quando decido ir de serverless, essa é a stack:
- API Gateway - REST ou HTTP (HTTP é mais barato)
- Lambda - Node.js 20
- DynamoDB - On-demand billing
- SQS - Pra processamento assíncrono
- EventBridge - Pra eventos entre serviços
- Step Functions - Pra workflows complexos
Funciona bem, custo baixo, e não preciso me preocupar com servidor.
Quer discutir se serverless faz sentido pro seu caso? Me chama no LinkedIn.