Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

05 - Dead Letter Queue (DLQ)

#dead-letter#dlq#mensagens-envenenadas#redrive#reprocessamento
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Dead Letter Queue (DLQ)

A Dead Letter Queue é a fila de destino para mensagens que não puderam ser processadas com sucesso após esgotar as tentativas definidas. Em vez de travar o fluxo em uma mensagem problemática ou descartá-la em silêncio, o broker transfere o envelope para uma área de quarentena com o conteúdo original e o contexto da falha. O padrão existe porque em qualquer pipeline de mensagens aparecerão mensagens envenenadas, aquelas que falham de forma determinística, seja por payload malformado, contrato desatualizado, bug no consumidor ou dependência indisponível por tempo demais. Em produção, a DLQ é simultaneamente mecanismo de confiabilidade, fonte de observabilidade e instrumento de recuperação: o tamanho dela, a velocidade de entrada e a idade das mensagens são termômetros diretos da saúde do processamento assíncrono. Tratar DLQ como acessório é um dos erros mais caros de mensageria, pois a fila envenenada não processada é dado de negócio parado, muitas vezes de valor financeiro direto.

Conceito: quarentena com contexto

A DLQ é uma fila comum usada com um papel especial: receber o que não passou pelo processamento. Ela não é lixeira, porque o conteúdo continua íntegro e reprocessável, e não é fluxo alternativo, porque nada deve consumir dela rotineiramente. A melhor analogia é a quarentena de um hospital: paciente que não pode seguir o fluxo, mas que segue sendo observado, diagnosticado e tratado.

O termo dead letter vem dos correios: correspondência que não pôde ser entregue nem devolvida era arquivada como carta morta por um escritório dedicado. O padrão herdou o nome e a função, com uma diferença importante: em sistemas de mensageria, a carta morta pode voltar ao fluxo, e a qualidade do contexto anexado é o que decide se ela volta corrigida ou apodrece no arquivo.

O que entra na DLQ carrega contexto de falha. Brokers maduros anexam metadados como contagem de tentativas, timestamp de cada rejeição, identidade do consumidor e motivo da rejeição. Em SQS, o atributo DeadLetterQueueSourceArn identifica a fila de origem; no RabbitMQ, os headers x-death registram cada morte, com fila de origem, motivo e momento.

A distinção conceitual mais importante é entre falha transiente e falha permanente. DLQ existe para as permanentes, e para as transientes que extrapolaram um limite razoável de tentativas. Mandar tudo direto para a DLQ na primeira falha desperdiça a resiliência do retry; nunca mandar nada vira loop infinito de mensagens impossíveis.

Vale separar DLQ de fila de retry, dois recursos que costumam ser confundidos. A fila de retry recebe a mensagem para nova tentativa com atraso, e o processamento ainda é esperado; a DLQ recebe o fim da linha, e o processamento parou de ser automático. Arquiteturas comuns encadeiam os dois: retry com backoff por algumas tentativas, e DLQ como destino final esgotado o limite.

O problema que resolve: a mensagem envenenada

A mensagem envenenada é a que falha sempre, com qualquer tentativa, porque o erro está no conteúdo ou no código, e não na circunstância. Exemplos típicos: JSON com campo obrigatório ausente, evento de versão antiga sem o campo novo, valor monetário negativo que o consumidor rejeita, payload gigante que estoura limite do destino.

Sem DLQ, cada envenenada custa caro de uma das duas formas. Com requeue infinito, ela trava o consumidor em loop, gera tempestade de logs e empurra o backlog para cima, bloqueando as mensagens saudáveis atrás dela. Com descarte, o dado some em silêncio, e o prejuízo aparece semanas depois como reclamação de cliente ou divergência financeira.

Com DLQ, a mensagem sai do fluxo principal com contexto completo, o fluxo continua drenando o backlog, e o time investiga com calma. O efeito sistêmico é a conversão de falha em caso de trabalho: um item para revisar, e não um incidente de fila parada.

Funcionamento interno: retry, limite e redrive

O ciclo completo tem três estágios. No primeiro, o consumidor tenta processar e falha; o mecanismo de ack controla se a mensagem volta. No segundo, tentativas subsequentes se repetem até o limite da política de retry, idealmente com backoff e atraso entre elas. No terceiro, esgotado o limite, o broker move a mensagem para a DLQ, anexando o contexto da falha.

Mecanismo Retry Envio à DLQ Observação
SQS Redrive policy com maxReceiveCount Automático pela política Conta entregas, não erros lógicos
RabbitMQ Requeue ou fila de retry com TTL basic_nack com requeue false Contagem em headers x-death
Kafka Sem DLQ nativa Tópico de erros escrito pelo app Padrões de error handler no consumer
Azure Service Bus maxDeliveryCount da fila Automático DLQ por fila e por assinatura

A política de redrive merece desenho explícito. Em SQS, a redrive policy liga a fila de origem à DLQ e define maxReceiveCount, tipicamente entre três e cinco. A contagem é de recebimentos, e não de falhas lógicas, o que significa que visibility timeout curto com processamento longo infla a contagem e manda mensagem boa para a DLQ; a regra prática é timeout maior que o pior processamento real.

# SQS: fila principal com redrive para DLQ
FilaPedidos:
  Type: AWS::SQS::Queue
  Properties:
    QueueName: pedidos-processamento
    VisibilityTimeout: 300          # maior que o pior processamento
    RedrivePolicy:
      deadLetterTargetArn: !GetAtt FilaPedidosDLQ.Arn
      maxReceiveCount: 4            # tentativas antes do redrive

