Registro / artículo

RabbitMQ: exchanges, ACKs y el trabajo que no puede perderse

Facturar un pedido significa generar un PDF de 40 páginas, firmarlo y enviarlo por correo. Tarda entre tres y ocho segundos. Lo metes dentro de la petición HTTP porque total, “solo pasa una vez por pedido”.

Un martes con campaña, el endpoint acumula 200 peticiones concurrentes, el pool de workers se llena y el gateway empieza a devolver 504. El usuario reintenta. Ahora tienes tres facturas para el mismo pedido, un cliente enfadado y un PDF que nadie envió.

El patrón correcto aquí no es Kafka ni un log de eventos. Es una cola de trabajo: encolar la tarea, devolver un 202 y que un worker la procese a su ritmo. RabbitMQ es la herramienta clásica para eso, y sigue siendo excelente en 2026.

Pero RabbitMQ no es “una cola”. Es un enrutador de mensajes con un modelo propio, y la mayoría de los incidentes vienen de configurarlo como si fuera una cola simple.

El modelo mental: productor, exchange, binding, cola

En AMQP 0-9-1 —el protocolo que habla RabbitMQ— el productor nunca escribe en una cola. Escribe en un exchange, y el exchange decide a qué colas va el mensaje según sus bindings y la routing key.

productor ──▶ exchange ──binding──▶ cola ──▶ consumidor
                  │
                  ├──binding──▶ cola
                  └──binding──▶ cola

Esa indirección es la que permite enrutar sin tocar el productor. El productor publica order.created y no sabe —ni le importa— cuántas colas hay detrás.

En un restaurante pasa lo mismo: el camarero no lleva la comanda directa a la plancha. La deja en el pase, que la reparte a cocina, barra o postres. La comanda espera en el raíl de cada estación hasta que un cocinero la coge, y solo se tira cuando el plato está terminado. Productor, exchange, cola y ACK, con delantal.

Hay cuatro tipos de exchange y elegir mal es la causa número uno de topologías insostenibles:

Exchange Cómo enruta Cuándo usarlo
direct routing key exacta una tarea, un destino
fanout a todas las colas vinculadas notificar a varios servicios
topic patrón con comodines (order.*, *.failed) enrutamiento por categoría
headers por cabeceras del mensaje casos raros, enrutamiento por atributos

Para el 90% de los sistemas, direct y topic alcanzan. headers existe, funciona y casi nadie lo necesita.

Un detalle que sorprende: si publicas a un exchange y ningún binding coincide, el mensaje se descarta en silencio. RabbitMQ no te avisa. Para detectarlo, publicas con el flag mandatory y consumes los basic.return, o usas una alternate exchange que reciba lo no enrutado. Es un fallo de configuración que en producción se traduce en “el evento se perdió y no sabemos dónde”.

La topología mínima que sí aguanta

Para el caso de la factura, la topología es pequeña:

channel.exchange_declare(exchange="orders", exchange_type="topic", durable=True)
channel.queue_declare(queue="invoices", durable=True)
channel.queue_bind(queue="invoices", exchange="orders", routing_key="order.created")

channel.basic_publish(
    exchange="orders",
    routing_key="order.created",
    body=payload,
    properties=pika.BasicProperties(delivery_mode=2),  # persistente
)

Cuatro decisiones en esas líneas:

  • durable=True en exchange y cola: la definición sobrevive a un reinicio del broker.
  • delivery_mode=2: el mensaje se escribe a disco, no solo en memoria.
  • routing_key: el contrato entre productor y topología. Cámbialo con cuidado.
  • Cola con nombre explícito: las colas anónimas y exclusivas sirven para RPC, no para trabajo duradero.

Y una quinta que falta en el snippet y es imprescindible: publisher confirms. Sin confirms, basic_publish no garantiza que el broker haya recibido nada. El mensaje puede perderse entre tu proceso y el broker y tú no te enteras.

channel.confirm_delivery()  # el publish lanza excepción si el broker no confirma

Durable + persistente + confirms: las tres. Si te falta una, tienes una falsa sensación de durabilidad. Es el equivalente a un INSERT sin COMMIT.

El consumidor: prefetch, ACK y el error que todos cometemos

Del lado del consumidor, RabbitMQ entrega mensajes y espera una confirmación explícita (basic_ack). Si el consumidor muere antes de confirmar, el mensaje vuelve a la cola y se reentrega.

channel.basic_qos(prefetch_count=20)

def on_message(ch, method, properties, body):
    try:
        process(body)
        ch.basic_ack(delivery_tag=method.delivery_tag)
    except RecoverableError:
        # fallo temporal: el mensaje vuelve a la cola
        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
    except Exception:
        # fallo definitivo: se rechaza y acaba en la DLQ
        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

El prefetch_count es la palanca más importante y la que más se ignora. Sin él, RabbitMQ empuja todos los mensajes disponibles al consumidor. Un worker lento acumula 50.000 mensajes en memoria, el resto de workers se queda sin trabajo y el sistema entero se degrada. Con prefetch_count=20, cada consumidor tiene como máximo 20 mensajes en vuelo y el reparto es justo.

