Defer Slow Work — Keep Requests Fast
Job Queues
Move slow tasks (emails, image processing, webhook fan-out) to a background worker. BullMQ is the standard.
What you'll learn
- Recognize the queue pattern
- Use BullMQ with Redis
- Handle retries and failures
Some work is too slow for a request: sending email, resizing images, calling a flaky third-party API. Push it onto a queue — a separate process picks it up.
The Pattern
HTTP request → enqueue job → reply 202 Accepted
↓
background worker → do the slow thing
BullMQ — The Standard
Uses Redis under the hood. Battle-tested.
npm install bullmq Producer (Web Server)
import { Queue } from "bullmq";
const emailQueue = new Queue("email", {
connection: { host: "localhost", port: 6379 },
});
app.post("/signup", async (req, res) => {
// 1. Save user — fast
const user = await db.users.create(req.body);
// 2. Enqueue the email — also fast
await emailQueue.add("welcome", { userId: user.id });
res.status(201).json(user);
}); Worker (Separate Process)
// worker.mjs — run with: node worker.mjs
import { Worker } from "bullmq";
new Worker("email", async (job) => {
if (job.name === "welcome") {
await sendWelcomeEmail(job.data.userId);
}
}, { connection: { host: "localhost", port: 6379 } }); Workers can run on a different machine. They poll Redis for jobs and execute them.
Retries
await emailQueue.add("welcome", { userId: 42 }, {
attempts: 5,
backoff: { type: "exponential", delay: 1000 }, // 1s, 2s, 4s, 8s, 16s
}); Flaky external APIs? Set retries. BullMQ handles exponential backoff for you.
Scheduling
// run in 1 hour
await emailQueue.add("reminder", { userId: 42 }, { delay: 60 * 60 * 1000 });
// repeating
await emailQueue.add("daily-digest", {}, {
repeat: { pattern: "0 9 * * *" }, // 9am daily (cron syntax)
}); Why You Want a Queue
- Decoupling — slow consumers don’t slow down the request
- Reliability — failed jobs retry; if a worker crashes, the job goes back in the queue
- Throttling — limit concurrency so you don’t hammer a third party
- Scheduling — run at a specific time
Without Redis
For simple cases, a plain in-memory queue + setImmediate works. But you lose: persistence across crashes, scaling beyond one process, visibility. For anything user-facing: use BullMQ + Redis.
Pub/Sub →