BullMQ
BullMQ is the Redis-backed queue library most Node teams use for email, webhooks, media processing, and background sync.
Search across all documentation pages
BullMQ is the Redis-backed queue library most Node teams use for email, webhooks, media processing, and background sync.
Quick-reference recipe card - copy-paste ready.
import { Queue, Worker } from "bullmq";
import IORedis from "ioredis";
const connection = new IORedis(process.env.REDIS_URL!, { maxRetriesPerRequest: null });
export const reportQueue = new Queue("reports", { connection });
new Worker(
"reports",
async (job) => {
await buildReport(job.data.reportId);
},
{ connection: connection.duplicate(), concurrency: 3 }
);When to reach for this:
// src/queues/connection.ts
import IORedis from "ioredis";
export function createBullConnection() {
return new IORedis(process.env.REDIS_URL!, {
maxRetriesPerRequest: null,
enableReadyCheck: false,
});
}
// src/queues/reports.ts
import { Queue, Worker, QueueEvents } from "bullmq";
import { createBullConnection } from "./connection";
import { buildReport } from "../services/reports";
const connection = createBullConnection();
export const reportsQueue = new Queue("reports", { connection });
export function startReportWorker() {
const worker = new Worker(
"reports",
async (job) => {
await job.updateProgress(10);
const url = await buildReport(job.data.reportId);
await job.updateProgress(100);
return { url };
},
{
connection: createBullConnection(),
concurrency: 2,
lockDuration: 30_000,
}
);
worker.on("failed", (job, err) => {
console.error({ msg: "job_failed", jobId: job?.id, err: err.message });
});
worker.on("stalled", (jobId) => {
console.warn({ msg: "job_stalled", jobId });
});
return worker;
}
// src/api/reports-route.ts
import express from "express";
import { reportsQueue } from "../queues/reports";
const router = express.Router();
router.post("/", async (req, res) => {
const job = await reportsQueue.add(
"build",
{ reportId: req.body.reportId },
{ attempts: 3, backoff: { type: "exponential", delay: 1000 } }
);
res.status(202).json({ jobId: job.id });
});
export default router;What this demonstrates:
failed and stalled handlers for ops visibilitylockDuration expiresconst worker = startReportWorker();
process.on("SIGTERM", async () => {
await worker.close(); // waits for current job by default
process.exit(0);
});terminationGracePeriodSeconds > longest job durationawait reportsQueue.add(
"nightly",
{},
{ repeat: { pattern: "0 2 * * *" }, jobId: "nightly-report" }
);jobId prevents duplicate repeatable definitions on redeployimport { FlowProducer } from "bullmq";
const flow = new FlowProducer({ connection });
await flow.add({
name: "invoice-pdf",
queueName: "pdf",
data: { invoiceId },
children: [{ name: "fetch-line-items", queueName: "data", data: { invoiceId } }],
});@bull-board/express behind admin authmaxRetriesPerRequest default on worker - breaks BRPOP. Fix: null on Bull connections.stalled events.| Alternative | Use When | Don't Use When |
|---|---|---|
| AWS SQS | No Redis ops, AWS-native | Need complex job flows without extra orchestration |
| pg-boss | Postgres-only stack | Redis already central |
| Temporal | Long-running sagas, human tasks | Simple email queue |
| RabbitMQ | Enterprise AMQP existing | Team has no Erlang ops appetite |
BullMQ is maintained successor with better TypeScript and performance. New projects use BullMQ.
Split by domain and SLO: email, webhooks, media. Avoid one giant queue.
Start 2-5 I/O jobs. CPU-bound work: concurrency 1 per core after profiling.
Supported with prefix and hash tags per BullMQ docs. Test failover.
BullModule.registerQueue in API module; separate worker bootstrap imports processors.
Stable jobId on queue.add. Combine with idempotency keys in worker.
BullMQ rate limiter option per queue protects vendor quotas.
Use Redis Testcontainer; assert job completes and side effect occurred once.
OpenTelemetry bullmq instrumentation + queue depth metrics (waiting, active, delayed).
High priority floods low - use separate queues for tiered SLAs instead.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 16, 2026