DEV Community

Cover image for Event-driven com EventBridge + SQS: quando cada um entra e como evitar os erros mais comuns
Diego Broetto
Diego Broetto

Posted on

Event-driven com EventBridge + SQS: quando cada um entra e como evitar os erros mais comuns

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

Comparação entre o papel do EventBridge (roteamento) e do SQS (durabilidade) em um fluxo de eventos

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-type ou valores dentro de detail
  • 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 maxReceiveCount tentativas
  • 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.

Diagrama do padrão EventBridge, SQS e Lambda, com o backlog visível na fila

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)
Enter fullscreen mode Exit fullscreen mode

Regras do bus de eventos no console do EventBridge, com um evento roteado para quatro destinos

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Métrica EstoqueBaixado comparada ao número de pedidos criados, com mensagens processadas em duplicidade


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
Enter fullscreen mode Exit fullscreen mode

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,
    )
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

Trigger SQS da Lambda de notificação com Report batch item failures ativo

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.

Resultado no Logs Insights mostrando mensagens que falharam e não voltaram para a fila

É 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}"
    )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Fila de Financeiro com a DLQ recebendo mensagens e o alarme em estado ALARM


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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
  • BatchSize compatí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
  • maxReceiveCount calibrado: 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:SendMessage só para a regra específica, com aws:SourceArn

9.2 Consumidor Lambda

  • ReportBatchItemFailures ligado no event source mapping, e não só no código
  • Handler com BatchProcessor (ou lógica equivalente) devolvendo batchItemFailures
  • Consumidor idempotente, porque a fila padrão entrega pelo menos uma vez
  • MaximumConcurrency definido onde o sistema downstream não aguenta o pico

9.3 EventBridge

  • DeadLetterConfig em 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 ApproximateNumberOfMessagesVisible de cada DLQ, incluindo a da regra
  • Alarme em ApproximateAgeOfOldestMessage das 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.

GitHub logo 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
Enter fullscreen mode Exit fullscreen mode

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

Conclusao

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)

Collapse
 
thiagosagara profile image
Thiago Sagara •

Maravilhoso Broetto.

Como sempre artigos completos, indo de fim-a-fim.
Parabéns meu jovem.

Collapse
 
broettod profile image
Diego Broetto •

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!