Defer Slow Work — Keep Requests Snappy
Background Jobs with BullMQ
Push slow work (emails, image processing, webhooks) to a queue. BullMQ + Redis is the standard.
What you'll learn
- Enqueue from an Express handler
- Process in a separate worker
- Configure retries and scheduling
If a request triggers slow work (sending email, resizing an image, calling a flaky API) — push it to a queue. Your handler stays fast; a separate worker does the work.
Install
npm install bullmq Producer — in Express
// src/jobs/email.js
import { Queue } from "bullmq";
export const emailQueue = new Queue("email", {
connection: { url: process.env.REDIS_URL },
}); // src/controllers/auth.js
import { emailQueue } from "../jobs/email.js";
export async function signup(req, res) {
const user = await db.users.create(req.validBody);
// Enqueue the email — fast
await emailQueue.add("welcome", { userId: user.id });
res.status(201).json({ data: user });
} The HTTP request returns instantly. The actual email gets sent later.
Worker — Separate Process
// src/jobs/worker.js — run with: node src/jobs/worker.js
import { Worker } from "bullmq";
import { sendWelcomeEmail } from "../lib/email.js";
new Worker("email", async (job) => {
if (job.name === "welcome") {
await sendWelcomeEmail(job.data.userId);
}
}, {
connection: { url: process.env.REDIS_URL },
concurrency: 5,
});
console.log("worker up"); Run this as a separate node process — separate container in
production. Workers can scale independently of your web servers.
Retries
await emailQueue.add("welcome", { userId }, {
attempts: 5,
backoff: { type: "exponential", delay: 1000 }, // 1s, 2s, 4s, ...
}); 5 attempts with exponential backoff. Flaky upstream APIs don’t break your job.
Scheduling
// run in 1 hour
await emailQueue.add("reminder", { ... }, { delay: 60 * 60 * 1000 });
// recurring
await emailQueue.add("digest", {}, {
repeat: { pattern: "0 9 * * *" }, // 9am daily (cron)
}); A Dashboard
npm install @bull-board/express import { createBullBoard } from "@bull-board/api";
import { BullMQAdapter } from "@bull-board/api/bullMQAdapter.js";
import { ExpressAdapter } from "@bull-board/express";
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath("/admin/queues");
createBullBoard({
queues: [new BullMQAdapter(emailQueue)],
serverAdapter,
});
app.use("/admin/queues", requireAdmin, serverAdapter.getRouter()); A built-in UI showing pending / failed / completed jobs. Great for debugging.
When To Reach For It
- The work takes longer than a few hundred ms
- The work isn’t strictly required for the response
- The work might fail and you want retries
- You need scheduling
Don’t reach for queues for everything — sub-100ms work is fine inline. The overhead of enqueuing then processing exceeds the savings.
Alternatives
agenda— Mongo-backedpg-boss— Postgres-backed (no Redis!)- Cloud queues — SQS, Cloud Tasks, Inngest
For most stacks BullMQ + Redis wins on tooling, retry semantics, and ecosystem.
Webhooks →