Dead Letter Queues: Tratamento de Mensagens Tóxicas em Sistemas Distribuídos
Impedir que mensagens tóxicas bloqueiem as filas
Uma fila de mensagens mortas (dead-letter queue) é a rede de segurança que captura mensagens que seus consumidores não conseguem processar, para que uma carga útil defeituosa não bloqueie ou descartie silenciosamente tudo o que vem depois dela na fila.
Todo sistema orientado a mensagens eventualmente recebe uma mensagem que não consegue lidar: uma carga útil malformada, um esquema que mudou por baixo do consumidor ou uma chamada para um serviço dependente que falha independentemente de quantas vezes você tente novamente. Sem uma fila de mensagens mortas, essa mensagem ou bloqueia a cabeça da fila para sempre ou é descartada silenciosamente, e ambos os resultados são piores do que saber sobre a falha.
Uma fila de mensagens mortas transforma uma falha invisível em uma visível e inspecionável. Ela oferece um local para quarentenar a mensagem, emitir um alerta sobre ela e decidir — deliberadamente, não por acidente — se deve corrigi-la e reproduzi-la ou descartá-la permanentemente.

Os mecanismos variam entre os brokers, mas o padrão subjacente é o mesmo em todos os lugares: um contador de tentativas de entrega, um limite e um destino para mensagens que o ultrapassam. Este guia aborda o que uma fila de mensagens mortas realmente faz, como distinguir uma mensagem venenosa de uma falha transitória, quando tentar novamente versus descartar, e como reproduzir com segurança após corrigir a causa raiz. Para o contexto mais amplo de padrões de integração onde este padrão se encaixa, consulte Arquitetura de Aplicação.
O que é uma Fila de Mensagens Mortas
Uma fila de mensagens mortas é uma fila comum e separada para a qual um broker ou consumidor encaminha uma mensagem após essa mensagem falhar no processamento muitas vezes. Não se trata de uma construção especial — a fila de mensagens mortas do RabbitMQ é uma fila regular vinculada a uma exchange regular, e uma DLQ do SQS é uma fila padrão ou FIFO comum. O que torna uma fila uma “DLQ” é puramente o fato de algo outro apontar mensagens falhas para ela.
Cada broker implementa o redirecionamento de forma diferente:
- Amazon SQS usa uma política de redrive com um
maxReceiveCount. Uma vez que uma mensagem tenha sido recebida esse número de vezes sem ser deletada, o SQS a move para odeadLetterTargetArnconfigurado. A AWS recomenda explicitamente manter o período de retenção de mensagens da DLQ mais longo do que o da fila de origem, pois o carimbo de tempo original de enfileiramento — e não o momento da movimentação — ainda rege a expiração. - RabbitMQ encaminha uma mensagem para dead-letter quando ela é rejeitada com
requeue=false, seu TTL por mensagem expira, a fila atinge um limite de comprimento ou uma fila quorum excede seudelivery-limit. Você configura isso com os argumentos de filax-dead-letter-exchange(e opcionalmentex-dead-letter-routing-key), e o RabbitMQ anexa cabeçalhosx-deathregistrando o motivo, a fila de origem e quantas vezes isso aconteceu. - Apache Kafka não possui uma DLQ nativa do broker. O Kafka apenas rastreia offsets; ele não tem o conceito de uma mensagem “falha”. O padrão de tópico de dead-letter é algo que você constrói no consumidor, em uma topologia Kafka Streams ou em um conector Kafka Connect — comumente pareado com uma camada de retry-topic antes do DLT terminal, como fazem o
@RetryableTopice oDeadLetterPublishingRecovererdo Spring Kafka. - Azure Service Bus encaminha automaticamente para dead-letter uma vez que a contagem de entrega de uma mensagem exceda o
MaxDeliveryCount(padrão 10), e também por um punhado de razões do sistema comoTTLExpiredException,HeaderSizeExceededeMaxTransferHopCountExceeded, cada uma registrada na propriedadeDeadLetterReasonda mensagem.
Para uma visão mais ampla de como os brokers e plataformas de streaming se encaixam operacionalmente, em vez de serem tratados apenas como um padrão de confiabilidade, Apache Kafka Quickstart e RabbitMQ no AWS EKS vs SQS cobrem o lado da infraestrutura de execução desses brokers.
Mensagens Venenosas
Uma mensagem venenosa é aquela que nunca terá sucesso, independentemente de quantas vezes um consumidor tente novamente — uma carga útil JSON malformada, um campo de esquema que um produtor renomeou, uma violação de regra de negócios ou um bug que lança uma exceção em uma entrada específica todas as vezes. Isso é diferente de uma falha transitória, onde a mensagem está correta, mas o ambiente brevemente não está: um tempo limite de um serviço dependente, uma falha momentânea na conexão do banco de dados, uma resposta de limite de taxa.
Tratar ambos os tipos de falha da mesma maneira é o erro mais comum em DLQs. Se você encaminhar para dead-letter na primeira falha, você pune erros transitórios que teriam tido sucesso na tentativa de repetição. Se você tentar novamente mensagens venenosas dezenas de vezes antes de desistir, você desperdiça capacidade de computação, atrasa mensagens não relacionadas atrás delas (em filas ordenadas e partições) e inunda seus logs com o mesmo rastreamento de pilha.
Alguns sinais de detecção ajudam a separar os dois casos:
- Tipo de exceção. Erros de desserialização, erros de validação e falhas do estilo
ClassCastExceptionsão quase sempre permanentes. ODefaultErrorHandlerdo Spring Kafka trata explicitamente certas exceções como fatais e pula as tentativas para elas em vez de esgotar o orçamento de tentativa primeiro. - Contagem de repetição sem variação. A matriz de cabeçalho
x-deathdo RabbitMQ permite que você veja exatamente quantas vezes uma mensagem foi encaminhada para dead-letter e por quê; uma mensagem com uma contagem crescente e umx-first-death-reasonidêntico em cada ciclo é venenosa, não azarada. - Falha consistente entre réplicas. Se todas as instâncias do consumidor falharem na mesma mensagem enquanto têm sucesso em tudo ao redor, a própria mensagem é o problema, não a infraestrutura.
Para distinguir falhas retryable de não retryable no nível do código — a mesma classificação na qual a política de DLQ depende — consulte Arquitetura de Tratamento de Erros em Go: Limites e Padrões.
Tentativa vs Descarte
A decisão de política central por trás de toda DLQ é o limite de tentativas: quantas tentativas de entrega uma mensagem recebe antes de ser quarentenada. Defina isso muito baixo e você encaminhará para dead-letter mensagens que teriam tido sucesso após uma breve oscilação do serviço dependente. Defina-o muito alto e uma mensagem venenosa permanecerá na fila principal por muito tempo, consumindo capacidade do trabalhador e — em sistemas ordenados — bloqueando tudo o que está enfileirado atrás dela.
As orientações atuais entre os principais brokers convergem para números semelhantes:
| Broker | Mecanismo | Limite típico |
|---|---|---|
| Amazon SQS | maxReceiveCount na política de redrive |
3–5 para cargas de trabalho mistas transitórias/permanentes |
| RabbitMQ (filas quorum) | Argumento de política delivery-limit |
3–5, ajustado por fila |
| Azure Service Bus | MaxDeliveryCount |
Padrão 10, frequentemente reduzido para filas sensíveis à latência |
| Kafka (via retry topics) | Cabeçalho de contagem de tentativa + camada retry-topic | 3–4 saltos retry-topic antes do DLT terminal |
Um meio prático no qual muitas equipes se estabelecem é: comece conservador (2–3 tentativas), observe a mistura real de falhas em produção e aumente o limite apenas para filas onde você pode demonstrar que a maioria das falhas se resolve em poucas tentativas. Associe a contagem de tentativas com backoff exponencial e jitter entre as tentativas para que uma interrupção do serviço dependente não se transforme em uma tempestade de tentativas — a mesma disciplina coberta no design de backoff e circuit breakers. Um circuit breaker no limite de integração complementa isso: ele impede o envio de solicitações para uma dependência não saudável, em vez de permitir que cada mensagem na fila descubra individualmente a interrupção e seja encaminhada para dead-letter uma por uma.
Uma vez que uma mensagem está na DLQ, “descartar” ainda deve ser uma ação deliberada, não negligência. Defina um período de retenção na própria DLQ — tempo suficiente para investigar (a AWS recomenda que a retenção da DLQ exceda a da fila de origem; uma semana é um piso comum para DLQs do RabbitMQ) — e emita alertas sobre a profundidade e a idade da DLQ para que as falhas sejam triadas em vez de expirarem silenciosamente. Uma mensagem que expira da DLQ sem ser examinada é uma mensagem que você decidiu perder sem decidir perdê-la.
A idempotência importa tanto aqui quanto em qualquer outro lugar: duplicatas podem ocorrer; uma mensagem que é redirecionada de uma DLQ de volta para a fila principal é, funcionalmente, uma entrega duplicada. Se seu consumidor não for seguro para ser executado duas vezes na mesma mensagem, o redirecionamento a partir de uma DLQ pode criar exatamente o bug de efeito colateral duplicado que você estava tentando evitar. Consulte Idempotência em Sistemas Distribuídos Que Realmente Funcionam para os padrões do lado do consumidor que tornam o redirecionamento seguro.
Estratégias de Reprodução
Retirar uma mensagem da DLQ corretamente é sua própria disciplina, separada de colocá-la lá.
- Corrija a causa raiz primeiro. Implantar a correção do consumidor antes de reproduzir é a diferença entre uma recuperação limpa e o envenenamento da fila com a mesma falha uma segunda vez.
- Redirecione deliberadamente, não automaticamente. O SQS suporta um recurso de redrive-to-source que move mensagens de volta para sua fila original (ou outro destino) sob demanda; RabbitMQ e Kafka exigem que você construa o consumidor ou ferramenta equivalente sozinho. De qualquer forma, trate a reprodução como uma ação acionada pelo operador com um registro do que foi reproduzido e quando.
- Preserve a ordem onde ela importa. Para Kafka, o tópico de dead-letter deve ter pelo menos tantas partições quanto o tópico de origem e deve reter a chave da mensagem original, para que as mensagens reproduzidas retornem à partição correta e preservem a ordem por chave.
- Limite as tentativas de reprodução. Uma mensagem que falha novamente após um ciclo de correção e reprodução não é transitória — roteie-a para um arquivo permanente (uma tabela de banco de dados, um bucket de armazenamento de objetos) em vez de repeti-la na DLQ indefinidamente. Os próprios documentos do RabbitMQ alertam que uma mensagem encaminhada para dead-letter pode ser roteada entre filas apenas um número limitado de vezes (16) antes que o encaminhamento para dead-letter baseado em TTL seja desabilitado.
- Nunca deixe uma DLQ encaminhar para dead-letter ela mesma. Se sua DLQ tiver sua própria
x-dead-letter-exchange(RabbitMQ) ou sua própria política de redrive (SQS) apontando de volta para a mesma cadeia, uma falha de reprodução pode criar um loop infinito. Mantenha a configuração de dead-letter da própria DLQ vazia ou aponte-a para um arquivo estritamente terminal. - Emita alertas sobre o volume, não apenas a presença. Uma única mensagem em uma DLQ é um ponto de dados; um pico repentino é um incidente. Conecte a profundidade da DLQ e a idade da mensagem ao mesmo pipeline de alerta que você usa para tudo o mais — consulte Design de Sistemas de Alerta Modernos para Equipes de Observabilidade para práticas de roteamento e redução de ruído que se aplicam diretamente aos alertas da DLQ.
Se seu fluxo de trabalho envolve processos longos e multietapas, em vez de mensagens únicas, o mesmo pensamento de dead-letter se aplica na camada de fluxo de trabalho — a lógica de compensação de uma saga precisa da mesma disciplina de “quarentena, inspecionar, decidir” quando uma etapa falha permanentemente em vez de transitariamente. E quando os próprios eventos se originam de uma gravação de banco de dados, o padrão de caixa de saída transacional já incorpora o tratamento de dead-letter no worker de relay, para que o padrão apareça uma camada mais acima do que o broker.
Onde as DLQs se Encaixam no Quadro Maior
Uma fila de mensagens mortas não faz com que as falhas desapareçam — ela torna as falhas sobreviveis e passíveis de revisão em vez de silenciosas. Ela funciona melhor junto com tentativas de backoff para o caso transitório, consumidores idempotentes para que o redirecionamento seja seguro e um circuit breaker para que uma dependência com dificuldades não inunde a fila principal (e, eventualmente, a DLQ) com a mesma falha milhares de vezes. Trate o limite, a retenção e o alerta da DLQ como decisões de configuração de primeira classe, não como padrões que você deixa intocados, e as mensagens mortas tornam-se uma ferramenta de diagnóstico em vez de um lugar onde os dados desaparecem silenciosamente.