Nota revisada y reescrita el 18 de octubre de 2026: la versión original tenía errores y datos sin fuente.
Si una petición HTTP tiene que generar un PDF, mandar tres emails y llamar a una API lenta, el usuario espera todo ese tiempo, y si algo falla a medias no hay forma limpia de reintentar. La solución clásica es una cola: la petición apunta el trabajo y responde enseguida, y un proceso aparte (el worker) lo ejecuta y lo reintenta si falla. En Node.js, BullMQ es la opción más usada, sobre Redis.
Productor y worker
// queue.js — se usa desde la API
import { Queue } from 'bullmq';
const connection = { host: process.env.REDIS_HOST, port: 6379 };
export const emails = new Queue('emails', { connection });
// En el handler HTTP:
await emails.add(
'welcome',
{ userId: user.id },
{
jobId: `welcome:${user.id}`, // idempotencia, ver abajo
attempts: 5,
backoff: { type: 'exponential', delay: 2000 }, // 2 s, 4 s, 8 s...
removeOnComplete: 1000, // conserva los 1.000 últimos
removeOnFail: 5000,
}
);
// worker.js — proceso aparte
import { Worker } from 'bullmq';
const worker = new Worker(
'emails',
async (job) => {
const user = await getUser(job.data.userId);
await sendWelcomeEmail(user); // si lanza un error, BullMQ reintenta
},
{
connection: { host: process.env.REDIS_HOST, port: 6379, maxRetriesPerRequest: null },
concurrency: 10,
}
);
worker.on('failed', (job, err) => console.error(`job ${job?.id} falló:`, err.message));
worker.on('error', (err) => console.error('worker error', err)); // problemas de conexión
Lo que exige la documentación para producción
maxmemory-policy noevictionen Redis. Según la guía de BullMQ, es la única configuración que garantiza que las colas funcionen bien. Con una política que expulsa claves (allkeys-lru, típica de Redis usado como caché), Redis puede borrar datos de los trabajos cuando se llena la memoria. Usa una instancia de Redis para las colas, separada de la de caché.maxRetriesPerRequest: nullen la conexión de los workers, para que un corte de Redis no rompa el worker con excepciones.- Limpiar los trabajos terminados. Por defecto, los completados y los fallidos se guardan para siempre.
removeOnCompleteyremoveOnFailevitan que Redis crezca sin límite. - Apagado limpio. Cierra los workers al recibir la señal de parada, para que terminen el trabajo en curso en lugar de dejarlo "atascado" (stalled):
async function shutdown() {
await worker.close();
process.exit(0);
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
Idempotencia: el trabajo puede ejecutarse más de una vez
Un worker puede morir a mitad de un trabajo (un despliegue, falta de memoria). BullMQ lo detecta como stalled y lo vuelve a ejecutar. Además, hay reintentos. Así que el procesador tiene que poder ejecutarse dos veces sin consecuencias:
jobIddeterminista al encolar. Si ya existe un trabajo con ese id,addno crea otro. Evita encolar dos veces el mismo email de bienvenida si la API recibe la petición duplicada.- Comprobar el efecto antes de hacerlo. "¿Ya se envió este email?" en tu base de datos, o una clave de idempotencia en la API externa. Stripe, por ejemplo, acepta la cabecera
Idempotency-Key.
Trabajos que dependen de otros
Para "genera las tres miniaturas y, cuando estén todas, publica el producto", BullMQ tiene flows: un trabajo padre que no se ejecuta hasta que terminan sus hijos.
import { FlowProducer } from 'bullmq';
const flow = new FlowProducer({ connection });
await flow.add({
name: 'publish-product',
queueName: 'products',
data: { productId },
children: ['small', 'medium', 'large'].map((size) => ({
name: 'thumbnail',
queueName: 'images',
data: { productId, size },
})),
});
Dentro del padre, await job.getChildrenValues() devuelve los resultados de los hijos.