Aprendiendo BullJS
Feb 23, 2022
Updated: Jun 24, 2026

Aprendiendo BullJS

Es bastante común encontrarnos con un proyecto en nuestra vida de programador en el que se nos presenten requerimientos muy específicos en cuanto a funciones y su ejecución. Por ejemplo, podría tratarse de funciones en JavaScript que necesiten ejecutarse un número determinado de veces, que deban reintentarse en caso de fallar, o que requieran priorización para saber cuál ejecutar primero, entre muchas otras cosas. En este contexto es donde los sistemas de manejo de colas para Node.js empiezan a tomar protagonismo. En este artículo nos centraremos en uno en particular: BullJS.

BullJS es una librería soportada por Node.js diseñada para estos escenarios, pero que además persiste la información en una base de datos Redis. No se queda ahí, también ofrece paralelismo de tareas, notificaciones entre productores y consumidores, y seguimiento del progreso de las tareas, entre otros.

El proyecto se autodefine como: "El sistema de colas más rápido y confiable basado en Redis para Node.js, cuidadosamente escrito para garantizar estabilidad, solidez y atomicidad".

Nota importante (BullMQ vs. `bull` clásico): Este post cubre el paquete clásico bull (npm install bull, import Queue from "bull"). Hoy los mantenedores (Taskforce.sh) tratan bull clásico como modo de mantenimiento y recomiendan BullMQ para todos los proyectos nuevos. Los conceptos que verás aquí (colas, jobs, opciones de delay/attempts, procesamiento, eventos) siguen siendo válidos y se trasladan casi directamente a BullMQ, pero algunas APIs cambian de nombre. Si vas a empezar un proyecto desde cero, instala bullmq y consulta su guía de migración. A lo largo del post te marco las diferencias relevantes.

Aquí encontrarás los enlaces a la documentación oficial:

  • Guide, tu punto de entrada para comenzar a desarrollar (documentación de BullMQ).
  • Repositorio, repositorio oficial de BullMQ.
  • Bull clásico, repositorio del paquete bull original, en el que se basa este tutorial.

Comenzando

Para instalar BullJS, primero necesitas tener Node.js instalado y, después, ejecutar el siguiente comando:

npm install bull

Como mencionamos anteriormente, Bull necesita una base de datos Redis, ya que es el lugar donde se almacenan y administran los "jobs" y los mensajes. Si tienes Docker instalado en tu máquina, puedes ejecutar:

docker run --name my-redis-container -p 6379:6379 -d redis

Esto iniciará una base de datos local de Redis que estará ejecutándose en 127.0.0.1:6379.

Introducción con un ejemplo

Imagina que queremos implementar un "job" (en BullJS las tareas se denominan "jobs") que consiste en lo siguiente: "7 días después de que alguien se suscriba a nuestro boletín de noticias, queremos enviarle un correo electrónico que contenga un enlace para calificar su experiencia de suscripción en nuestro sitio web".

BullJS tiene dos elementos principales que definen todo el ecosistema para trabajar: las colas o "Queues" y las tareas o "Jobs". Primero, veamos las colas.

Queues en BullJS

Una cola es un objeto de JavaScript que puede producir y consumir jobs. En nuestro ejemplo vamos a llamar a una cola newsLetterMail, pero tú puedes ponerle el nombre que desees. Al crear una instancia de una cola debemos especificar el host y el puerto de nuestra base de datos Redis, ya que el predeterminado es 127.0.0.1:6379. A continuación, veamos cómo se vería esto:

// Importa la clase Queue de la biblioteca "bull"
import Queue from "bull";

// Crea una nueva instancia de Queue llamada "newsLetterMailQueue" para manejar el envío de correos de boletines informativos
const newsLetterMailQueue = new Queue("newsLetterMail", {
  // Configura la conexión con el servidor Redis, que se utilizará para almacenar y gestionar las tareas en la cola
  redis: {
    // Establece la dirección IP del servidor Redis (en este caso, la dirección IP local)
    host: "127.0.0.1",
    // Establece el puerto en el que escucha el servidor Redis (el puerto predeterminado es 6379)
    port: 6379,
  },
});

Observa que hemos importado Bull con el alias Queue y hemos creado la cola pasándole dos argumentos: el nombre y un objeto con la configuración de Redis. Ahora que tenemos una cola, pasemos a los Jobs.

Jobs en BullJS

Con nuestra Queue lista, creemos nuestro primer Job. Para esto vamos a pasar un objeto con datos que contenga la dirección de correo electrónico a la que queremos enviar el email, además de algunas opciones. En este ejemplo queremos procesar el job 7 días después de haber sido creado, y si el job falla, se intentará ejecutar tres veces.

// Crea un objeto llamado "data" que contiene información para enviar un correo electrónico
const data = {
  // Establece la dirección de correo electrónico a la que se enviará el boletín informativo
  email: "foo@bar.com",
};

