main content

Cómo implementar un sistema de gestión de colas y procesamiento de tareas en segundo plano para tu aplicación web a medida

Toda aplicación web tiene un momento de crisis existencial. Llega cuando el usuario pulsa "Generar informe" y el servidor se queda ahí, pensando, durante 12 segundos eternos mientras monta un PDF. O cuando el proceso se cae a mitad de camino y el usuario se queda mirando un spinner hasta el fin de los tiempos. ¿La tentación? Optimizar ese endpoint hasta que sangre. ¿La solución real? Sacarlo del ciclo petición-respuesta por completo. Que lo haga otro. En otro momento. En otro proceso.

Bienvenido al mundo de las colas de mensajes y el procesamiento asíncrono.

Aquí vamos a cubrir la arquitectura, las herramientas y los patrones de diseño que necesitas para que tu aplicación web no se ahogue cuando le pidas algo que tarda más de lo polite.


Por qué el modelo síncrono tiene fecha de caducidad

Piénsalo como una cadena de montaje con un solo operario. Mientras ese operario está soldando una pieza, nadie más puede pasar por la línea. Tu servidor web funciona igual: el ciclo de vida de una petición HTTP tiene un límite de tiempo implícito —y muchas veces explícito, porque proxies como Nginx o load balancers como ALB de AWS cortan conexiones inactivas a los 60 segundos—. Cualquier operación que se acerque a ese umbral, o que simplemente no necesite devolver un resultado al instante, debería procesarse de forma asíncrona.

Los sospechosos habituales:

  • Envío de emails y notificaciones: conectarse a un servidor SMTP o a SendGrid, construir el payload, esperar la respuesta… pueden ser varios segundos. El usuario no tiene por qué quedarse esperando a que un servidor de correo en Virginia le diga "OK".
  • Procesamiento de imágenes: redimensionar, convertir formatos, aplicar watermarks o ejecutar modelos de visión artificial. Operaciones CPU-intensivas que bloquean el event loop si las dejas donde no deben.
  • Generación de PDFs e informes: librerías como Puppeteer, WeasyPrint o JasperReports consumen memoria y tiempo considerables. He visto Puppeteer tragarse 500 MB de RAM para un informe de 20 páginas. No quieres eso en tu proceso principal.
  • Sincronización con sistemas externos: llamadas a ERPs, CRMs o APIs de terceros con latencia variable o límites de tasa. Esa API del ERP que responde en 200 ms cuando le da la gana y en 8 segundos cuando no, no merece bloquear a tu usuario.
  • Indexación en motores de búsqueda: actualizar Elasticsearch o Algolia después de una operación de escritura.

La arquitectura es simple: introduces una cola de mensajes entre el productor (tu API o servidor web) y el consumidor (el worker que ejecuta el trabajo). El productor dice "hay que hacer esto", se desentiende, y el consumidor lo hace cuando puede.


Message brokers: cuál elegir y por qué

Un message broker es el intermediario que recibe los mensajes del productor, los almacena de forma durable y los entrega a los consumidores. No todos sirven para lo mismo, y la elección impacta directamente en la complejidad operacional, las garantías de entrega y el rendimiento. Elegir mal aquí duele durante años.

RabbitMQ

RabbitMQ implementa el protocolo AMQP y lleva más tiempo en producción que la mayoría de frameworks que usamos hoy. Su modelo de exchanges y bindings permite enrutamiento sofisticado: direct exchanges para enviar mensajes a una cola específica, topic exchanges para enrutamiento basado en patrones, y fanout exchanges para broadcast a múltiples colas simultáneamente.

¿Cuándo tiene sentido? Cuando necesitas garantías fuertes de entrega, enrutamiento complejo entre múltiples consumidores con lógica diferente, o cuando tu stack es heterogéneo (productores en Python, consumidores en Java, alguien decidió meter un microservicio en Go). La contrapartida: requiere administración real. Gestionar la topología de exchanges y colas, configurar persistencia, monitorizar el broker… eso no se hace solo.

Redis + BullMQ (o Bull)

Si ya tienes Redis en tu stack —y probablemente lo tienes como caché— BullMQ te da un sistema de colas de alta calidad sin añadir un solo servicio nuevo a tu docker-compose. Prioridades, jobs programados con delay, repetición configurable, y una UI (Bull Board) para inspeccionar el estado de las colas en tiempo real. Todo eso sobre la infraestructura que ya tienes corriendo.

La opción pragmática para equipos con stacks Node.js o TypeScript que quieren velocidad de implementación sin más complejidad operacional. Ojo: las garantías de durabilidad dependen de la configuración de Redis (AOF o RDB persistence). Si tu Redis no persiste a disco y se reinicia, tus jobs desaparecen. Algo que conviene tener muy presente en producción.

Amazon SQS

