Job Queues with BullMQ (Redis)

Lab
Easy
Medium
Hard

Retries with exponential backoffMedium

A flaky job fails twice, waits 2 s then 4 s, then succeeds.

What the producer adds

Workers

Concurrency (number of workers)
A worker crashes…
1 / 90 done · 0 failed
Producer
The app calls queue.add() for each task. BullMQ stores every job in Redis (a hash per job plus entries in lists/sorted sets for its state), so jobs survive restarts. 2 workers are ready.

Job states in a BullMQ queue

state lives in Redis, workers are stateless
Workers
Worker 1idle
Worker 2idle
Waiting
ready for a worker
0
Delayed
scheduled / backoff
0
Active
locked by a worker
0
Completed
finished OK
0
Failed
out of attempts
0
what BullMQ keeps in Redis right now
bull:emails:wait (LIST)0 items
bull:emails:delayed (ZSET)0 items
bull:emails:active (LIST)0 items
bull:emails:completed (ZSET)0 items
bull:emails:failed (ZSET)0 items
✓ All 4 jobs completed in 12s

Average wait before a worker picked a job up: 0.7 s. Try fewer workers, a rate limit or a poison job to see what changes.

Learn the theory

Job Queues with BullMQ (Redis): how it works

4 min read · Updated

Some work shouldn't happen inside a web request: sending email, resizing images, calling slow APIs, generating reports. A job queue lets the request hand the work off and return immediately while workers process it in the background. BullMQ is the popular Node.js queue built on Redis: jobs and their states live in Redis data structures, workers are stateless processes that take jobs, and the library handles retries, delays, priorities, rate limits and crashed workers for you.

In short

  • A job moves through waiting → active → completed/failed, with delayed for scheduled jobs and retry backoff.
  • All state lives in Redis, so jobs survive deploys and crashes; workers can be scaled horizontally.
  • Retries with exponential backoff handle transient errors; a failed set holds jobs that ran out of attempts.
  • If a worker dies mid-job, its lock expires and the job is detected as stalled and re-queued — so handlers must be idempotent.
  • Use concurrency, priority and rate limiters to control throughput and protect downstream services.

Why queues?

  • Fast responses: accept the request, enqueue, reply in milliseconds.
  • Reliability: work survives restarts and is retried automatically.
  • Smoothing bursts: a spike becomes a queue depth instead of an outage.
  • Scale: add workers without changing the producers.
import { Queue, Worker } from 'bullmq';

const emails = new Queue('emails', { connection });
await emails.add('welcome', { userId: 42 }, { attempts: 4, backoff: { type: 'exponential', delay: 2000 } });

new Worker('emails', async (job) => sendEmail(job.data), { connection, concurrency: 5 });

The job lifecycle

StateMeaningRedis structure (BullMQ)
waitingReady to be picked up (ordered by priority, then FIFO).bull:<queue>:wait (list) / prioritized (zset)
delayedWaiting for a time: an explicit delay or a retry backoff.bull:<queue>:delayed (sorted set by due time)
activeLocked by a worker that is running it.bull:<queue>:active (list) + a lock key with a TTL
completedFinished successfully (kept for a while for inspection).bull:<queue>:completed (sorted set)
failedOut of attempts (kept with the error and stack trace).bull:<queue>:failed (sorted set)

Each job's data and options are stored in a Redis hash (bull:<queue>:<id>). Workers use atomic Lua scripts to move jobs between states, which is why two workers never process the same job.

Retries, backoff and poison jobs

When a job throws, BullMQ increments its attempt counter. If attempts remain it moves the job to delayed for the backoff period, then back to waiting.

  • Fixed backoff: the same delay every time.
  • Exponential backoff: delay × 2^(attempt-1), e.g. 2 s, 4 s, 8 s — gives a struggling dependency time to recover and avoids hammering it.
  • Poison jobs can never succeed (bad payload, bug). After the last attempt they land in the failed set. Alert on that set and don't retry forever.

Crashed workers and stalled jobs

While a worker runs a job it keeps renewing a lock in Redis. If the process crashes or hangs, the lock stops being renewed and expires. A stalled-job checker notices an active job with no lock and moves it back to waiting so another worker can take it.

The consequence: a job can run more than once (the first attempt may have done part of the work). Make job handlers idempotent — the same advice as for webhooks.