// Crea un objeto llamado "options" que contiene opciones de configuración para agregar una tarea a la cola
const options = {
  // Establece un retraso para la ejecución de la tarea, en este caso 7 días (86400000 milisegundos por día)
  delay: 86400000 * 7,
  // Establece el número máximo de intentos para procesar la tarea en caso de que falle
  attempts: 3,
};

// Agrega una nueva tarea a la cola "newsLetterMailQueue" con la información del correo y las opciones configuradas
newsLetterMailQueue.add(data, options);

Para añadir un job a una cola utilizamos la función add, que viene en el objeto de JavaScript que nos devuelve la creación de la cola. Esto hace que BullJS añada el job a la base de datos con las opciones que hemos especificado.

Procesando un Job

Para procesar un Job necesitamos especificar una función que pueda ser llamada por cada job en una cola, sin importar cuántos sean. Esta función se llama process y forma parte del objeto de la cola que hemos definido:

newsLetterMailQueue.process(async (job) => {
  await sendNewsLetterMailTo(job.data.email);
});

Hemos extraído la propiedad email del Job mediante job.data y luego llamamos a una función que se encarga de enviar el correo. Si esta función llega a fallar por algún error de JavaScript, BullJS controlará dicho error e intentará ejecutarlo de nuevo hasta un máximo de 3 veces, o las veces que hayamos especificado en las opciones del Job.

Completando un Job

Ahora imaginemos que la ejecución ha finalizado. ¿Cómo podemos saber esto? O mejor aún, ¿cómo sabemos si algo falló? Cada vez que finalice el proceso de un Job necesitamos resolver una promesa o ejecutar un callback. Veamos estas dos opciones:

newsLetterMailQueue.process(async (job, done) => {
  await sendNewsLetterMailTo(job.data.email);
  done(null, { message: "Email sent" });
});

En el ejemplo anterior, el callback done recibe dos parámetros: error y resultado. Como todo salió bien, hemos enviado el error en null y, en el resultado, un objeto con el mensaje de éxito.

newsLetterMailQueue.process(async (job) => {
  await sendNewsLetterMailTo(job.data.email);
  return Promise.resolve({ message: "Email sent" });
});

Ahora, utilizando promesas, tenemos la opción de retornar una promesa resuelta o fallida. En este caso, como queremos completar el job sin errores, retornamos el resultado dentro del resolve de nuestra promesa.

Además, es posible notificar sobre el progreso de un Job mediante job.progress, ya que si tenemos alguna otra entidad escuchando jobs en una cola, será una excelente señal de notificación entre ambos sistemas.

Por ejemplo, podríamos actualizar el progreso del Job de la siguiente manera:

newsLetterMailQueue.process(async (job) => {
  job.progress(50); // Actualiza el progreso al 50%
  await sendNewsLetterMailTo(job.data.email);
  job.progress(100); // Actualiza el progreso al 100%
  return Promise.resolve({ message: "Email sent" });
});

Esto nos permitirá mantener un seguimiento del progreso de cada Job y comunicarlo entre diferentes partes del sistema que estén interesadas en el estado de los Jobs.

Si usas BullMQ: El método para reportar progreso cambió de nombre. En lugar de job.progress(50) debes usar await job.updateProgress(50). El resto del concepto es idéntico.

Manejando errores en Jobs con BullJS

Bull es una biblioteca muy útil para manejar colas y trabajos en Node.js. En esta sección veremos cómo manejar errores, gestionar la concurrencia de trabajos y escuchar el estado de los trabajos utilizando BullJS.

Manejo de errores en un Job

Cuando trabajamos con BullJS es importante saber que los bloques try catch no funcionan dentro de la función .process(). Por lo tanto, debemos manejar los errores utilizando el objeto done o retornando una promesa rechazada. Aquí tienes un ejemplo:

newsLetterMailQueue.process(async (job, done) => {
  await sendNewsLetterMailTo(job.data.email);
  done(new Error("Algo salió muy mal"));
});

newsLetterMailQueue.process(async (job) => {
  await sendNewsLetterMailTo(job.data.email);
  return Promise.reject(new Error("Algo salió muy mal"));
});

Fíjate en que rechazamos con new Error(...) y no con un objeto plano. Bull y Node esperan instancias de Error para obtener stack traces correctos y un payload útil en el evento failed, así que mantenemos el mismo estilo que usamos con done(new Error(...)).

Concurrencia de Jobs

BullJS nos permite manejar la concurrencia de jobs aprovechando los procesadores de nuestro computador. Para ello colocamos la función process en un archivo independiente y luego le indicamos a la cola, mediante el primer argumento de process, cuántos jobs queremos ejecutar en paralelo. Primero definimos el procesador en su propio archivo:

// path/to/funcion/file.js
const processJob = async (job) => {
  // Do something
  await sendNewsLetterMailTo(job.data.email);
};

module.exports = processJob;

