Module P-12·23 min read

Why job queues exist, BullMQ with Redis, workers and concurrency, retries and exponential backoff, dead letter queues, cron jobs, and real-world use cases.

Module P-12 — Background Jobs and Task Queues with BullMQ

What this module covers: Some work is too slow, too unreliable, or too risky to do inside a request. Sending email, resizing images, calling third-party APIs, generating reports, sending push notifications — all of these should happen off the request path. This module covers why job queues exist, BullMQ's architecture with Redis, defining and dispatching jobs, writing workers with concurrency control, retries with exponential backoff, dead letter queues for failed jobs, repeatable cron jobs, and the patterns that make queues reliable in production.


Why Job Queues

Consider sending a transactional email when an order is placed. Three approaches:

typescript

A job queue gives you:

  • Decoupled execution — the response returns before the work is done
  • Automatic retries — if the email provider is down, the job retries with backoff
  • Visibility — you can see queued, active, completed, and failed jobs
  • Rate limiting — process at most N jobs per second regardless of how many are enqueued
  • Concurrency control — run M workers in parallel without overloading the system
  • Persistence — jobs survive process restarts (stored in Redis)

BullMQ Architecture

BullMQ uses Redis as its store. Three components:

text

The queue lives in Redis — producers and workers are just Node.js processes that connect to it. You can have multiple producers (different API instances) and multiple workers (separate scaling).

bash

Defining Queues and Adding Jobs

typescript

Adding jobs from your service:

typescript

Job options per-add

typescript

Writing Workers

A worker subscribes to a queue and processes jobs:

typescript

Running workers

Workers can run in the same process as your API (simple) or a separate process (recommended for production — independent scaling and crash isolation):

typescript
json
yaml

Retries and Exponential Backoff

When a job fails (throws an error), BullMQ retries it according to the attempts and backoff settings:

typescript

Override per-job for different retry strategies:

typescript

Dead letter queue pattern

After all retry attempts are exhausted, jobs land in BullMQ's failed set. For critical jobs, move them to a dedicated dead letter queue for manual review:

typescript

Repeatable Jobs (Cron)

BullMQ supports repeatable jobs using cron syntax:

typescript

The worker for scheduled jobs is identical to any other worker. BullMQ handles the timing.


Multiple Queues by Priority

Separate queues for different workload types:

typescript

BullMQ Dashboard

Inspect queues in a web UI with Bull Board:

bash
typescript

Visit /admin/queues to see job counts, retry failed jobs, clear queues, and inspect job data.


Real-World Patterns

Pattern 1: Always-on image processing

typescript

Pattern 2: Chaining jobs

typescript

Pattern 3: Bulk operations off the request path

typescript

Summary

  • Job queues decouple slow/unreliable work from the request path. The response returns in milliseconds; the worker does the heavy lifting asynchronously.
  • BullMQ uses Redis for persistence — jobs survive process restarts. Workers and producers are just Node.js processes connected to the same Redis instance.
  • Exponential backoff retries transient failures automatically. Failed jobs after all attempts go to the failed set — move them to a dead letter queue for critical workflows.
  • Concurrency and rate limiting prevent workers from overwhelming downstream services. Set concurrency per worker instance, limiter per queue.
  • Repeatable jobs replace setInterval for cron-style work — they persist across restarts and won't double-schedule.
  • Separate queues by workload type so a spike in image processing doesn't delay emails.
  • Bull Board provides a web UI for inspecting, retrying, and clearing jobs without writing code.

Next: JSON internals — what JSON.parse and JSON.stringify actually do, the edge cases that bite at scale, streaming JSON for large payloads, and when MessagePack is worth the complexity.


Knowledge Check

What is a key architectural benefit of using a job queue like BullMQ instead of using an inline await or fire-and-forget promise.catch() inside a request handler?


In BullMQ, what does setting attempts: 5 and a backoff: { type: 'exponential', delay: 2000 } achieve when a job fails?


Why is it generally recommended to run BullMQ workers in a separate process or container from the main Express API in a production environment?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.