FilaPedidosDLQ:
  Type: AWS::SQS::Queue
  Properties:
    QueueName: pedidos-processamento-dlq
    MessageRetentionPeriod: 1209600 # 14 dias para investigação
# Consumidor: separa falha transiente de falha permanente
def processar(mensagem):
    try:
        evento = validar(mensagem.corpo)      # schema e contrato
        faturar(evento)                       # negócio + dependências
        mensagem.ack()
    except ErroDeContrato as e:
        # permanente: retry não vai curar payload inválido
        registrar_erro(mensagem, motivo=str(e))
        mensagem.nack(requeue=False)          # direto para DLQ
    except ErroTransitorio:
        # transiente: deixa a contagem de entregas decidir
        raise

Monitoramento: DLQ viva é sinal, DLQ esquecida é dívida

A métrica primária é o número de mensagens na DLQ, acompanhada da idade da mais antiga. Entrada nova com crescimento sustentado indica problema sistemático, como deploy com bug ou contrato quebrado; fila estável com poucas mensagens e idade baixa é operação saudável; fila crescendo sem alerta é incidente silencioso de dados parados.

O alerta deve disparar no primeiro item, para filas de baixo volume, e em taxa de crescimento, para filas de alto volume. Cada entrada em DLQ deveria gerar evento de observabilidade com a fila de origem, motivo e id da mensagem, permitindo correlacionar com deploy, release e telemetria do consumidor.

A armadilha clássica é a DLQ esquecida: criada junto com a fila, nunca monitorada, acumulando milhares de mensagens por meses. O custo aparece quando o negócio pergunta por pedidos que nunca faturaram, ou quando a retenção expira e o dado apaga antes de qualquer investigação. DLQ sem alerta não é confiabilidade, é perda com etapa a mais.

# Alarme de DLQ: dispara no primeiro item em fila crítica
alarms:
  - nome: dlq-pedidos-nao-vazia
    metrica: ApproximateNumberOfMessagesVisible
    dimensao: QueueName=pedidos-processamento-dlq
    operador: GreaterThanThreshold
    limiar: 0
    periodos: 1
    acao: sns-alerta-oncall
  - nome: dlq-pedidos-idade
    metrica: ApproximateAgeOfOldestMessage
    dimensao: QueueName=pedidos-processamento-dlq
    operador: GreaterThanThreshold
    limiar: 3600        # 1 hora é tempo demais para dado de negócio
    acao: sns-alerta-oncall

Reprocessamento manual e automático

O reprocessamento manual é a operação padrão: inspecionar a mensagem, diagnosticar a causa, corrigir o que precisa ser corrigido e devolver o envelope para a fila de origem ou para uma fila de correção. Em SQS, o console oferece redrive para a fila de origem; em ferramentas próprias, o caminho seguro copia a mensagem para a origem com o mesmo id, preservando a idempotência do consumidor.

O reprocessamento automático é para falhas transientes que extrapolaram o limite, como instabilidade de dependência durante um incidente. Uma tarefa periódica move lotes da DLQ de volta para a origem com limitação de taxa, monitorando a DLQ de retorno. O gatilho precisa ser criterioso: reprocessar em massa um problema permanente só recarrega a DLQ com atraso.

Em ambos os modos, a ordem correta é sempre: primeiro corrigir a causa, depois reprocessar. Enviar de volta antes de entender o problema é reciclar o veneno, e a DLQ cresce novamente com o mesmo conteúdo.

No Kafka, o equivalente é o dead letter topic, escrito pelo próprio consumidor quando uma mensagem esgota as tentativas internas. Frameworks como Spring Kafka e Flink trazem o comportamento pronto, e o desenho do tópico deve seguir as mesmas regras: retenção longa, contexto da falha no header, alerta de crescimento e ferramenta de replay. O nome muda, a disciplina é a mesma.

Erros comuns

  • DLQ sem alerta e sem dono: fila cresce por semanas, e o dado parado é descoberto pelo negócio, não pela engenharia.
  • Retenção curta na DLQ: mensagens expiram antes da investigação, transformando falha processável em perda definitiva.
  • Reprocessar sem corrigir a causa: lote devolvido retorna integralmente para a DLQ, e o time confunde movimento com progresso.
  • Fila principal sem DLQ configurada: mensagens envenenadas em loop ou descartadas, e ninguém percebe até a conciliação financeira fechar.
  • Usar a mesma DLQ para origens diferentes: contexto misturado dificulta triagem, e a taxa de entrada deixa de apontar a causa.
  • Visibility timeout menor que o processamento: mensagens boas esgotam o maxReceiveCount durante execução legítima e enchem a DLQ de itens saudáveis.

Boas práticas

  • Crie a DLQ junto com cada fila e assinatura, como parte do template padrão de infraestrutura, e não como exceção pedida depois.
  • Configure alerta no primeiro item para filas críticas, com runbook que diga quem aciona, onde inspecionar e qual é o caminho de correção.
  • Defina retenção generosa na DLQ, na casa de dias a semanas, compatível com o tempo real de investigação do time de plantão.
  • Anexe contexto à mensagem morta, com contagem de tentativas, motivo e origem, para que a triagem não dependa de adivinhação.
  • Separe DLQ por fluxo e por criticidade, mantendo taxas de entrada comparáveis e triagem objetiva.
  • Trate o reprocessamento como procedimento operacional documentado, com verificação de causa corrigida, limitação de taxa e monitoramento do retorno.
  • Audite periodicamente as DLQs do catálogo, verificando se cada uma tem alerta ativo, dono definido e retenção compatível com o fluxo que a alimenta.