Concurrency, priority and rate limiting

  • Concurrency: how many jobs one worker runs at once (I/O-bound work benefits from higher numbers). Add more worker processes to scale further.
  • Priority: lower number = served first, so a password-reset email overtakes a newsletter backlog.
  • Rate limiter: cap jobs started per time window to respect third-party API limits.
  • Delayed jobs: delay schedules work for later, e.g. “remind the user in 3 days”.
  • Repeatable jobs: cron-like schedules stored in Redis, shared by all workers.

Running queues in production

  • Use a Redis with persistence (AOF) and maxmemory-policy noeviction for queue data — evicting job keys loses work.
  • Monitor queue depth, job age, failure rate and worker count (Bull Board or Taskforce UI).
  • Set sensible job retention (removeOnComplete, removeOnFail) to bound memory.
  • Handle graceful shutdown: let the worker finish active jobs before exiting.
  • Keep payloads small — store IDs, not whole objects.

Frequently asked questions

What is BullMQ?
A Node.js library for building job queues on top of Redis. It stores jobs and their states in Redis and provides workers, retries, delays, priorities, rate limiting and repeatable jobs.
What are the job states in BullMQ?
Waiting, delayed, active, completed and failed (plus waiting-children and prioritized for advanced flows). Jobs move between them via atomic Redis scripts.
How do retries and backoff work in BullMQ?
Set attempts and a backoff option on the job. When it fails and attempts remain, it goes to the delayed set for the backoff time (fixed or exponential), then back to waiting.
What is a stalled job?
A job that is marked active but whose worker stopped renewing its lock, usually because the process crashed or the event loop was blocked. BullMQ re-queues it, so it may run more than once.
How do I scale BullMQ workers?
Increase the concurrency option for I/O-bound work and run more worker processes or containers. They all pull from the same Redis queue without coordinating.
Should I use BullMQ or RabbitMQ?
BullMQ is the simple choice if you already use Redis and Node.js and want job semantics (retries, delays, priorities). RabbitMQ is a general-purpose broker with rich routing and works across languages; it suits complex messaging topologies.

Keep exploring

More interactive labs

All labs

Redis Commands & Data Structures

Strings, lists, hashes, sets and sorted sets, one command at a time.

Redis15 min · Beginner

Webhooks: Delivery, Retries & Signatures

Why you get duplicate deliveries, lost events and forged requests — and how to stop them.

Queues & Jobs12 min · Intermediate

JS Event Loop Visualizer

See the call stack, microtasks and timers run, frame by frame.

JavaScript10 min · Intermediate

CORS & Preflight Simulator

Edit the request and the server headers, then see exactly why the browser blocks it.

Networking12 min · Intermediate

HTTP Caching Explained

Cache-Control, ETag and 304s, one request at a time.

Networking12 min · Intermediate

What Happens When You Type a URL

DNS, TCP, TLS and HTTP — with a stopwatch.

Networking10 min · Beginner

How Database Indexes Work (B-Tree)

See a B-tree lookup beat a full table scan, and why some queries ignore your index.

Databases12 min · Intermediate

What Happens When 100 Reads Hit the Database

Connection pools, CPU workers, caches and replicas, as 100 requests arrive at once.

Databases12 min · Intermediate

SQL Isolation Levels Lab

Run two transactions side by side and watch dirty reads, phantoms and write skew appear.

Databases15 min · Advanced

Sharding & Replication

Split data across servers, copy it for reads, and see what each choice costs.

Databases15 min · Advanced

Redis Caching: Cache-Aside, TTL & Eviction

Hit or miss? Watch LRU, LFU, TTLs and a cache stampede decide your database load.

Redis12 min · Intermediate

Cron Jobs: Schedules, Overlaps & Missed Runs

Write a cron expression and watch what really happens when jobs overlap or the scheduler is down.

Queues & Jobs10 min · Beginner

RabbitMQ: Exchanges, Queues, Acks & Dead Letters

Direct, topic and fanout routing, competing consumers, prefetch, redelivery and dead-letter queues, drawn.

Queues & Jobs14 min · Intermediate

Git Commands, HEAD & Refs (Visual)

Every core git command drawn with arrows: three areas, commits, branches, HEAD and remotes.

Git15 min · Beginner

Git Merge vs Rebase

Watch the commit graph change under merge, rebase and squash.

Git10 min · Beginner