A caixa de devolvidos
Uma carta com endereço inexistente não fica circulando pelo país para sempre. Depois de algumas tentativas, ela vai para o setor de devolvidos — que guarda a carta, anota o motivo, e permite que alguém decida o que fazer. O que ninguém faz é deixá-la no meio do malote, atrasando as outras entregas por tempo indeterminado.
A mensagem envenenada
O consumidor falha, devolve a mensagem para a fila, pega a mesma mensagem de novo, falha de novo. Se a causa for de conteúdo — e não de infraestrutura —, esse ciclo não tem fim.
Em fila com ordenação por chave, o efeito é pior: nada atrás daquela mensagem é processado enquanto ela não sair da frente.
A distinção que resolve isto é entre falha transitória e falha permanente. Banco indisponível, timeout de rede, deploy em andamento: tentar de novo daqui a pouco funciona. JSON malformado, campo obrigatório ausente, referência a um pedido que foi apagado: tentar de novo daqui a mil anos dá o mesmo resultado. Retry serve à primeira; DLQ serve à segunda.
Na prática nem sempre dá para classificar com segurança, e por isso a heurística padrão é por contagem: tenta N vezes com backoff e, se ainda assim falhar, trata como permanente e desvia. Não é perfeito — um incidente de infraestrutura longo o bastante manda mensagens boas para a DLQ —, e é justamente por isso que a DLQ precisa permitir reprocessamento em massa.
O que separa uma DLQ útil de um cemitério é o contexto. Uma fila que guarda só o corpo original obriga quem investiga a reproduzir o erro do zero. Guardar a exceção, o número de tentativas, o momento da primeira falha e a fila de origem transforma o desvio em material de diagnóstico. E, principalmente: alguém precisa ser avisado. Um alerta simples em "profundidade da DLQ maior que zero" é o que impede que ela vire um lugar onde dados somem em silêncio.
- Fila principal
- entregaConsumidor
- falhouTransitória ou permanente?
- permanente, ou passou de N tentativasDead Letter Queue
- alguém precisa olharAlerta e reprocessamento
Como isso aparece na prática
O contrato que mudou sem avisar
Cenário
Um produtor passa a enviar valor como string em vez de número. O consumidor lança exceção ao desserializar e devolve a mensagem para a fila. Em uma hora há 40 mil mensagens circulando e o consumo de CPU do consumidor está no teto.
Como o padrão entra
Com DLQ configurada em 5 tentativas, as mensagens quebradas saem da fila principal e se acumulam no desvio. O alerta dispara, a fila principal volta ao normal, e as mensagens desviadas são reprocessadas depois da correção.
Trade-off: As mensagens ficam paradas até alguém agir — o processamento delas deixa de ser em tempo real. É uma perda real, e ainda assim melhor que travar a fila inteira.
O webhook do parceiro que some
Cenário
Um serviço entrega notificações para URLs cadastradas por clientes. Um cliente desativa o endpoint sem remover o cadastro, e as entregas passam a falhar indefinidamente para aquele destino.
Como o padrão entra
Após o limite de tentativas com backoff, a notificação vai para a DLQ e o endpoint é marcado como suspeito. O cliente recebe um aviso de que as entregas estão falhando.
Trade-off: Um endpoint com instabilidade longa mas legítima pode ser marcado indevidamente, exigindo um caminho de reativação. Sem o limite, porém, um único cliente inativo consumiria capacidade de entrega para sempre.
Onde isto dá errado
DLQ sem alerta
Uma fila de desvio que ninguém observa é um lugar onde mensagens desaparecem em silêncio — com a agravante de todo mundo achar que existe uma rede de segurança. O alerta em profundidade maior que zero é parte do padrão, não um extra.
Guardar só o corpo original
Sem a exceção, o número de tentativas e o momento da falha, quem investiga precisa reproduzir o erro do zero, muitas vezes semanas depois. O contexto custa alguns campos e é o que torna a DLQ diagnosticável.
Não ter caminho de reprocessamento
Corrigido o bug, as mensagens desviadas precisam voltar. Se a única forma for um script escrito às pressas durante o incidente, a DLQ vira arquivo morto. O caminho de volta se constrói antes de precisar dele.
Reprocessar sem idempotência
Boa parte do que cai na DLQ falhou depois de ter efeito parcial. Reprocessar em massa sem chave de idempotência é a forma mais eficiente já inventada de cobrar todo mundo duas vezes.
Limite de tentativas alto demais
Cinquenta tentativas com backoff longo mantêm a mensagem envenenada circulando por horas antes de sair — que é exatamente o problema que a DLQ deveria resolver. Poucas tentativas e um desvio rápido servem melhor.
Uma DLQ só para todas as filas
Misturar desvios de origens diferentes dificulta o diagnóstico e impede reprocessar uma origem sem mexer nas outras. Uma DLQ por fila mantém a investigação e a correção com escopo pequeno.
Classificar, contar e desviar
// ERRADO: a mensagem envenenada volta para a fila para sempre.
// Ela bloqueia a ordem, queima CPU e enche o log — e nunca vai dar certo.
async function consumir(msg: Mensagem) {
try {
await processar(msg);
await msg.confirmar();
} catch {
await msg.devolver(); // volta pro topo... e recomeca o ciclo
}
}
// CERTO: contar tentativas e desviar o que nao tem conserto.
const MAX_TENTATIVAS = 5;
async function consumirComDLQ(msg: Mensagem) {
try {
await processar(msg);
await msg.confirmar();
return;
} catch (erro) {
// Falha de infra (banco fora) merece nova tentativa.
// Falha de conteudo (JSON invalido) nao melhora tentando.
const repetivel = ehTransitoria(erro);
if (repetivel && msg.tentativas < MAX_TENTATIVAS) {
await msg.devolverComAtraso(backoff(msg.tentativas));
return;
}
// O contexto e o que torna a DLQ util: sem o erro e o
// momento, ela vira um cemiterio que ninguem sabe ler.
await dlq.publicar({
original: msg.corpo,
erro: String(erro),
tentativas: msg.tentativas,
falhouEm: msg.recebidoEm,
origem: msg.fila,
});
await msg.confirmar(); // tira da fila principal: ja esta guardada
}
}MAX_TENTATIVAS = 5
def consumir(msg, dlq, agora):
try:
processar(msg.corpo)
msg.confirmar()
return
except Exception as erro:
transitoria = eh_transitoria(erro)
if transitoria and msg.tentativas < MAX_TENTATIVAS:
msg.devolver_com_atraso(backoff(msg.tentativas))
return
# Guardar o suficiente para diagnosticar E reprocessar depois.
dlq.publicar({
"original": msg.corpo,
"erro": repr(erro),
"tentativas": msg.tentativas,
"falhou_em": agora, # injetado, nunca datetime.now() aqui
"origem": msg.fila,
})
msg.confirmar()
# A DLQ so vale se alguem olhar. Um alerta em "profundidade da DLQ > 0"
# e o que separa uma rede de seguranca de um cemiterio silencioso.Quando usar × quando evitar
Use quando
3- Em qualquer consumo de fila em produção.
- Quando o insucesso precisa ser investigado depois, não perdido.
- Quando a ordem importa e uma mensagem travada bloqueia as seguintes.
Evite quando
2- Quando a mensagem perde valor e reprocessar não faz sentido — descartar com métrica é mais honesto.
- Como depósito permanente sem alerta e sem caminho de volta.
Testar o que ficou
6 armadilhas de Dead Letter Queue viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
redrive policy do SQSVocê declara o número máximo de recebimentos e a fila de destino; o broker faz o desvio sozinho.Dead letter exchange do RabbitMQMensagem rejeitada, expirada ou com fila cheia é roteada para uma exchange separada.A caixa de devolvidos do correioO nome vem daí: carta sem destinatário válido não circula para sempre — vai para um setor próprio.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Depois de N falhas, desvia — não trava a fila.
if (tentativas > 5) await dlq.send(msg);O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Uma fila a mais para monitorar, com alarme e processo de reprocessamento
- Mensagens paradas na DLQ são trabalho não feito que exige alguém para investigar
Não vale se: a falha de uma mensagem pode ser simplesmente descartada sem consequência — aí desviar para uma fila é guardar lixo.
Onde isto aparece
Continue por aqui
- Retry com backoff e jitterFalhas passageiras se resolvem sozinhas se você esperar um pouco e tentar de novo. Mas repetir do jeito errado — sem espera crescente, sem ruído aleatório, sem filtrar o que é repetível — transforma um soluço em uma queda.
- IdempotênciaExecutar a mesma operação duas vezes tem o mesmo efeito de executar uma: é o que torna o retry seguro. Em rede, onde 'não sei se foi' é resposta comum, é a diferença entre reenviar e cobrar em dobro.
- WebhooksEm vez de perguntar 'já aconteceu?' de minuto em minuto, você registra uma URL e o outro sistema te chama quando acontece. É o Observer atravessando a internet — com tudo que a rede cobra por isso: assinatura, retry e duplicata.
- SagaCoordena uma transação distribuída de longa duração como uma sequência de transações locais, cada uma com uma ação compensatória para desfazer semanticamente em caso de falha.