Background Jobs with BullMQ

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.

4 min read Level 2/5 #express#jobs#bullmq
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-backed
  • pg-boss — Postgres-backed (no Redis!)
  • Cloud queues — SQS, Cloud Tasks, Inngest

For most stacks BullMQ + Redis wins on tooling, retry semantics, and ecosystem.

Webhooks →