Skip to the solution
MediumSystem Design5 min read

What is a background job queue (BullMQ/Redis) and when do you need one?

1.1k views

The problem

Question presented to candidate: "A user signs up, and your API needs to send a welcome email through a third-party service that occasionally takes 3+ seconds to respond, or even times out. Should the signup request wait for that email to actually send before responding to the user?"

What a strong answer should cover:

  • No β€” this is close to the canonical use case for a background job queue: the signup request should respond as soon as the user is genuinely created, and the (slower, less time-critical, occasionally-failing) email send should happen asynchronously, decoupled from the request-response cycle the user is actually waiting on.
  • πŸ“Œ Verified, not assumed: a real in-memory job queue's enqueue() call returned in 3ms, while the job's actual handler kept running asynchronously afterward β€” the handler's first real attempt genuinely failed, was genuinely re-queued, and its second real attempt genuinely succeeded 79ms after the original enqueue call had already returned.
  • πŸ“Œ Interview term: BullMQ (a popular Node.js job-queue library) is built on Redis specifically because Redis provides the durability an in-memory queue (like the one demonstrated here, built to show the underlying mechanism clearly) cannot: a job survives the Node.js process itself restarting or crashing, since the job's state lives in Redis, not in that one process's memory β€” the same durability concern verified elsewhere in this bank for why an in-process cache is not automatically consistent or persistent across process restarts/instances.
  • A precise answer names the concrete triggers for reaching for a job queue over just doing the work inline: work that is slow relative to an acceptable response time (verified above β€” the prompt's email send), work with a real chance of transient failure that benefits from retry (verified above β€” the real fail-then-retry-then-succeed cycle), work that should be rate-limited or scheduled independently of request volume (batch reports, scheduled digests), or work that should survive the originating request's own process ending.
  • A precise answer also connects this to idempotency: because a real job queue's retry (verified above) means a handler can genuinely run more than once for logically the same job, the handler itself needs to be safe to run twice β€” exactly the mechanism verified with a real double-charge bug and its fix in the dedicated idempotency question.

Clarifying questions expected:

  • "How time-critical is the actual result of this specific piece of work to the user waiting on the request?" β€” the single question that decides whether it belongs inline or in a background job.
  • "Does this job need to survive the process restarting, or is best-effort, in-process-only handling acceptable for this specific use case?" β€” decides whether a durable (Redis-backed) queue is actually required versus a simpler in-process approach.

Code / implementation expected: Yes β€” the real, measured enqueue-returns-immediately-while-processing-continues-asynchronously behavior, including a genuine fail-then-retry-then-succeed cycle, is the concrete, convincing demonstration of exactly what a job queue buys over doing the work inline.

  • nodejs
  • queue
  • bullmq
  • redis
  • async

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

5 min read Β· 8 sections

Read the code

JavaScriptA real in-memory job queue: enqueue genuinely returns immediately while the handler retries and succeeds asynchronously
class JobQueue {
  constructor() { this.jobs = []; this.processing = false; }
  add(job) {
    this.jobs.push({ ...job, attempts: 0 });
    console.log(`[producer] enqueued job=${job.id} at t=${Date.now() - t0}ms, queue length now ${this.jobs.length}`);
    if (!this.processing) this._process();
  }
  async _process() {
    this.processing = true;
    while (this.jobs.length) {
      const job = this.jobs.shift();
      job.attempts++;
      try {
        await job.handler();
        console.log(`[worker] job=${job.id} SUCCEEDED on attempt ${job.attempts} at t=${Date.now() - t0}ms`);
      } catch (err) {
        console.log(`[worker] job=${job.id} FAILED attempt ${job.attempts} (${err.message})`);
        if (job.attempts < 3) { console.log(`[worker] job=${job.id} re-queued for retry ${job.attempts + 1}/3`); this.jobs.push(job); }
        else console.log(`[worker] job=${job.id} exhausted retries, moved to dead-letter`);
      }
    }
    this.processing = false;
  }
}

const t0 = Date.now();
const queue = new JobQueue();
let sendAttempts = 0;

console.log(`[producer] calling queue.add() for job=email-1 at t=${Date.now() - t0}ms`);
queue.add({
  id: "email-1",
  handler: async () => {
    sendAttempts++;
    await new Promise(r => setTimeout(r, 30));
    if (sendAttempts < 2) throw new Error("SMTP timeout");
    return "sent";
  },
});
console.log(`[producer] queue.add() returned immediately at t=${Date.now() - t0}ms β€” did NOT wait for the email to actually send`);

// [producer] calling queue.add() for job=email-1 at t=0ms
// [producer] enqueued job=email-1 at t=3ms, queue length now 1
// [producer] queue.add() returned immediately at t=3ms β€” did NOT wait for the email to actually send
// [worker] job=email-1 FAILED attempt 1 (SMTP timeout)
// [worker] job=email-1 re-queued for retry 2/3
// [worker] job=email-1 SUCCEEDED on attempt 2 at t=79ms

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 51 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextMedium

What is the 12-factor app methodology as applied to Node.js services?

Continue