Ejecutar el redimensionamiento de imágenes dentro de una ruta de Express es una receta para el desastre. Un usuario sube una foto de diez megabytes, tu servidor comienza a procesar píxeles y, treinta segundos después, la solicitud agota el tiempo de espera. Las colas de trabajos en segundo plano existen precisamente para evitar este tipo de problemas. En el ecosistema de Node.js, Bull y BullMQ se han convertido en los dos pesos pesados para manejar el trabajo asíncrono a través de Redis. Comparten ADN, pero divergen drásticamente en su filosofía y ergonomía diaria. Elegir el adecuado es importante porque cambiar más adelante no es una simple actualización de paquete.

La base compartida

Ambas librerías utilizan Redis como su columna vertebral. Redis gestiona operaciones atómicas, conjuntos ordenados (sorted sets) para trabajos retrasados y pub/sub para eventos. Si ya utilizas Redis para caché o sesiones, añadir una cola de trabajos no requiere nueva infraestructura. Tanto Bull como BullMQ admiten prioridades, reintentos con backoff, controles de concurrencia y trabajos repetibles. Esa superposición hace que la elección sea más difícil, no más fácil. No puedes limitarte a una lista de características. En su lugar, tienes que observar cómo cada librería quiere que estructures tu código.

Bull: El veterano probado en batalla

Bull lleva años existiendo y se ejecuta en miles de aplicaciones de producción. Funciona. La API envuelve todo en una única instancia de Queue. La instancias, defines una función de procesamiento y escuchas eventos, todo en el mismo objeto. Este diseño monolítico resulta familiar si vienes de patrones más antiguos de Node.js. Las bases de código anteriores a la adopción generalizada de async/await encajan con Bull de forma natural, ya que creció junto a los callbacks y los primeros clientes de Redis.

La desventaja es el acoplamiento estrecho. Cuando tu servidor de API crea un trabajo, importa el mismo objeto Queue que contiene la lógica del worker. En la práctica, esto significa que tu proceso web arrastra dependencias que nunca llega a ejecutar. No es un fallo fatal, pero molesta a la arquitectura limpia. Para cargas de trabajo sencillas, puede que nunca lo notes. Para equipos grandes con docenas de módulos, la fricción se acumula.

BullMQ: Una reconstrucción desde cero

BullMQ es el sucesor oficial. Fue reescrito en TypeScript desde el primer día, por lo que los tipos no son algo añadido a posteriori sobre el código fuente de JavaScript. La API divide las responsabilidades en clases distintas. Queue se encarga de añadir trabajos. Worker se encarga de procesarlos. QueueEvents se encarga de la observabilidad. Esta separación refleja cómo operan realmente los sistemas distribuidos modernos. Tus pods de API solo necesitan la clase Queue y una conexión a Redis. Tus pods de worker importan la clase Worker. El límite es físico, no solo conceptual.

Este cambio rinde frutos en equipos grandes. Un desarrollador que lanza una nueva funcionalidad puede encolar un trabajo sin saber qué archivo contiene el procesador. El compilador detecta los desajustes de tipos entre los datos del trabajo y los manejadores (handlers) de forma temprana, en lugar de hacerlo en tiempo de ejecución. La API async/await también se siente nativa en el Node.js moderno. No te encontrarás luchando contra convenciones heredadas.

Flujos de trabajo: De trucos improvisados a ciudadanos de primera clase

Los flujos de trabajo de múltiples pasos exponen la brecha más amplia entre las dos librerías.

Supongamos que estás construyendo un pipeline de facturación para un e-commerce. Un cliente realiza una compra. Necesitas reservar inventario, cobrar una tarjeta, generar un PDF y enviar un correo electrónico. Con Bull, encadenar estos pasos significa llevar una gestión manual. Podrías tener un procesador que dispare el siguiente trabajo, pasando el estado a través de Redis o de cargas de datos pesadas. Tú mismo escribes la coordinación padre-hijo. Funciona hasta que deja de funcionar. La lógica de reintentos se vuelve caótica. Si el paso del PDF falla, revertir el cobro requiere código de compensación personalizado que es fácil de implementar incorrectamente.

BullMQ introduce FlowProducer. Defines un árbol de trabajos donde los padres esperan automáticamente a sus hijos. En el ejemplo de facturación, creas un trabajo raíz llamado finalize-order con tres hijos: reserve-inventory, charge-payment y generate-pdf. Puedes hacer que la notificación por email sea un hijo del trabajo del PDF. Redis almacena la estructura del grafo. El padre solo se activa cuando todas las dependencias tienen éxito. Si un hijo falla, toda la rama se detiene. No tienes que escribir bucles de sondeo (polling loops) ni generadores de trabajos recursivos. Esto no es azúcar sintáctica. Cambia la forma en que modelas la lógica de negocio.

Limitación de tasa: Instrumento contundente vs. bisturí

Ambas librerías pueden limitar el rendimiento (throughput), pero la granularidad difiere enormemente.

Bull applies rate limits per queue. If you set a queue to process one hundred jobs per second, that ceiling covers every job in the queue equally. This is fine for homogeneous workloads. It breaks down in multitenant SaaS platforms. Imagine one noisy customer dumping a million webhook deliveries into a shared queue. Bull's queue-level limit means you cannot slow that tenant without slowing everyone else. Your options are ugly. Spin up separate Redis queues per customer and manage them dynamically, or accept the unfairness.

BullMQ adds group-based rate limiting. You tag each job with a group key, typically a tenant or user ID, and define limits per group. The same queue processes jobs for all tenants, but the scheduler throttles each group independently. A burst from Customer A does not starve Customer B. You avoid queue sprawl and keep your Redis keyspace tidy. For platforms with noisy-neighbor concerns, this alone can justify the migration.

Cleaner Architecture in Practice

The separation of Queue and Worker is subtle until you debug a production incident. With Bull, it is common to see job creation code deep in route handlers that also import heavy processing dependencies. BullMQ forces you to decide where work happens. Your web servers stay lean. Your worker containers bundle the heavy libraries, image processors, or headless browsers. If a memory leak appears, you know exactly which process type to profile. The mental model is closer to systems like Celery or Sidekiq.

Making the Choice

Start with BullMQ if you are laying fresh tracks. The TypeScript definitions are accurate and complete. Job flows eliminate reams of orchestration code. Group rate limiting solves fairness problems before they start. The async/await API feels native. There is little reason to choose the older library for a greenfield project.

Stay on Bull if it is already working. Migrations cost time and risk stability. If your jobs are flat and independent, you are not missing features you actually need. A queue that sends password reset emails and resizes avatars does not need flow graphs. Rewriting working code for theoretical purity is not engineering. It is hobbyism.

Migration Reality Check

If you do switch, treat it as an infrastructure change, not a code refactor. Bull and BullMQ use different Redis key schemas. They cannot read each other's job data or state. You cannot flip a feature flag and hope old jobs finish. You must drain every existing queue to zero, deploy the new workers, and start enqueueing with BullMQ. Plan for a maintenance window or a blue-green deployment where old workers consume the legacy queue while new workers handle the new one.

The Real Take