Si tu infraestructura vive en AWS, SQS elimina por completo la gestión del broker. Servicio gestionado, disponibilidad prácticamente ilimitada, dos modos (Standard con entrega al-menos-una-vez, y FIFO con exactamente-una-vez y ordenación garantizada), e integración nativa con Lambda, ECS y EC2.

El modelo de coste por mensaje lo hace especialmente atractivo cuando tu carga es variable: pagas solo por lo que usas, sin servidores encendidos a las 3 AM procesando cero mensajes. ¿El trade-off? Vendor lock-in. Y un modelo de programación ligeramente distinto (polling vs. push) que puede requerir ajustar tu forma de pensar el consumo de mensajes.


Patrones de diseño para colas de tareas

Work queues (colas de trabajo)

El patrón más simple y probablemente el que implementarás primero: múltiples workers compiten por los mensajes de una misma cola. Cada mensaje lo procesa exactamente un worker. Escalabilidad horizontal natural — añadir workers aumenta el throughput sin tocar nada más. Como añadir cajeros en un supermercado: misma cola, más puntos de atención.

Pero la implementación correcta requiere acknowledgments explícitos. El worker solo confirma el mensaje al broker una vez que lo ha procesado satisfactoriamente. Si el worker muere a mitad del procesamiento —porque alguien ha desplegado, porque el OOM killer ha actuado, porque Murphy—, el broker reencola el mensaje automáticamente. Sin acks explícitos, pierdes mensajes. Así de simple.

Pub/Sub (publicar y suscribir)

Aquí el modelo cambia: un evento publicado se entrega a todos los suscriptores registrados. Piensa en ello como un periódico — cada suscriptor recibe su propia copia. Cuando se crea un pedido, emites un evento order.created al que suscriben independientemente el servicio de emails, el servicio de inventario y el sistema de analytics. Cada uno procesa el mismo evento sin que el productor sepa nada de ellos. Desacoplamiento puro.

RabbitMQ implementa esto con fanout exchanges. En ecosistemas de microservicios, suele complementarse con un event bus como Kafka para mayor durabilidad del log de eventos.

Delayed jobs y jobs programados

BullMQ y RabbitMQ (con el plugin rabbitmq_delayed_message_exchange) permiten encolar un mensaje para que sea procesado en un momento futuro. Recordatorios, expiraciones de sesión, envíos de emails diferidos, reintentos con backoff exponencial… Todo eso se resuelve con delayed jobs.

// Ejemplo con BullMQ: encolar un email para dentro de 24 horas
await emailQueue.add(
  'send-followup',
  { userId: 123, template: 'onboarding-day2' },
  { delay: 24 * 60 * 60 * 1000 }
);

Tres líneas. El usuario se registra hoy, mañana recibe el email de seguimiento. Sin cron jobs artesanales, sin tablas de "cosas pendientes" consultadas cada minuto.


Dead Letter Queues y estrategias de reintento

Uno de los errores más dolorosos al implementar colas: ignorar qué pasa cuando un job falla. Lo he visto demasiadas veces. Sin estrategia de gestión de fallos, los mensajes que lanzan excepciones se pierden silenciosamente o —peor aún— entran en un bucle de reintentos infinito que colapsa al worker mientras genera 40 GB de logs por hora.

Una Dead Letter Queue (DLQ) es una cola secundaria a la que el broker mueve automáticamente los mensajes que han superado el número máximo de intentos o que han expirado. Tener una DLQ te permite:

  1. Investigar el mensaje original y el motivo del fallo.
  2. Corregir el bug en el worker.
  3. Reimportar los mensajes fallidos a la cola principal.

Tres pasos. Nada se pierde, todo se puede recuperar. Compáralo con "el job falló y nadie se enteró hasta que el cliente llamó".

La estrategia de reintento recomendada es backoff exponencial con jitter: primer reintento a 1 segundo, segundo a 2, tercero a 4, y así sucesivamente, añadiendo un factor aleatorio para evitar que todos los workers reintenten al mismo tiempo. Ese factor aleatorio es lo que te salva del "thundering herd" — veinte workers haciendo retry simultáneo contra un servicio que acaba de recuperarse. Receta para tumbarlo otra vez.

En BullMQ:

await queue.add('process-image', payload, {
  attempts: 5,
  backoff: {
    type: 'exponential',
    delay: 1000,
  },
});

Un matiz que mucha gente olvida: separa los errores transitorios (timeout de red, servicio externo caído) de los errores permanentes (datos malformados, bug en el código). Para los segundos, reintentar no tiene sentido. Márcalos como fallidos inmediatamente y envíalos a la DLQ. Reintentar un JSON roto cinco veces no va a arreglarlo.


Monitorización: la cola que no ves te mata

