Saltar al contenido

Colas de mensajes en la práctica

RabbitMQ, Redis Streams o Kafka — los patrones de mensajería detrás de sistemas desacoplados y resilientes, y cuándo conviene usar cada herramienta.

4 min de lectura
Arquitectura de una cola de mensajes que muestra publicadores, temas y grupos de consumidores

Las colas de mensajes son la columna vertebral de los sistemas distribuidos. Desacoplan a los productores de los consumidores, absorben picos de tráfico y permiten una comunicación confiable entre servicios que no necesitan estar en línea al mismo tiempo. Pero elegir entre RabbitMQ, Kafka, Redis Streams o SQS sin entender los patrones subyacentes lleva a soluciones sobrediseñadas.

Punto a punto vs. Pub/Sub

Todo patrón de mensajería entra en una de dos categorías.

tstypescript
// Point-to-point: one message, one consumer
// Use case: task distribution (send email, process payment)
// Each message is processed by exactly ONE worker
 
// Pub/Sub: one message, many consumers
// Use case: event broadcasting (order placed → notify analytics, inventory, email)
// Each message is delivered to ALL subscribers
PatrónEntregaConsumidoresEjemplo
Punto a puntoUn consumidor por mensajeWorkers en competenciaColas de trabajo, distribución de tareas
Pub/SubTodos los suscriptoresConsumidores independientesDifusión de eventos, notificaciones
Fan-outTodos los consumidores, cada uno recibe una copiaProcesamiento independienteSincronización entre múltiples sistemas

Cuándo necesitas una cola de mensajes

No toda interacción entre servicios necesita una cola. Las llamadas HTTP directas funcionan bien cuando:

  • La respuesta se necesita de inmediato
  • Ambos servicios deben estar disponibles
  • La falla debe ser visible para quien hace la llamada

Las colas aportan valor cuando:

  • El trabajo puede posponerse
  • Los servicios tienen requisitos de disponibilidad distintos
  • El tráfico es irregular y necesita amortiguación
tstypescript
// ❌ Synchronous chain — one failure breaks everything
app.post("/api/orders", async (req, res) => {
  const order = await createOrder(req.body);
  await inventoryService.reserve(order.items);      // If this fails...
  await paymentService.charge(order.total);          // ...none of this runs
  await emailService.sendConfirmation(order);        // ...the user sees an error
  await analyticsService.trackPurchase(order);
  res.json(order);
});
 
// ✅ Async event — order is placed, downstream services react independently
app.post("/api/orders", async (req, res) => {
  const order = await createOrder(req.body);
 
  await messageQueue.publish("order.placed", {
    orderId: order.id,
    items: order.items,
    total: order.total,
    customerId: order.customerId,
  });
 
  res.json(order);
});
 
// Each service consumes the event independently
// inventory-service listens to "order.placed"
// payment-service listens to "order.placed"
// email-service listens to "order.placed"
// analytics-service listens to "order.placed"

Garantías de entrega

Los sistemas de mensajería ofrecen distintos niveles de confiabilidad en la entrega.

tstypescript
// At-most-once: fire and forget
// Message may be lost, but never duplicated
// Use for: metrics, logging, non-critical notifications
 
// At-least-once: guaranteed delivery, possible duplicates
// Consumer MUST be idempotent
// Use for: most business events (payments, emails, orders)
 
// Exactly-once: no loss, no duplicates (very expensive)
// Requires distributed transactions or deduplication
// Use for: financial transactions (or use at-least-once + idempotency)

En la práctica, at-least-once con consumidores idempotentes es la opción correcta por defecto. La entrega exactly-once es una garantía teórica extremadamente costosa de implementar correctamente.

RabbitMQ: broker de mensajería tradicional

RabbitMQ se destaca en enrutamiento, confirmación de mensajes y colas punto a punto.

tstypescript
import amqp from "amqplib";
 
// Producer
async function publishOrderEvent(order: Order) {
  const connection = await amqp.connect(process.env.RABBITMQ_URL!);
  const channel = await connection.createChannel();
 
  await channel.assertExchange("orders", "topic", { durable: true });
 
  channel.publish(
    "orders",
    "order.placed",
    Buffer.from(JSON.stringify(order)),
    { persistent: true }, // Survive broker restarts
  );
}
 
// Consumer
async function startOrderConsumer() {
  const connection = await amqp.connect(process.env.RABBITMQ_URL!);
  const channel = await connection.createChannel();
 
  await channel.assertExchange("orders", "topic", { durable: true });
  const queue = await channel.assertQueue("inventory-service", {
    durable: true,
  });
  await channel.bindQueue(queue.queue, "orders", "order.placed");
 
  channel.prefetch(10); // Process 10 messages at a time
 
  channel.consume(queue.queue, async (msg) => {
    if (!msg) return;
 
    try {
      const order = JSON.parse(msg.content.toString());
      await reserveInventory(order);
      channel.ack(msg); // Acknowledge successful processing
    } catch (error) {
      channel.nack(msg, false, true); // Requeue on failure
    }
  });
}

Kafka: streaming de eventos

Kafka no es una cola tradicional, sino un registro de confirmaciones distribuido. Los mensajes se persisten y pueden reproducirse.

tstypescript
import { Kafka } from "kafkajs";
 
const kafka = new Kafka({
  clientId: "order-service",
  brokers: [process.env.KAFKA_BROKER!],
});
 
// Producer
const producer = kafka.producer();
await producer.connect();
 
await producer.send({
  topic: "orders",
  messages: [
    {
      key: order.id, // Partition by order ID for ordering guarantees
      value: JSON.stringify({ event: "order.placed", data: order }),
    },
  ],
});
 
// Consumer group — messages distributed among group members
const consumer = kafka.consumer({ groupId: "inventory-service" });
await consumer.connect();
await consumer.subscribe({ topic: "orders", fromBeginning: false });
 
await consumer.run({
  eachMessage: async ({ topic, partition, message }) => {
    const event = JSON.parse(message.value!.toString());
 
    if (event.event === "order.placed") {
      await reserveInventory(event.data);
    }
  },
});

Cómo elegir la herramienta adecuada

CaracterísticaRabbitMQKafkaRedis StreamsSQS
EnrutamientoAvanzado (exchanges, bindings)Topics + particionesSimpleBásico
Reproducción de mensajesNo (consumido = perdido)Sí (log persistente)LimitadaNo
Throughput~50K msg/s~1M msg/s~100K msg/s~3K msg/s
OrdenPor colaPor particiónPor streamSolo colas FIFO
Complejidad operativaMediaAltaBaja (si ya usas Redis)Ninguna (administrado)

Usa RabbitMQ para enrutamiento complejo, colas con prioridad y distribución tradicional de tareas. Usa Kafka para streaming de eventos de alto throughput con capacidad de reproducción. Usa Redis Streams para colas livianas cuando ya tienes Redis. Usa SQS cuando quieras cero sobrecarga operativa en AWS.

Puntos clave

  1. No toda llamada entre servicios necesita una cola — úsalas para trabajos diferibles, independientes o con tráfico irregular
  2. At-least-once + consumidores idempotentes es el estándar práctico por defecto para una mensajería confiable
  3. RabbitMQ para enrutamiento, Kafka para streaming de eventos de alto throughput, Redis Streams para colas livianas
  4. Confirma siempre los mensajes de forma explícita — la confirmación automática arriesga la pérdida de mensajes ante fallas del consumidor
  5. Particiona y usa como clave el ID de la entidad para garantizar el orden dentro de los eventos de una misma entidad
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX