StackPractices
intermediate Por Mathias Paulenko

Patrón Message Queue Load Leveling

Suaviza picos de tráfico colocando una cola entre el productor y el consumidor. El productor escribe a cualquier ritmo; el consumidor procesa a ritmo constante.

Descripción General

Cuando un servicio recibe tráfico irregular, puede saturar sistemas descendentes que no están diseñados para picos. Una base de datos puede manejar 50 consultas por segundo de forma constante pero caerse a 500 en un pico. El patrón Message Queue Load Leveling coloca una cola entre el productor y el consumidor para que el productor escriba mensajes a cualquier ritmo mientras el consumidor los procesa a su propio ritmo constante.

Cuándo Usar

  • El tráfico hacia un sistema descendente es irregular y el sistema no maneja picos
  • Necesitas desacoplar la tasa de peticiones de la tasa de procesamiento
  • Las tareas no son sensibles al tiempo (los usuarios no necesitan respuestas inmediatas)
  • Quieres escalar consumidores independientemente de los productores
  • Trabajos en segundo plano como generación de reportes, envío de emails o procesamiento de archivos
  • Necesitas entrega confiable — los mensajes persisten en la cola incluso si el consumidor está temporalmente offline

Cuándo Evitar

  • Requests de usuario en tiempo real. Load leveling añade latencia de cola. Si el usuario espera respuesta, procesa de forma síncrona.
  • Orden estricto entre todos los mensajes. Múltiples consumidores rompen el orden. Usa un consumidor único o el patrón Sequential Convoy.
  • Tráfico bajo sin picos. Si el tráfico se mantiene consistentemente bajo, la cola añade complejidad sin devolverte nada.
  • Los mensajes deben procesarse en una ventana de tiempo específica. Los delays de cola pueden empujar mensajes más allá de su deadline.
  • No puedes tolerar procesamiento duplicado. Las colas pueden reentregar. Si la idempotencia está fuera de la mesa, elige otra arquitectura.

Solución

Python (Celery + Redis)

from celery import Celery
import time

app = Celery("tasks", broker="redis://localhost:6379", backend="redis://localhost:6379")

# El consumidor procesa una tarea a la vez a su propio ritmo
@app.task(bind=True, max_retries=3)
def process_order(self, order_id):
    try:
        # Simula procesamiento lento (escrituras DB, llamadas API)
        time.sleep(2)
        print(f"Pedido {order_id} procesado")
        return {"status": "done", "order_id": order_id}
    except Exception as exc:
        raise self.retry(exc=exc, countdown=5)

# El productor encola a cualquier ritmo
def submit_orders(order_ids):
    for order_id in order_ids:
        process_order.delay(order_id)
    print(f"{len(order_ids)} pedidos encolados")

if __name__ == "__main__":
    # Pico: 1000 pedidos enviados al instante
    # El consumidor los procesa 1 a la vez cada 2 segundos
    submit_orders(range(1000))

JavaScript (BullMQ + Redis)

import { Queue, Worker } from "bullmq";

const orderQueue = new Queue("orders", {
  connection: { host: "localhost", port: 6379 },
});

// Productor: encolar a cualquier ritmo
async function submitOrders(orderIds) {
  const jobs = orderIds.map((id) => ({
    name: "process-order",
    data: { orderId: id },
  }));
  await orderQueue.addBulk(jobs);
  console.log(`${orderIds.length} pedidos encolados`);
}

// Consumidor: procesar a ritmo controlado
const worker = new Worker(
  "orders",
  async (job) => {
    // Simula procesamiento lento
    await new Promise((resolve) => setTimeout(resolve, 2000));
    console.log(`Pedido ${job.data.orderId} procesado`);
    return { status: "done", orderId: job.data.orderId };
  },
  {
    connection: { host: "localhost", port: 6379 },
    concurrency: 1, // Procesa una a la vez
    limiter: { max: 1, duration: 2000 }, // Máx 1 job cada 2 segundos
  }
);

// Pico: 1000 pedidos enviados al instante
await submitOrders(Array.from({ length: 1000 }, (_, i) => i));

