O problema sem dead letter queues
Sem uma dead letter queue (DLQ), mensagens que falham no processamento são descartadas ou ficam em loop infinito de retry, bloqueando a fila principal.
As duas alternativas ruins:
- Requeue eterno: a mensagem volta para a fila e bloqueia processamento de outras mensagens
- Descartar em caso de erro: você perde dados de produção silenciosamente
DLQs resolvem isso: mensagens que não conseguem ser processadas vão para uma fila separada, preservadas para investigação e reprocessamento.
Como funciona no RabbitMQ
Uma mensagem vai para a dead letter exchange (DLX) quando:
- É rejeitada (
nackoureject) comrequeue: false - Expira por TTL
- A fila atinge o limite máximo de mensagens
Producer → [main-queue] → Consumer
↓ (nack ou TTL)
[dlx-exchange] → [dead-letter-queue]
Configuração com @nestjs/microservices
Declarando as filas
// rabbitmq.setup.ts
import { Connection } from "amqplib";
export async function setupQueues(connection: Connection) {
const channel = await connection.createChannel();
// 1. Declara a dead letter exchange
await channel.assertExchange("dlx", "direct", { durable: true });
// 2. Declara a dead letter queue
await channel.assertQueue("orders.dead-letter", {
durable: true,
arguments: {
"x-message-ttl": 7 * 24 * 60 * 60 * 1000, // mensagens expiram em 7 dias
},
});
// 3. Bind DLQ → DLX
await channel.bindQueue("orders.dead-letter", "dlx", "orders");
// 4. Declara a fila principal com referência ao DLX
await channel.assertQueue("orders.processing", {
durable: true,
arguments: {
"x-dead-letter-exchange": "dlx", // onde vai quando falha
"x-dead-letter-routing-key": "orders", // routing key na DLX
"x-message-ttl": 30_000, // TTL de 30s na fila principal
"x-max-length": 10_000, // máximo de 10k mensagens
},
});
}Consumer com tratamento de erro
@Injectable()
export class OrdersConsumer {
private readonly logger = new Logger(OrdersConsumer.name);
@RabbitSubscribe({
exchange: "orders",
routingKey: "orders.process",
queue: "orders.processing",
queueOptions: {
durable: true,
arguments: {
"x-dead-letter-exchange": "dlx",
"x-dead-letter-routing-key": "orders",
},
},
})
async handleOrder(
@Payload() message: OrderMessage,
@RabbitMQMessage() rawMessage: ConsumeMessage
) {
try {
await this.orderService.process(message);
// Ack implícito em caso de sucesso
} catch (error) {
this.logger.error(
`Falha ao processar order ${message.orderId}: ${error.message}`,
{ messageId: rawMessage.properties.messageId }
);
// Erros transientes: requeue para retry
if (this.isTransient(error)) {
throw error; // @golevelup/nestjs-rabbitmq faz nack + requeue
}
// Erros permanentes (dados inválidos, etc): vai para DLQ
// Nack sem requeue → DLX
throw new Nack(false);
}
}
private isTransient(error: Error): boolean {
return (
error.message.includes("ECONNREFUSED") ||
error.message.includes("timeout") ||
error instanceof ServiceUnavailableException
);
}
}Retry com backoff antes da DLQ
Em vez de ir direto para a DLQ, implemente filas de retry com TTL crescente:
[orders.processing] →(falha)→ [orders.retry.10s] →(TTL 10s)→ [orders.processing]
↓ (3ª falha)
[orders.dead-letter]
export async function setupQueuesWithRetry(channel: Channel) {
// Retry exchange (envia de volta para a fila principal após TTL)
await channel.assertExchange("retry-exchange", "direct", { durable: true });
// Filas de retry com TTL crescente
const retryDelays = [10_000, 30_000, 120_000]; // 10s, 30s, 2min
for (const delay of retryDelays) {
await channel.assertQueue(`orders.retry.${delay}ms`, {
durable: true,
arguments: {
"x-message-ttl": delay,
"x-dead-letter-exchange": "", // default exchange
"x-dead-letter-routing-key": "orders.processing", // volta para a fila principal
},
});
}
}Para implementar isso, use o header x-death que o RabbitMQ adiciona automaticamente:
async handleOrder(message: OrderMessage, rawMessage: ConsumeMessage) {
const deathCount = this.getDeathCount(rawMessage);
try {
await this.orderService.process(message);
} catch (error) {
if (deathCount >= 3) {
// Esgotou as tentativas: vai para DLQ
throw new Nack(false);
}
// Envia para a fila de retry correspondente
const delays = [10_000, 30_000, 120_000];
const delay = delays[deathCount] ?? delays[delays.length - 1];
await this.channel.publish("retry-exchange", `orders.retry.${delay}ms`, message, {
headers: rawMessage.properties.headers,
});
// Ack para remover da fila principal (o retry cuida do resto)
// Nack(false) iria para DLQ, não queremos isso aqui
}
}
private getDeathCount(rawMessage: ConsumeMessage): number {
const deaths = rawMessage.properties.headers?.["x-death"];
if (!Array.isArray(deaths)) return 0;
return deaths.reduce((acc, d) => acc + (d.count ?? 0), 0);
}Consumer da DLQ
A DLQ precisa de atenção ativa. Configure alertas e um consumer de monitoramento:
@Injectable()
export class DeadLetterConsumer {
private readonly logger = new Logger(DeadLetterConsumer.name);
@RabbitSubscribe({
exchange: "dlx",
routingKey: "orders",
queue: "orders.dead-letter",
})
async handleDeadLetter(
@Payload() message: OrderMessage,
@RabbitMQMessage() rawMessage: ConsumeMessage
) {
const deaths = rawMessage.properties.headers?.["x-death"];
this.logger.error("Mensagem na DLQ", {
orderId: message.orderId,
reason: deaths?.[0]?.reason,
firstFailureAt: deaths?.[0]?.time,
attempts: this.getDeathCount(rawMessage),
});
// Salva no banco para investigação
await this.deadLetterRepo.save({
payload: message,
headers: rawMessage.properties.headers,
reason: deaths?.[0]?.reason,
failedAt: new Date(),
});
// Alerta via Slack/PagerDuty se for crítico
if (this.isCritical(message)) {
await this.alertService.notify(
`Order ${message.orderId} na DLQ após ${this.getDeathCount(rawMessage)} tentativas`
);
}
}
}Endpoint de reprocessamento
Após investigar e corrigir o problema, você pode reprocessar mensagens da DLQ:
@Post("dead-letter/reprocess/:id")
@UseGuards(AdminGuard)
async reprocess(@Param("id") id: string) {
const deadLetter = await this.deadLetterRepo.findById(id);
if (!deadLetter) throw new NotFoundException();
await this.ordersPublisher.publish(deadLetter.payload);
await this.deadLetterRepo.markAsReprocessed(id);
return { message: "Mensagem enviada para reprocessamento" };
}Conclusão
Dead letter queues são a rede de segurança do sistema de mensageria. Sem elas, falhas silenciosas levam a perda de dados ou loops infinitos. Com elas, você tem visibilidade sobre o que falhou, por quê, e controle para reprocessar quando o problema for corrigido.
Configure DLQs desde o início: é muito mais difícil adicionar depois quando já tem mensagens em produção.