¿Cuánto vale 20? Depende del tiempo de proceso: si cada tarea tarda 2 segundos, un prefetch alto es desperdicio; si tarda 20 ms, uno bajo deja al consumidor esperando. Empieza en 10-50 y mídelo.

El segundo error clásico es confirmar antes de procesar. basic_ack al principio del handler significa que, si el worker muere, la factura nunca se generó y el mensaje ya no existe. Confirma al final, siempre.

Y como en cualquier sistema de entrega al menos una vez, vas a procesar duplicados. La solución es la misma que en cualquier broker: idempotencia. Un índice único sobre order_id en la tabla de facturas convierte el reintento en un no-op.

Fallos: DLX, reintentos y mensajes venenosos

Un mensaje que siempre falla —un JSON corrupto, una referencia inexistente— no puede quedarse rebotando para siempre. Si lo reencolas sin límite, bloqueas la cola y llenas los logs.

El patrón estándar en RabbitMQ:

  1. La cola principal tiene un dead-letter exchange configurado.
  2. Cuando un mensaje se rechaza sin requeue, o expira su TTL, va a la DLX.
  3. La DLX lo enruta a una cola de descarte que alguien revisa.
  4. Para reintentos con espera, se usa una cola intermedia con TTL y DLX de vuelta a la principal: el mensaje espera 30 segundos y vuelve.

No hay un “retry con backoff” nativo. Se construye con TTL y dead-lettering, y conviene tenerlo claro antes de prometerlo en una reunión.

Dos recomendaciones de operación:

  • Colas de quórum para todo lo que no pueda perderse. Son colas replicadas por Raft, con consistencia real. Son la opción recomendada desde RabbitMQ 4.0, que retiró las antiguas colas clásicas replicadas.
  • Límites de longitud (x-max-length o x-max-length-bytes) con política de descarte a la DLX. Una cola sin límite es un disco lleno esperando su momento.

Orden: lo que RabbitMQ sí y no te da

Una cola entrega mensajes en orden… mientras haya un solo consumidor. En cuanto pones tres workers compitiendo, el orden se rompe: el mensaje 2 puede terminar antes que el 1.

Si necesitas orden por entidad —pedidos de un mismo cliente, operaciones de una misma cuenta— tienes dos opciones:

  • Una cola por entidad, con un consumidor por cola. Escala mal si tienes millones de entidades.
  • Single Active Consumer: RabbitMQ elige un consumidor activo por cola y los demás esperan. Orden garantizado, paralelismo limitado.

Si tu requisito real es orden por clave a gran escala con replay, eso ya no es terreno de RabbitMQ. Es terreno de un log particionado. Lo vemos en el artículo de Kafka.

Números honestos

  • Un nodo de RabbitMQ mueve decenas de miles de mensajes por segundo con latencias de milisegundos. Para la mayoría de sistemas empresariales, sobra.
  • El rendimiento se degrada con colas largas: millones de mensajes acumulados significan más memoria, más disco y arranques lentos. Una cola sana se vacía rápido.
  • El tamaño máximo de mensaje por defecto ronda los 128 MB, pero mandar payloads grandes por el broker es una mala idea. Guarda el archivo en S3 y envía la referencia: el patrón claim-check.
  • Las colas de quórum son más seguras y algo más lentas que las clásicas. Es un precio que casi siempre vale la pena.

Cuándo NO usar RabbitMQ

  • Necesitas replay o auditoría. Cuando un mensaje se confirma, desaparece. No hay forma de volver a procesar los últimos tres días sin que alguien los reenvíe.
  • Varios equipos necesitan el mismo flujo con historia independiente. Un fanout copia el mensaje a varias colas, pero cada copia se borra al consumirse. No es un log.
  • Volumen masivo de eventos con retención. Millones por segundo y días de historia: eso es Kafka.
  • Quieres cero operaciones. RabbitMQ hay que instalarlo, actualizarlo, monitorizarlo y saber qué hacer cuando el disco se llena. Si no quieres eso, mira SQS y SNS.

Checklist antes de poner RabbitMQ en producción

  • ¿Exchange y colas son durable, y los mensajes persistentes?
  • ¿Tienes publisher confirms activados?
  • ¿Hay una alternate exchange o mandatory para detectar mensajes no enrutados?
  • ¿Cada consumidor tiene un prefetch_count razonable?
  • ¿Se confirma después de procesar, nunca antes?
  • ¿Los handlers son idempotentes?
  • ¿Hay DLX y una política de reintentos con límite?
  • ¿Las colas tienen límite de longitud y alertas de backlog?
  • ¿Usas colas de quórum para lo que no puede perderse?

RabbitMQ hace una cosa muy bien: repartir trabajo y enrutarlo. Si ese es tu problema, no necesitas nada más grande. El problema empieza cuando le pides que sea un log de eventos: ahí ya no es la herramienta adecuada.

Si quieres ver el mapa completo —RabbitMQ, Kafka, SQS y SNS— con la matriz de decisión y los costes, la guía de elección cierra la serie.

Para seguir leyendo