Java (RabbitMQ + Spring AMQP)

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class OrderProcessor {

    private final RabbitTemplate rabbitTemplate;

    public OrderProcessor(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    // Productor: enviar a cualquier ritmo
    public void submitOrders(List<Integer> orderIds) {
        for (Integer orderId : orderIds) {
            rabbitTemplate.convertAndSend("orders", "order." + orderId, orderId);
        }
        System.out.println(orderIds.size() + " pedidos encolados");
    }

    // Consumidor: procesar una a la vez
    @RabbitListener(queues = "orders", concurrency = "1")
    public void processOrder(Integer orderId) {
        try {
            Thread.sleep(2000); // Simula procesamiento lento
            System.out.println("Pedido " + orderId + " procesado");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Explicación

La cola funciona como un buffer entre productor y consumidor. El productor empuja mensajes tan rápido como puede; el consumidor los extrae al ritmo que puede manejar. Si el productor envía 1.000 mensajes en un segundo pero el consumidor procesa uno cada 2 segundos, la cola crece a 1.000 mensajes y se vacía durante los siguientes 2.000 segundos.

Mermaid flowchart LR diagram

El ciclo se cierra con el acknowledgment: tras procesar, el consumidor acusa recibo del mensaje. Si cae antes de acusar, el broker reentrega el mensaje a otro consumidor — por eso la cola también te da entrega confiable, no solo suavizado.

El insight clave es el desacoplamiento de tasas: la tasa del productor y la del consumidor son independientes, y la cola absorbe la diferencia durante los picos. Protege al sistema descendente de la saturación a costa de latencia — los mensajes esperan en la cola hasta que un consumidor puede tomarlos. Para trabajo sensible al tiempo, sube la concurrencia del consumidor o usa el patrón Priority Queue.

Variantes

VarianteTipo de ColaCaso de UsoCompromiso
Consumidor ÚnicoCola FIFOOrden estricto, simpleThroughput lento
Múltiples ConsumidoresCola FIFOMayor throughputSin garantía de orden
Cola de PrioridadCola prioritariaAlgunos mensajes son urgentesComplejidad en la lógica de prioridad
Retardo ProgramadoCola con delayProcesar en momentos específicosLos mensajes esperan hasta el horario
Procesamiento por LotesConsumidor batchAgrupar mensajes para eficienciaMayor latencia por mensaje

Qué Funciona

  • Dimensiona la cola para el volumen de pico esperado dividido por la tasa de procesamiento del consumidor — con un margen de seguridad de 2x encima
  • Monitorea la profundidad de cola y alerta cuando supera un umbral (el 80 % de capacidad te da tiempo de escalar antes de que se llene)
  • Escala consumidores horizontalmente cuando la profundidad se mantiene alta; para load leveling quieres promedio + ~20 % de margen, no capacidad de pico
  • Usa dead-letter queues para mensajes que fallan tras los máximos reintentos
  • Establece visibility timeouts para que el mensaje de un consumidor caído se reentregue en vez de perderse
  • Mantén consumidores idempotentes — los brokers pueden reentregar, y lo harán
  • Reintenta fallos con backoff exponencial (1s, 2s, 4s, 8s) para que las tormentas de reintentos no golpeen al consumidor

Errores Comunes

  • Sin monitoreo de profundidad de cola: una cola creciente significa que los consumidores no dan abasto. Sin monitoreo, te enteras cuando la cola se queda sin almacenamiento.
  • Consumidor muy lento para tráfico sostenido: load leveling maneja picos, no sobrecarga sostenida. Si la tasa promedio de producción supera la de consumo, la cola crece para siempre.
  • No manejar mensajes venenosos: un mensaje que siempre falla bloquea al consumidor. Muévelo a una DLQ tras N reintentos y alerta.
  • Productor síncrono esperando al consumidor: contradice todo el propósito. El productor debe fire-and-forget.
  • Ignorar el orden de mensajes: múltiples consumidores lo rompen. Si el orden importa, usa un consumidor único o particiona por clave para que cada clave caiga en un solo consumidor.
  • Bloquear al productor cuando falla la cola: cuando el broker cae, decide de antemano — fallar rápido, caer a procesamiento síncrono o bufferizar localmente y reintentar.
  • Mensajes perdidos al reiniciar: persiste los mensajes antes de acusar recibo, para que una caída del consumidor a mitad de proceso no tire el trabajo.

Ejemplos del Mundo Real

Amazon SQS + Lambda

Una plataforma de e-commerce bufferiza mensajes de pedidos en SQS durante rebajas flash. Los pedidos llegan a 50.000/s pero el backend de pagos maneja 5.000/s, así que SQS absorbe el pico mientras las funciones Lambda lo vacían a una concurrencia controlada de 5.000/s. Sin la cola, el servicio de pagos caería en los primeros segundos de la venta. Consulta las cuotas y límites de SQS para restricciones de retención y tamaño.

RabbitMQ en Sistemas Financieros

Una plataforma de trading recibe bursts de datos de mercado al abrir la jornada. Las colas de RabbitMQ absorben el burst mientras el servicio de analítica lo procesa a ritmo constante — sin load leveling, el servicio caería bajo el pico de apertura.

Azure Service Bus para IoT

Una plataforma IoT recolecta telemetría de millones de dispositivos que se reconectan en ráfagas tras cortes de red. Las colas de Service Bus bufferizan las tormentas de reconexión mientras los servicios backend ingieren a tasa controlada, evitando ahogar la base de datos.

Ver También

Preguntas frecuentes

¿En qué se diferencia del patrón Producer-Consumer?

Load Leveling se centra en suavizar picos de tráfico mediante un buffer en cola. Producer-Consumer es un patrón general de concurrencia para dividir trabajo entre threads o servicios. Load Leveling es una aplicación específica de aquel, con énfasis en el desacoplamiento de tasas.

¿Cuál es la diferencia entre load leveling y rate limiting?

Rate limiting rechaza requests sobre un umbral; load leveling los encola para después. Rate limiting protege el sistema descartando trabajo, load leveling lo protege bufferizándolo. Usa rate limiting cuando el trabajo es prescindible, load leveling cuando debe hacerse sí o sí.

¿Qué pasa si la cola crece demasiado?

Escala consumidores, descarta mensajes de baja prioridad o empuja backpressure hacia el productor (HTTP 429, un flag compartido en Redis, una cola de control). También conviene limitar los consumidores al número de particiones — los extra no reciben nada — y alertar sobre la profundidad para ver el crecimiento antes de que la cola se llene.

¿Cómo elijo el tamaño correcto de cola?

Estima el volumen de pico y divídelo por la tasa de procesamiento del consumidor. Un pico de 10.000 mensajes con consumidores a 100/s necesita una cola que contenga 10.000. Añade un margen de seguridad de 2x y después observa la profundidad real para ajustar.

¿Debería usar un servicio de cola administrado o auto-alojado?

Los servicios administrados (SQS, Azure Service Bus, Cloud Pub/Sub) cubren escalado, durabilidad y monitoreo de fábrica. El auto-alojado (RabbitMQ, listas de Redis) da más control a costa de trabajo de ops. Para la mayoría de equipos, lo administrado gana.

¿Puedo usar este patrón para requests de usuario en tiempo real?

No — añade latencia por diseño. Para requests donde los usuarios esperan respuesta, usa procesamiento síncrono con circuit breakers y timeouts. Load leveling pertenece al trabajo en segundo plano.

¿Cómo manejo el orden de mensajes con múltiples consumidores?

Múltiples consumidores rompen el orden global. Si el orden importa, particiona los mensajes por una clave (ID de cliente, ID de pedido) para que cada clave caiga en un solo consumidor — las partition keys de Kafka y los message groups de SQS FIFO funcionan así. Un consumidor único preserva el orden total pero limita el throughput.

¿Cómo testeo load leveling?

Envía una ráfaga de mensajes y confirma que la cola crece, que los consumidores procesan a la tasa configurada y que la cola se vacía después. Mata un consumidor a mitad del test y comprueba que los mensajes se reentregan. Después haz load test con volúmenes realistas antes de darlo por hecho.