Un sistema de colas sin observabilidad es una bomba de relojería con la mecha invisible. Las métricas que necesitas tener en un dashboard desde el día uno:

  • Queue depth: número de mensajes pendientes. Un crecimiento sostenido significa que los workers no dan abasto. Si ves esa línea subir y no bajar, tienes un problema que empeora con cada minuto.
  • Processing time por tipo de job: identifica qué tareas son los cuellos de botella. ¿El 80% del tiempo lo consume el generador de PDFs? Ahí tienes tu próxima optimización.
  • Error rate y tasa de DLQ: necesitas alertas cuando superen umbrales predefinidos.
  • Worker throughput: mensajes procesados por segundo por worker. Tu línea base para decisiones de escalado.

Prometheus es el estándar para recoger estas métricas en aplicaciones cloud-native. La mayoría de los clientes de BullMQ, RabbitMQ y SQS tienen exportadores para Prometheus. Combinado con Grafana para la visualización y Alertmanager para las notificaciones, tienes un stack de observabilidad completo y probado en miles de sistemas de producción.

Para RabbitMQ, el plugin de gestión expone una API HTTP con métricas que Prometheus puede scrape directamente. Para SQS, CloudWatch proporciona métricas nativas exportables a Prometheus con cloudwatch-exporter.


Escalado de workers

La ventaja fundamental de las work queues: el escalado horizontal es trivial. Más workers = más throughput. Sin coordinación entre ellos, porque el broker se encarga de distribuir los mensajes. Cada worker es independiente, stateless, reemplazable. Como debería ser.

Escalado automático con Kubernetes

Si tu infraestructura corre en Kubernetes, KEDA (Kubernetes Event-Driven Autoscaler) permite escalar automáticamente el número de pods de workers en función del queue depth. Cola con más de 100 mensajes pendientes: KEDA lanza más workers. Cola vacía: los reduce a cero. Pagas solo por el compute que realmente necesitas.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: worker-scaler
spec:
  scaleTargetRef:
    name: image-worker
  triggers:
  - type: rabbitmq
    metadata:
      queueName: image-processing
      value: "100"

Concurrencia dentro del worker

Cada instancia de worker puede procesar múltiples jobs en paralelo mediante threads o async/await. En BullMQ, el parámetro concurrency controla cuántos jobs ejecuta simultáneamente cada worker. Para tareas I/O-bound (llamadas a APIs externas), valores de 10-50 son razonables. Para tareas CPU-bound (procesamiento de imágenes), no superes el número de cores disponibles — poner concurrency 50 en una máquina de 4 cores para tareas CPU-bound solo consigue context switching y peor rendimiento.


Implementación práctica: por dónde empezar sin sobredimensionar

La arquitectura mínima viable para procesamiento asíncrono tiene tres componentes. Ni más ni menos:

  1. El productor: tu API REST o GraphQL que, en lugar de ejecutar la tarea, la encola y devuelve un 202 Accepted con un jobId. El usuario recibe respuesta inmediata, la tarea se ejecuta en background.
  2. El broker: RabbitMQ, Redis/BullMQ o SQS según tu stack.
  3. El worker: un proceso independiente que lee de la cola y ejecuta la lógica de negocio.

¿Cómo elegir? Para aplicaciones Node.js de tamaño medio, BullMQ sobre Redis tiene la menor fricción de arranque. Si ya estás en AWS y necesitas escalar a millones de mensajes por día, SQS con Lambda es difícil de superar en relación coste-complejidad. Para sistemas con enrutamiento complejo entre múltiples servicios heterogéneos, RabbitMQ sigue siendo la herramienta con más flexibilidad.

Si estás construyendo tu aplicación desde cero y necesitas tomar esta decisión con conocimiento de causa —teniendo en cuenta tu stack, tu equipo y tus necesidades de escala— es exactamente el tipo de decisión arquitectónica donde la experiencia de alguien que ya ha pasado por esto marca la diferencia entre acertar a la primera y refactorizar dentro de seis meses.

Habla con nuestro equipo de arquitectura


Colas bien hechas: la diferencia entre escalar y rezar

Implementar un sistema de colas no es complejidad gratuita — es la inversión que separa las aplicaciones que se rompen bajo carga de las que absorben picos sin despeinarse. La clave: elegir la herramienta adecuada para tu contexto, implementar acknowledgments y DLQs desde el primer día (no "cuando tengamos tiempo"), y medir todo con Prometheus o equivalente.

Procesamiento asíncrono bien diseñado no solo mejora el rendimiento. Mejora la resiliencia, porque los fallos son recuperables. Mejora la observabilidad, porque cada job tiene estado trazable. Mejora la experiencia de usuario, porque las respuestas son inmediatas con feedback de progreso. Tres beneficios que juntos justifican sobradamente cada línea de configuración extra.