Azure Service Bus: cuándo usar colas y cuándo topics
Una cola absorbe picos y fallos temporales; un topic reparte el mismo evento a varios consumidores. Cuándo usar cada uno en Service Bus y qué cuidar.
El endpoint que confirma un pedido hace cinco cosas: guarda el pedido, genera la factura, envía el correo, descuenta inventario y notifica a la bodega. Tarda tres segundos. Y si el servicio de correo falla, la confirmación completa falla, aunque el pedido ya se había guardado.
Mover parte de ese trabajo a una cola cambia el problema por completo: el usuario recibe su confirmación en 200 ms, y lo demás ocurre después, con reintentos si algo falla. Pero una cola también agrega una pieza que hay que entender y operar.
Cuándo una cola resuelve un problema real
- Trabajo que no necesita terminar antes de responder: facturas, correos, notificaciones, integraciones.
- Picos de carga: la cola absorbe 10.000 pedidos en un minuto y los consumidores los procesan a su ritmo, sin tumbar la base de datos.
- Dependencias que fallan: si el servicio de correo cae una hora, los mensajes esperan y se procesan cuando vuelve, en lugar de perderse.
- Desacoplar despliegues: el productor y el consumidor pueden actualizarse por separado.
Si el usuario necesita el resultado para continuar (por ejemplo, saber si el pago fue aprobado), una cola no es la herramienta: necesita una respuesta síncrona.
Colas: un mensaje, un consumidor
En una cola, cada mensaje lo procesa un solo consumidor. Si hay tres instancias leyendo, se reparten el trabajo:
await using var client = new ServiceBusClient(fullyQualifiedNamespace, new DefaultAzureCredential());
var sender = client.CreateSender("invoices-to-generate");
await sender.SendMessageAsync(new ServiceBusMessage(BinaryData.FromObjectAsJson(new { OrderId = order.Id }))
{
MessageId = $"invoice-{order.Id}", // permite detectar duplicados si está activado
Subject = "GenerateInvoice"
});
Ideal para trabajo: "genera esta factura", "procesa este archivo". Una tarea, alguien la hace.
Topics: un evento, varios suscriptores
Un topic entrega una copia del mensaje a cada suscripción. Cada suscripción funciona como una cola propia:
Topic: order-placed
├── Suscripción: billing → genera la factura
├── Suscripción: inventory → descuenta el stock
└── Suscripción: email → envía la confirmación
El productor publica "se creó un pedido" sin saber quién lo escucha. Agregar un nuevo interesado (por ejemplo, analítica) es crear una suscripción, sin tocar el código del productor.
Las suscripciones pueden tener filtros: la de "pedidos grandes" solo recibe los mensajes con Total > 5000000, sin que el consumidor tenga que descartar el resto.
Regla práctica: una cola para comandos (algo que alguien debe hacer); un topic para eventos (algo que pasó y a varios les puede interesar).
Procesar mensajes de forma confiable
var processor = client.CreateProcessor("invoices-to-generate", new ServiceBusProcessorOptions
{
AutoCompleteMessages = false, // completamos solo si todo salió bien
MaxConcurrentCalls = 8, // protege a la base de datos
PrefetchCount = 16
});
processor.ProcessMessageAsync += async args =>
{
var message = args.Message.Body.ToObjectFromJson<InvoiceRequest>();
await invoices.GenerateAsync(message!.OrderId, args.CancellationToken);
await args.CompleteMessageAsync(args.Message);
};
processor.ProcessErrorAsync += args =>
{
logger.LogError(args.Exception, "Error procesando mensajes de {EntityPath}", args.EntityPath);
return Task.CompletedTask;
};
await processor.StartProcessingAsync();
Con el modo peek-lock (el predeterminado), el mensaje queda bloqueado mientras se procesa. Si el consumidor falla o no lo completa a tiempo, vuelve a la cola y se reintenta.
Lo que hay que cuidar en producción
Los mensajes pueden llegar más de una vez
Service Bus garantiza entrega al menos una vez, no exactamente una vez. Si el consumidor genera la factura y se cae justo antes de completar el mensaje, el mensaje vuelve y la factura se genera de nuevo. Los consumidores tienen que ser idempotentes: verificar si la factura ya existe antes de crearla, o usar una restricción única en la base de datos.
La cola de mensajes muertos
Después de un número de intentos (MaxDeliveryCount, 10 por defecto), el mensaje pasa a la dead-letter queue. Si nadie la mira, son pedidos sin factura que nadie sabe que existen. Una alerta sobre la cantidad de mensajes muertos es tan importante como la cola misma.
El orden no está garantizado
Con varios consumidores en paralelo, los mensajes se procesan en cualquier orden. Si el orden importa (por ejemplo, eventos de un mismo pedido), las sesiones de Service Bus garantizan que los mensajes con el mismo SessionId se procesen en orden y por un solo consumidor a la vez.
Publicar después de guardar
await db.SaveChangesAsync(ct);
await sender.SendMessageAsync(message, ct); // si esto falla, el pedido existe pero nadie se entera
El guardado y el envío no son atómicos. La solución robusta es el patrón outbox: guardar el mensaje en una tabla dentro de la misma transacción y que un proceso aparte lo publique. Lo vimos al hablar de transacciones y llamadas externas.
El trade-off
Una cola mejora la resiliencia y el tiempo de respuesta, a cambio de:
- Consistencia eventual: la factura aparece segundos después del pedido, y la interfaz tiene que contemplarlo.
- Más piezas que operar: mensajes muertos, reintentos, monitoreo del tamaño de la cola.
- Depuración más difícil: un flujo que antes era una pila de llamadas ahora cruza procesos.
Cuándo no usar colas
- Cuando el trabajo es rápido, confiable y el usuario necesita el resultado.
- En sistemas pequeños, donde un proceso en segundo plano que lee una tabla de tareas pendientes resuelve lo mismo sin infraestructura nueva. Si ese proceso necesita escalar con los eventos, Azure Functions es el paso intermedio.
- Para comunicar dos partes del mismo proceso que siempre se despliegan juntas.
Conclusión
Colas para trabajo que alguien debe hacer; topics para eventos que varios pueden escuchar. En los dos casos, consumidores idempotentes, alertas sobre los mensajes muertos y cuidado con el momento de publicar. Con eso, la mensajería deja de ser una fuente de sorpresas y se convierte en lo que promete: un sistema que sigue funcionando aunque alguna de sus partes falle.
Lleva tu aplicación a la nube con criterio
Diseñamos arquitecturas en Azure que crecen con tu negocio sin disparar los costos de infraestructura.
Ver soluciones escalables