Y luego registramos ese archivo como procesador, indicando la concurrencia máxima como primer argumento (en este caso, 5 jobs en paralelo):

// Procesa hasta 5 jobs simultáneamente usando el archivo de procesador externo
newsLetterMailQueue.process(5, "/path/to/funcion/file.js");

Al pasar la ruta a un archivo, Bull ejecuta cada job en un procesador aislado (sandboxed), en su propio proceso de Node.js. De esta manera, BullJS puede ejecutar varios trabajos de forma simultánea sin que se bloqueen entre sí.

Si solo necesitas concurrencia dentro del mismo proceso, también puedes pasar el número como primer argumento a un procesador en línea: newsLetterMailQueue.process(5, async (job) => {... }).

Escuchando el estado de Jobs

Una de las características más interesantes de BullJS es que podemos escuchar el estado y el resultado de un trabajo en la misma aplicación o en una aplicación externa. Para hacerlo debemos crear una cola con el mismo nombre y escuchar un evento que nos indique cuándo se ha completado un trabajo.

import Queue from "bull";

const newsLetterMailQueue = new Queue("newsLetterMail", {
  redis: {
    host: "127.0.0.1",
    port: 6379,
  },
});

newsLetterMailQueue.on("global:completed", async (jobId, result) => {
  // En result obtenemos el resultado que se envía y, además, un identificador único del Job
  await sendSMS();
});

Hay dos cosas importantes que debemos notar aquí:

  1. El servidor que está escuchando los mensajes debe tener BullJS instalado.
  2. Debe apuntar exactamente al mismo Redis que está usando el otro servidor.

El evento global:completed es el que se usa entre servidores, es decir, para servidores externos al que está procesando el job. Pero si lo que quieres es hacerlo todo junto en el mismo servidor o proyecto, simplemente debes escuchar el evento completed.

Además del evento completed hay una lista enorme de eventos por los que se puede escuchar. Aquí los vemos:

queue
  .on("error", function (error) {
    // An error occured.
  })
  .on("waiting", function (jobId) {
    // A Job is waiting to be processed as soon as a worker is idling.
  })
  .on("active", function (job, jobPromise) {
    // A job has started. You can use `jobPromise.cancel()` to abort it.
  })
  .on("stalled", function (job) {
    // A job has been marked as stalled. This is useful for debugging job
    // workers that crash or pause the event loop.
  })
  .on("progress", function (job, progress) {
    // A job's progress was updated!
  })
  .on("completed", function (job, result) {
    // A job successfully completed with a `result`.
  })
  .on("failed", function (job, err) {
    // A job failed with reason `err`!
  })
  .on("paused", function () {
    // The queue has been paused.
  })
  .on("resumed", function (job) {
    // The queue has been resumed.
  })
  .on("cleaned", function (jobs, type) {
    // Old jobs have been cleaned from the queue. `jobs` is an array of cleaned
    // jobs, and `type` is the type of jobs cleaned.
  })
  .on("drained", function () {
    // Emitted every time the queue has processed all the waiting jobs (even if
    // there can be some delayed jobs not yet processed).
  })
  .on("removed", function (job) {
    // A job successfully removed.
  });

Puedes agregar el prefijo global: a cualquiera de estos eventos para usarlos entre proyectos.

Conclusiones

BullJS es una librería potente, optimizada y fácil de usar que te permite controlar tus tareas, la concurrencia y las notificaciones entre proyectos. Te animamos a probar BullJS y a explorar todos sus ejemplos. Y recuerda: para proyectos nuevos en 2026, mira primero BullMQ, que es el sucesor recomendado.

Ejercicios propuestos

  1. Implementa un sistema de cola para procesar imágenes usando BullJS.
  2. Añade manejo de errores y concurrencia a tu implementación del sistema de cola.
  3. Crea una aplicación separada que escuche y responda a eventos de trabajos completados, mostrando notificaciones en tiempo real a los usuarios.

Resumen en 3 puntos

  1. BullJS nos permite manejar errores en trabajos mediante el objeto done o retornando una promesa rechazada con new Error(...).
  2. Podemos gestionar la concurrencia de trabajos colocando la función process en un archivo independiente y pasando la concurrencia máxima como primer argumento de process.
  3. BullJS nos permite escuchar y responder a eventos de trabajos en la misma aplicación o en aplicaciones externas, facilitando la comunicación entre servidores con el prefijo global:.

Eso es todo, espero que este artículo te haya sido útil y que lo puedas aplicar a algún proyecto que tengas en mente. Si tienes alguna duda o comentario, no dudes en dejarlo en la sección de comentarios a continuación. Y si te gustó, comparte este artículo con tus amigos en las redes sociales usando los enlaces de aquí abajo. ¡Hasta la próxima!

Sebastian Gomez

Sebastian Gomez

Creador de contenido principalmente acerca de tecnología.

Leave a Reply

0 Comments

Advertisements

Related Posts

Categorias