Ouço muito a pergunta "EventBridge ou SQS?". Quase sempre ela está mal formulada.
Na maioria dos fluxos com mais de um consumidor, os dois trabalham juntos: o EventBridge decide para onde o evento vai, e o SQS segura esse evento até alguém poder processar. Roteamento e durabilidade são problemas diferentes, então um não substitui o outro.
Este artigo mostra onde cada peça entra, o que cada uma resolve sozinha e quais erros aparecem quando alguém usa as duas sem saber por quê.
1.0 O que cada serviço realmente resolve
Os dois aparecem juntos em quase toda discussão porque ambos recebem mensagens. Mas o trabalho de cada um é diferente: o EventBridge decide para onde a informação vai, e o SQS guarda a informação até alguém processá-la.
1.1 EventBridge
O EventBridge é um barramento de eventos. Você publica um evento uma vez, e ele avalia as regras contra o conteúdo desse evento para decidir quais alvos recebem. O produtor não sabe quantos consumidores existem nem quem são. Para incluir um consumidor novo, basta criar uma regra. O código de quem publica não muda.
O que ele faz bem:
- Entregar o mesmo evento a vários alvos
- Filtrar por conteúdo com event patterns, usando campos como
source,detail-typeou valores dentro dedetail - Receber eventos de serviços AWS e de parceiros SaaS sem código de integração
- Arquivar eventos e reenviá-los depois (archive e replay)
O que ele não faz:
- Garantir ordem entre eventos
- Segurar um evento por tempo indeterminado. Por padrão, ele tenta entregar ao alvo por até 24 horas e até 185 vezes. Esgotadas as tentativas, se o alvo não tem DLQ, o evento é descartado.
- Controlar o ritmo do consumidor. Ele empurra o evento para o alvo, então quem precisa aguentar o volume é o alvo.
1.2 SQS
O SQS é uma fila. A mensagem fica guardada até um consumidor processá-la e apagá-la de forma explícita. Se o consumidor cair no meio do processamento, a mensagem volta a ficar visível depois do visibility timeout, e outro consumidor pode pegá-la.
O que ele faz bem:
- Absorver picos. A fila cresce durante o pico, e o consumidor processa no ritmo que aguenta.
- Limitar a concorrência do consumidor (no Lambda, com
MaximumConcurrency) - Reter mensagens por até 14 dias (o padrão é 4)
- Mover para uma DLQ as mensagens que falham de forma persistente, depois de
maxReceiveCounttentativas - Manter a ordem das mensagens, quando você usa FIFO
O que ele não faz:
- Rotear. Quem publica escolhe a fila de destino.
- Entregar a mesma mensagem a vários consumidores. Consumidores na mesma fila competem pelas mensagens. Para broadcast, o caminho é uma fila por assinante, com SNS ou EventBridge na frente.
- Garantir entrega única na fila padrão. Ela é at-least-once, então o consumidor precisa ser idempotente. A FIFO tem deduplicação, mas só dentro de uma janela de tempo.
Falando em FIFO, estou pensando em fazer uma série de artigos voltados a fundamentos da computação, coisas como Filas, Pilhas, Listas, Programação Orientada a Objetos, entre outros... O que vocês acham?
1.3 Quando usar cada um
Vale reparar nos sentidos do fluxo: a regra do EventBridge empurra o evento para a fila, mas quem lê a fila é o Lambda, que faz polling pelo event source mapping.
Para decidir, pergunte duas coisas: quem precisa receber esse evento? E quem precisa segurá-lo até estar pronto para processar?
| Necessidade | Use | Por quê |
|---|---|---|
| Um evento para vários consumidores que não se conhecem | EventBridge | O produtor só publica. Cada consumidor cria a própria regra. |
| Absorver picos antes do processamento | SQS | A fila segura o excesso, e o consumidor anda no próprio ritmo. |
| Processar em ordem estrita | SQS FIFO | Só a FIFO garante ordem dentro de um grupo de mensagens. |
| Rotear pelo conteúdo do evento | EventBridge | Event patterns decidem o alvo sem código. |
| Reprocessar se o consumidor cair no meio | SQS | A mensagem permanece na fila até ser apagada. |
| Consumidor rápido, de baixo volume, sem necessidade de controlar o ritmo | EventBridge direto para a Lambda | O Lambda já guarda o evento numa fila interna e tenta de novo. Uma fila SQS na frente seria mais uma peça sem ganho. |
| Um único consumidor, sem roteamento | SQS | O EventBridge seria mais uma peça para manter, sem trazer ganho. |
A maioria dos fluxos que precisam de mais de um consumidor acaba usando as duas peças: o EventBridge decide quem recebe, e o SQS segura o evento até o consumidor estar pronto. É o padrão da seção 2.0.
2.0 O padrão mais comum: EventBridge → SQS → Lambda
Por que não colocar a Lambda como alvo direto da regra do EventBridge? Funciona, e é mais protegido do que parece. O EventBridge invoca o Lambda de forma assíncrona: o evento entra numa fila interna do Lambda, que continua tentando por até 6 horas quando a função sofre throttling. Se a função der erro, são mais duas tentativas.
O que essa fila interna não oferece é controle. Você não enxerga quantos eventos estão esperando, não escolhe quantos são processados ao mesmo tempo (a não ser reservando concorrência para a função inteira) e não processa em lote. Com o SQS no meio, o backlog vira métrica (ApproximateNumberOfMessagesVisible e ApproximateAgeOfOldestMessage), a concorrência do consumidor fica no MaximumConcurrency do event source mapping, e a mensagem pode esperar até 14 dias.
O desenho é simples. O que costuma dar errado é a configuração de cada peça: visibility timeout, permissões entre EventBridge e SQS, comportamento de lote na Lambda. O resto do artigo cobre isso com um projeto que reproduz cada erro de propósito.
3.0 O cenário: um evento, quatro destinos
O PedidoCriado do projeto de pedidos agora é roteado (fan-out) para três filas, uma por domínio, e direto para a Lambda que classifica o pedido, sem fila no meio:
┌─→ SQS Estoque → Lambda (baixa)
PedidoCriado ├─→ SQS Notificação → Lambda (e-mail)
→ EventBridge ─────┼─→ SQS Financeiro → Lambda (cobrança)
└─→ Lambda processar_pedido (sem fila)
Um fluxo linear, com uma fila e um consumidor, prova pouco. Dá para fazer isso só com SQS e ninguém notaria a diferença. O fan-out é o argumento a favor do EventBridge: o mesmo evento alimenta estoque, notificação e faturamento, e nenhum desses consumidores sabe que os outros existem. Se amanhã surgir um quinto consumidor, como um serviço de detecção de fraude, ele assina o evento e pronto. O código que cria o pedido não muda uma linha.
Cada fila do projeto reproduz de propósito um erro comum.
4.0 Visibility timeout: a fila reentregando antes da hora
A fila de Estoque simula uma chamada a um sistema legado, com latência entre 3 e 15 segundos por mensagem.
# Simula latência de um sistema de estoque legado
latencia = random.uniform(LATENCIA_MIN_SEGUNDOS, LATENCIA_MAX_SEGUNDOS)
time.sleep(latencia)
O visibility timeout da fila é de 180 segundos, para uma função com timeout de 30. São as 6x que a documentação do Lambda recomenda para filas que alimentam uma função.
EstoqueQueue:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 180
Validando o projeto, apareceu um detalhe que eu não tinha previsto. A primeira versão lia lotes de 5 mensagens. Com cada baixa levando até 15 segundos, o pior caso de um lote chegava a 75 segundos, bem acima do timeout de 30 da função. Troquei para BatchSize: 1. Quando cada mensagem é lenta, o tamanho do lote também entra na conta do timeout.
O que acontece se você reduzir o visibility timeout para 10 segundos? O SQS entrega a mensagem ao consumidor e começa a contar. Se a mensagem não for apagada dentro desses 10 segundos, ela volta a ficar visível e pode ser entregue de novo, possivelmente enquanto o primeiro consumidor ainda está no meio do time.sleep(). Com isso, dois consumidores processam o mesmo pedido e o estoque é debitado duas vezes.
No projeto dá para reproduzir: troque o VisibilityTimeout de 180 para 10 no template, faça o deploy e compare a métrica EstoqueBaixado com o número de pedidos criados.
5.0 Lote com falhas: o que volta para a fila e o que some
A fila de Notificação processa lotes de até 10 mensagens por invocação. O problema aparece quando uma mensagem do lote falha.
Sem tratamento especial, o comportamento é tudo ou nada. Se uma exceção sobe do handler, o lote inteiro é considerado falho e as 10 mensagens voltam a ficar visíveis, inclusive as 9 que já tinham mandado o e-mail de confirmação ao cliente.
# Versão ingênua, evite
def lambda_handler(event, context):
for record in event["Records"]:
enviar_notificacao(record) # se UMA lançar exceção, TODAS voltam
A correção é a resposta parcial de lote, com o utilitário de batch do Powertools:
from aws_lambda_powertools.utilities.batch import (
BatchProcessor, EventType, process_partial_response,
)
processor = BatchProcessor(event_type=EventType.SQS)
def lambda_handler(event, context):
return process_partial_response(
event=event,
record_handler=processar_registro,
processor=processor,
context=context,
)
O BatchProcessor processa cada mensagem isoladamente, captura as falhas por item e devolve a lista batchItemFailures com os IDs que falharam. Só essas voltam para a fila. Se todas as mensagens do lote falharem, ele lança BatchProcessingError, e aí o lote inteiro volta.
Além do código, o event source mapping precisa da flag:
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt NotificacaoQueue.Arn
BatchSize: 10
FunctionResponseTypes: [ReportBatchItemFailures]
Sem a flag, o cenário é pior que o da versão ingênua. O handler captura as falhas e termina sem erro, e a documentação do Lambda diz que, quando a função processa o lote com sucesso, o Lambda apaga as mensagens da fila. A lista de falhas vira um valor de retorno qualquer, e as mensagens que falharam somem sem passar pela DLQ.
É um erro fácil de cometer porque o código parece certo, e a outra metade da correção está no template. No projeto, o consumer de notificação simula 25% de falha aleatória, o suficiente para gerar lotes mistos com frequência.
6.0 DLQ: separando erro passageiro de erro permanente
A fila de Financeiro simula outro tipo de falha: um erro de negócio permanente, como um valor de cobrança inválido, que nenhum retry corrige.
if random.random() < TAXA_ERRO_PERMANENTE:
raise InvalidOperation(
f"Valor de cobrança inválido para pedido {pedido_id}: {valor}"
)
A diferença importa. Para falha passageira (timeout de rede, serviço fora do ar por alguns segundos), retry é o comportamento certo. Para falha permanente, retry só atrasa o inevitável e gasta concorrência. A fila tenta 3 vezes antes de desistir:
FinanceiroQueue:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 90 # 6x o timeout de 15s da função
RedrivePolicy:
deadLetterTargetArn: !GetAtt FinanceiroDLQ.Arn
maxReceiveCount: 3
Depois da terceira entrega sem sucesso, o SQS move a mensagem para a DLQ sozinho. Não precisa de código.
Nessa fila, o erro mais comum é esquecer o alarme na própria DLQ:
FinanceiroDLQAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
Namespace: AWS/SQS
MetricName: ApproximateNumberOfMessagesVisible
Dimensions:
- Name: QueueName
Value: !GetAtt FinanceiroDLQ.QueueName
Statistic: Maximum
Period: 300
EvaluationPeriods: 1
Threshold: 0
ComparisonOperator: GreaterThanThreshold
Sem o alarme, as mensagens se acumulam na DLQ sem ninguém ser avisado, e o problema só aparece quando alguém nota que uma parte dos pedidos nunca gerou cobrança. Até lá, podem ser semanas de mensagens para investigar uma por uma.
7.0 Quando o EventBridge sozinho basta
Nem todo consumidor precisa de fila no meio. O processar_pedido, que classifica o pedido por faixa de valor, é rápido, tem volume baixo e não precisa de controle de ritmo. Para ele, a fila interna do Lambda dá conta, e uma fila SQS na frente seria só mais uma peça para manter.
No projeto, a quarta rota leva o PedidoCriado direto para essa função:
ProcessarPedidoFunction:
Type: AWS::Serverless::Function
Properties:
Events:
PedidoCriado:
Type: EventBridgeRule
Properties:
EventBusName: !Ref PedidosEventBus
Pattern:
source: [observability-demo.pedidos]
detail-type: [PedidoCriado]
DeadLetterConfig:
Arn: !GetAtt RuleDLQ.Arn
EventInvokeConfig:
MaximumRetryAttempts: 2
DestinationConfig:
OnFailure:
Type: SQS
Destination: !GetAtt ProcessarFalhasQueue.Arn
Sem fila no meio, essa rota precisa de duas redes de segurança, uma de cada lado da entrega.
A primeira é o DeadLetterConfig na regra, que pega falha de entrega. O EventBridge tentou invocar a função por até 24 horas e não conseguiu, por exemplo por causa de uma permissão alterada sem ninguém notar.
A segunda é o destino OnFailure no Lambda. Quando o Lambda aceita o evento na fila interna, a entrega do EventBridge está concluída. Se depois a função errar na primeira tentativa e nas duas seguintes, o Lambda descarta o evento, e a DLQ da regra não fica sabendo de nada. O OnFailure manda esse evento para a ProcessarFalhasQueue em vez de jogá-lo fora. É fácil configurar só a primeira rede e achar que a rota está protegida.
Nas rotas com fila, a divisão é parecida. A RuleDLQ também protege os três alvos SQS contra falha de entrega, e a falha de processamento fica com a DLQ de cada fila, pelo maxReceiveCount da seção 6.0.
8.0 A policy da fila, que só aparece quando falta
O fan-out não funciona sem um passo que é fácil de pular: o EventBridge precisa de permissão explícita para escrever em cada fila.
EstoqueQueuePolicy:
Type: AWS::SQS::QueuePolicy
Properties:
Queues: [!Ref EstoqueQueue]
PolicyDocument:
Statement:
- Effect: Allow
Principal: { Service: events.amazonaws.com }
Action: sqs:SendMessage
Resource: !GetAtt EstoqueQueue.Arn
Condition:
ArnEquals: { "aws:SourceArn": !GetAtt EstoqueRule.Arn }
Sem essa policy, a regra é criada, o alvo aparece configurado, e os eventos não chegam. O produtor não vê erro nenhum, porque o PutEvents deu certo: a falha acontece depois, na entrega. É o tipo de falha que a RuleDLQ da seção 7.0 captura. Sem ela, você só descobre quando o time de um domínio reclama que não recebe nada.
A condição ArnEquals limita a permissão àquela regra. Em bus customizado, o ARN da regra inclui o nome do bus (rule/pedidos-bus-dev/nome-da-regra), então usar !GetAtt EstoqueRule.Arn evita montar esse ARN à mão e errar o formato.
9.0 Checklist antes de ir para produção
Use esta lista como revisão do template antes do primeiro deploy em produção. Cada item é uma falha que aparece nos cenários deste artigo.
9.1 Filas SQS
- Visibility timeout de pelo menos 6x o timeout da função consumidora, em cada fila
-
BatchSizecompatível com o timeout: lote vezes o tempo de cada mensagem cabe na função - DLQ configurada em toda fila que alimenta um consumidor crítico
-
maxReceiveCountcalibrado: baixo o bastante para não represar retry inútil, alto o bastante para sobreviver a uma falha passageira - Resource policy da fila liberando
sqs:SendMessagesó para a regra específica, comaws:SourceArn
9.2 Consumidor Lambda
-
ReportBatchItemFailuresligado no event source mapping, e não só no código - Handler com
BatchProcessor(ou lógica equivalente) devolvendobatchItemFailures - Consumidor idempotente, porque a fila padrão entrega pelo menos uma vez
-
MaximumConcurrencydefinido onde o sistema downstream não aguenta o pico
9.3 EventBridge
-
DeadLetterConfigem cada alvo da regra, principalmente quando não há fila no meio - Destino
OnFailure(ou DLQ da função) quando a regra invoca a Lambda direto
9.4 Alarmes
- Alarme em
ApproximateNumberOfMessagesVisiblede cada DLQ, incluindo a da regra - Alarme em
ApproximateAgeOfOldestMessagedas filas principais, para detectar consumidor atrasado
10.0 Projeto de exemplo
O repositório estende o observability-demo dos artigos anteriores com o fan-out completo: três filas com seus consumidores, a rota direta para a Lambda de classificação, as policies e os alarmes descritos acima. Cada consumer tem um comentário no código explicando a lição daquela fila.
diegobroetto
/
observability-demo
Demo serverless de observabilidade com CloudWatch + X-Ray (logs, metricas EMF, traces) - AWS SAM
observability-demo
Exemplo completo de observabilidade serverless com CloudWatch + X-Ray para o artigo:
"Observabilidade serverless sem surpresa no bolso: logs, métricas e traces com CloudWatch + X-Ray do jeito certo"
O que este projeto demonstra
Pilar
Ferramenta
Onde ver no código
Logs estruturados
Powertools Logger + JSON nativo do Lambda
observability.py, todos os handlers
Retenção de logs
AWS::Logs::LogGroup com RetentionInDays
template.yaml → LogGroups
Log level por ambiente
Mappings no SAM + LOG_EVENT flag
template.yaml → Mappings
Métricas sem custo extra
Embedded Metric Format (EMF)
criar_pedido/handler.py
Métricas com dimensão de negócio
Powertools Metrics + add_dimension
processar_pedido/handler.py
Traces por subsegmento
Powertools Tracer + @capture_method
Todos os handlers
Anotações no X-Ray
tracer.put_annotation
consultar_pedido/handler.py
Alarmes em p99
AWS::CloudWatch::Alarm + ExtendedStatistic
template.yaml → Alarmes
Dashboard consolidado
AWS::CloudWatch::Dashboard
template.yaml → Dashboard
Queries prontas
CloudWatch Logs Insights
monitoring/insights-queries.md
Endpoints
Método
Path
Função
Descrição
POST
/pedidos
CriarPedidoFunction
Cria o pedido e publica
PedidoCriado no EventBridge
GET
/pedidos/{id}
Para rodar:
sam build --use-container
sam deploy --config-env dev
O README traz o passo a passo para reproduzir os três erros de propósito: reduzir o visibility timeout, remover a flag de resposta parcial e ver a DLQ recebendo mensagem.
O projeto tem limitações que vale deixar claras. Não há testes automatizados, os alarmes cobrem só a fila de Estoque e as DLQs de Financeiro, da regra e do processar_pedido, e os consumers simulam as integrações (estoque, e-mail, cobrança) em vez de chamar sistemas reais.
11.0 Conclusão
A pergunta "EventBridge ou SQS?" parte de uma premissa errada. O que importa é o que cada ponto do fluxo precisa: roteamento ou durabilidade. Às vezes é só um dos dois. No projeto de pedidos, com quatro consumidores, precisou dos dois.
Configure DLQ no alvo da regra do EventBridge e em cada fila SQS desde o primeiro deploy, não depois que algo já sumiu. E antes de copiar um BatchSize: 10 de algum exemplo, faça duas contas: quanto tempo o lote leva no pior caso, e o que acontece quando a nona mensagem dá certo e a décima falha.
Diego Broetto é Tech Lead na Darede e escreve sobre AWS, serverless e infraestrutura como código.
Já passou por alguma dessas pegadinhas de um jeito diferente? Comenta aqui embaixo.








Top comments (2)
Maravilhoso Broetto.
Como sempre artigos completos, indo de fim-a-fim.
Parabéns meu jovem.
Muito obrigado pelo feedback, Sagara! Fico muito contente por saber que o conteúdo está sendo útil. A ideia é mesmo entregar o máximo de valor possível! Um abraço!