Colas de mensajes fallidos (DLQ) en SQS, RabbitMQ y Kafka
Configura dead letter queues en SQS, RabbitMQ y Kafka: políticas de reenvío, límites de reintentos, mensajes tóxicos y replay seguro.
Visión General
Las dead letter queues (DLQs) capturan mensajes que fallan en el procesamiento después de varios intentos en arquitecturas orientadas a eventos. Sin una, un mensaje malformado puede bloquear una partición de la cola para siempre o desaparecer sin dejar rastro. Una DLQ bien gestionada separa los mensajes tóxicos (payloads que nunca van a procesarse) de los fallos transitorios, de modo que el equipo de operaciones pueda inspeccionar, reprocesar o descartar mensajes fallidos sin tocar el flujo principal.
Los ejemplos de abajo configuran una DLQ funcional en SQS, RabbitMQ y Kafka, y el resto de la receta cubre las prácticas operativas que mantienen una DLQ útil en lugar de convertirla en un cementerio.
Cuándo Usar
Usa este recurso cuando:
- Los consumidores encuentran errores irrecuperables como payloads malformados, esquemas incompatibles o referencias faltantes.
- Un solo mensaje defectuoso puede bloquear una partición o todos los mensajes encolados detrás de él.
- Operaciones necesita visibilidad sobre los mensajes fallidos para revisión manual o auditorías de cumplimiento.
- Necesitas un tope de reintentos para que los fallos transitorios no entren en un bucle infinito.
Una DLQ es excesiva cuando la pérdida de mensajes es aceptable y el broker ya reentrega a bajo costo (pings de presencia, métricas no críticas). Tampoco aporta mucho cuando el procesamiento es idempotente y tan barato que un reintento sin límite cuesta menos que operar una segunda cola.
Solución
Amazon SQS — política de reenvío (AWS CLI)
# Crear la cola principal y su DLQ
aws sqs create-queue --queue-name orders-queue
aws sqs create-queue --queue-name orders-dlq
# Resolver las URLs y el ARN de la DLQ
QUEUE_URL=$(aws sqs get-queue-url --queue-name orders-queue --query 'QueueUrl' --output text)
DLQ_URL=$(aws sqs get-queue-url --queue-name orders-dlq --query 'QueueUrl' --output text)
DLQ_ARN=$(aws sqs get-queue-attributes --queue-url "$DLQ_URL" \
--attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
# Enviar el mensaje a la DLQ después de 3 recepciones fallidas
aws sqs set-queue-attributes \
--queue-url "$QUEUE_URL" \
--attributes '{
"RedrivePolicy": "{\"deadLetterTargetArn\":\"'$DLQ_ARN'\",\"maxReceiveCount\":3}"
}'
RabbitMQ — dead-letter exchange (Python + pika)
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
# Dead-letter exchange (DLX) y su cola
channel.exchange_declare(exchange='orders.dlx', exchange_type='direct')
channel.queue_declare(queue='orders-dlq', durable=True)
channel.queue_bind(queue='orders-dlq', exchange='orders.dlx', routing_key='failed')
# La cola principal enruta los mensajes rechazados o expirados al DLX
args = {
'x-dead-letter-exchange': 'orders.dlx',
'x-dead-letter-routing-key': 'failed',
'x-message-ttl': 300000 # 5 minutos
}
channel.queue_declare(queue='orders', durable=True, arguments=args)
def handle_message(ch, method, properties, body):
try:
process_order(body)
ch.basic_ack(delivery_tag=method.delivery_tag)
except UnrecoverableError:
# requeue=False envía el mensaje al DLX
ch.basic_reject(delivery_tag=method.delivery_tag, requeue=False)
channel.basic_consume(queue='orders', on_message_callback=handle_message)
channel.start_consuming()
Kafka — dead-letter topic (Node.js + KafkaJS)
Kafka no tiene una DLQ integrada; el consumidor publica en un topic separado cuando se agotan los reintentos.
const { Kafka } = require('kafkajs');
async function main() {
const kafka = new Kafka({ brokers: ['localhost:9092'] });
const consumer = kafka.consumer({ groupId: 'order-processors' });
const producer = kafka.producer();
await consumer.connect();
await producer.connect();
await consumer.subscribe({ topic: 'orders', fromBeginning: false });
await consumer.run({
eachMessage: async ({ topic, partition, message }) => {
try {
await processOrder(JSON.parse(message.value.toString()));
} catch (err) {
// Publicar en la DLQ con el contexto necesario para depurar y reprocesar
await producer.send({
topic: 'orders-dlq',
messages: [{
key: message.key,
value: message.value,
headers: {
'error.type': err.name,
'error.message': err.message,
'original.topic': topic,
'original.partition': String(partition),
'original.offset': String(message.offset),
'retry.count': '3'
}
}]
});
}
}
});
}
main().catch(console.error);
Código companion: el repo companion de dead-letter-queue tiene estos ejemplos como archivos ejecutables.
Explicación
Un mensaje acaba en la DLQ cuando ni el broker ni el consumidor ven forma de entregarlo al flujo principal. Cada broker toma esa decisión de forma distinta.
| Condición | Cuándo enviar a la DLQ | Acción |
|---|---|---|
| Reintentos máximos agotados | Después de N intentos fallidos | Mover a la DLQ |
| Mensaje imposible de parsear | JSON inválido, esquema incompatible | Mover de inmediato |
| Dependencia faltante | El registro referenciado no existe | Reintentar, luego DLQ |
| Violación de regla de negocio | Pedido de un producto inexistente | Mover de inmediato |
Reintentar un mensaje tóxico es trabajo desperdiciado, así que los casos irrecuperables deberían saltarse los reintentos e ir directo a la DLQ. En RabbitMQ eso es basic_reject con requeue=False; en SQS es el maxReceiveCount de la política de reenvío; en Kafka es la lógica del propio consumidor, porque el broker no sabe nada de los fallos de procesamiento.
Monitoriza la DLQ como una métrica de primera clase. Alerta cuando la profundidad supere un umbral (10 mensajes es un punto de partida razonable) y cuando el mensaje más antiguo supere tu SLA de incidentes: una DLQ que crece es un indicador temprano de un problema sistémico, no un detalle de gestión de colas. Adjunta el trace ID original a cada mensaje de la DLQ para seguirlo a través del tracing distribuido y correlacionar el fallo con el despliegue o la dependencia que lo causó.
Un detalle importante en Kafka: publicar en un topic DLQ confirma el offset de origen, así que los mensajes posteriores de la partición siguen fluyendo. Si necesitas orden por clave, pausa el flujo de esa clave o restablece el orden en la lógica de replay en lugar de asumir que la DLQ preserva la secuencia.
Variantes
| Broker | Mecanismo DLQ | Configuración |
|---|---|---|
| AWS SQS | Política de reenvío | maxReceiveCount + ARN destino |
| RabbitMQ | Dead-letter exchange | x-dead-letter-exchange |
| Kafka | Gestionado por el consumidor | Topic separado + lógica del productor |
| Azure Service Bus | DLQ integrada | MaxDeliveryCount agotado, expiración de TTL o dead-letter explícito |
| Google Pub/Sub | Dead-letter topic | deadLetterPolicy.maxDeliveryAttempts |
SQS y Azure Service Bus gestionan el dead-lettering a nivel de broker; RabbitMQ enruta a través de un exchange que declaras tú; Kafka lo deja todo en manos del código cliente. Para una guía centrada solo en RabbitMQ con triggers de TTL y longitud máxima, consulta la receta de dead-letter queues en RabbitMQ. Para la visión de patrón de diseño de la misma idea, consulta el patrón Dead Letter Channel.
Lo que funciona
- Pon un tope real de reintentos: 3-5 intentos equilibran el tiempo de recuperación con la presión sobre la cola; combínalo con backoff exponencial para que los fallos transitorios se agoten antes de llegar a la DLQ.
- Sella los mensajes de la DLQ con contexto: cola o topic de origen, tipo de error, número de reintentos y trace ID. El ejemplo de KafkaJS muestra el patrón de headers; sin él, operaciones no puede distinguir “JSON corrupto” de “base de datos caída”.
- Separa las DLQs por tipo de fallo: los mensajes con esquema inválido nunca deberían reprocesarse, mientras que los fallos de dependencia sí, una vez que la dependencia se recupere. Con DLQs separadas, cada tipo de fallo tiene reglas de replay propias.
- Alerta por profundidad y antigüedad, no por existencia: el primer mensaje en una DLQ es normal; una cola que no para de crecer es la señal que importa. Muéstrala en los mismos dashboards que llevan tu recolección de métricas para que la señal no quede enterrada.
- Reprocesa en lotes después de la corrección: republica un puñado de mensajes, observa la tasa de error y después vacía el resto. Un replay masivo a ciegas vuelve a disparar el fallo original a escala.
Errores Comunes
- No tener DLQ: los mensajes fallidos desaparecen silenciosamente o bloquean a los consumidores para siempre.
- Requeue infinito: reencolar sin un máximo crea procesamiento perpetuo y puede paralizar una partición entera.
- Una DLQ sin monitorizar: dejada a su suerte se convierte en cementerio en lugar de red de seguridad; nadie la mira hasta que llega la auditoría.
- Sin metadatos del fallo: sin el tipo de error y el número de reintentos, cada triage de la DLQ empieza de cero.
- Una sola DLQ compartida por todos los topics: un mensaje tóxico de un topic de poco valor se mezcla con fallos críticos y rompe el replay por topic.
- Reprocesar sin arreglar la causa raíz: el mismo error vuelve, ahora multiplicado por cada mensaje reprocesado.
- Tratar la DLQ como cola de reintentos: una DLQ guarda mensajes que ya agotaron todos los reintentos; si quieres reentrega programada, eso es una cola de reintentos con backoff, un mecanismo distinto.
Operación de la DLQ
- Herramientas de inspección: dale a operaciones una forma de leer los mensajes de la DLQ sin consumirlos: cuerpo, headers, cola de origen, motivo del fallo y timestamp, con filtros por tipo de error o fecha. La consola de SQS y la UI de gestión de RabbitMQ (puerto 15672) lo permiten; Kafka necesita una herramienta de UI o un pequeño consumidor que inspeccione sin confirmar offsets.
- Pipeline de replay: construye un script o acción de administración que republique los mensajes de la DLQ en la cola de origen. Soporta replay selectivo por tipo de error o rango de fechas, incluye un modo dry-run y registra cada evento de replay para auditoría. Valida cada mensaje contra el esquema actual antes de reencolarlo: reprocesar un mensaje anterior a un cambio de esquema solo lo vuelve a envenenar.
- Retención: expira los mensajes de la DLQ pasado un período mayor que tu SLA de incidentes (7-14 días es lo habitual). Si el cumplimiento exige un rastro de auditoría, archiva los mensajes expirados en almacenamiento frío antes de borrarlos.
- DLQs multinivel: enruta los fallos transitorios (timeouts, cortes de conexión) a una DLQ reintentable y los fallos permanentes (validación, esquema) a una terminal. Cada una tiene reglas de replay y umbrales de alerta distintos.
- Dimensionado y circuit breaker: planea que entre el 1 y el 5% del volumen aterrice en la DLQ en condiciones normales. Un pico sostenido por encima de eso es señal de pausar consumidores o activar un circuit breaker, no de seguir absorbiendo fallos; el runbook para incidentes de DLQ cubre el procedimiento operativo.
Lectura Adicional
- Dead-letter queues de Amazon SQS — referencia oficial de la política de reenvío.
- Dead-letter exchanges de RabbitMQ — documentación oficial del DLX.
- Dead-letter queues de Azure Service Bus — semántica de la DLQ integrada.
- Dead-letter topics de Google Pub/Sub — referencia de
deadLetterPolicy. - Dead Letter Channel — Enterprise Integration Patterns — la descripción original del patrón.
- Runbook de dead letter queues — plantilla operativa para inspección, análisis de causa raíz y replay.
Preguntas frecuentes
¿Debería reprocesar los mensajes de la DLQ automáticamente?
No hasta que hayas encontrado y corregido la causa raíz del fallo. Un replay a ciegas desperdicia recursos y puede volver a disparar el mismo error en cada mensaje reprocesado.
¿Cuánto tiempo debería conservar los mensajes de la DLQ?
Más que tu SLA de respuesta a incidentes. 7-14 días es lo habitual; archiva en almacenamiento barato más allá de eso si necesitas el rastro de auditoría.
¿Cuál es la diferencia entre una DLQ y una cola de reintentos?
Las colas de reintentos retienen mensajes para reprocesarlos de forma programada. La DLQ, en cambio, es donde acaban los mensajes cuando se han agotado todos los intentos.
¿Cada cola debería tener su propia DLQ?
Para cualquier flujo que no puedas permitirte perder, sí. Las DLQs por cola mantienen limpio el contexto del fallo y hacen seguro el replay; una DLQ compartida solo es aceptable para topics de poco valor con un tratamiento idéntico.
¿Una DLQ preserva el orden de los mensajes?
No por defecto. Al pasar a la DLQ, el mensaje sale de la secuencia mientras los posteriores siguen fluyendo; si el orden importa para una clave de agregado, pausa el flujo de esa clave o gestiona el orden durante el replay.
Recursos Relacionados
Configurar Dead-Letter Queues en RabbitMQ para Mensajes
Configurar dead-letter queues y exchanges en RabbitMQ con expiracion TTL, limites de longitud, routing por rechazo y patrones de retry para mensajeria resiliente.
PatternPatrón Dead Letter Channel
Rutar mensajes no procesables a una cola de mensajes muertos separada para inspeccion y replay. Evitar que mensajes venenosos bloqueen la cola principal indefinidamente.
DocRunbook de Dead Letter Queue
Runbook para handlear y replayear dead letter queue messages en Kafka y RabbitMQ: DLQ setup, inspection procedures, root cause analysis, replay strategies, monitoring alerts y automation scripts para failed message recovery.
RecipeIdempotencia en Procesamiento de Mensajes
Hacé que el procesamiento de mensajes sea idempotente para que entregas duplicadas no generen side effects en sistemas event-driven.
RecipeRetry Backoff: Budgets, Deadlines y Config de Librerías
Implementa estrategias de retry resilientes con exponential backoff, jitter e integración de circuit breaker para recuperación de fallas transitorias.
GuideColas de Mensajes — Inmersión en RabbitMQ, Kafka y SQS
Referencia Detallada sobre colas de mensajes: cuándo usar RabbitMQ, Kafka o SQS. Cubre patrones, throughput, ordenamiento y consideraciones operativas.