BullMQ Stalled Jobs: What Causes Them and How to Fix Them
Stalled jobs are the most confusing state in BullMQ: a job that looks like it is still processing, but no worker is actually working on it. If you run BullMQ in production, understanding why jobs stall and how BullMQ detects them is the difference between silently double-processing work and catching a dying worker before it causes an incident.
What Is a Stalled Job in BullMQ?
A stalled job is a job that was being processed by a worker but whose processing lock expired before the job finished. BullMQ does not mark it as failed — at least, not right away. It gets flagged as stalled, which is why your "active" count can lie to you.
- Every active job in BullMQ is protected by a Redis-based lock owned by the worker that fetched it.
- If the lock expires while the job is still in progress, the job is considered stalled.
- Stalled jobs are moved back to the waiting list and picked up by another worker — so the same job can run more than once.
That last point matters more than any other: BullMQ's delivery model is at-least-once, and stalled jobs are the most common reason a job executes twice.
How BullMQ Detects Stalled Jobs
When a worker fetches a job, it acquires a lock on the job's Redis key with a TTL equal to lockDuration (30 seconds by default). While the worker is healthy, it renews that lock for as long as the job is being processed. BullMQ does this automatically — as long as the worker's event loop is responsive.
Every stalledInterval milliseconds (default 30 seconds), workers scan for active jobs whose locks have expired. Any job that matches is moved back to the waiting list so a healthy worker can retry it:
import { Worker } from "bullmq";
const worker = new Worker(
"emails",
async (job) => {
// your processing logic
},
{
connection: { host: "127.0.0.1", port: 6379 },
stalledInterval: 30000, // how often stalled jobs are checked
maxStalledCount: 2, // times a job may stall before it fails
lockDuration: 30000, // TTL of the processing lock
}
);
If the same job keeps getting stalled, it will not be retried forever. Once a job has been detected as stalled more than maxStalledCount times (default 1), BullMQ moves it to the failed set with a message like job stalled more than 1 times — that exact string is worth adding to your error alerts.
Common Causes of Stalled Jobs
In practice, stalled jobs almost always come from one of these:
- Workers dying mid-job. OOM kills,
SIGKILL, or a deployment that restarts containers without draining workers leave locks unreleased. - A blocked event loop. Synchronous CPU-heavy work or blocking I/O prevents the worker from renewing locks, so the lock expires while the job is "running".
- Jobs that outlive the lock. A job that legitimately takes longer than
lockDurationwith no progress updates and no lock extension will stall — see the fix below. - Redis connectivity problems. A network partition or a slow Redis can prevent lock renewal even when the worker is alive.
- Too few workers. When queues are deep and stalls exceed
maxStalledCountfast, recovery cannot keep up.
Note the pattern: stalled jobs are usually a symptom of infrastructure problems (deploys, memory, Redis), not a bug in BullMQ itself.
How to Fix and Prevent Stalled Jobs
There are six things worth doing, roughly in order of impact — and how you structure jobs in the first place (chunking, priorities, retries) is part of the same worker-design conversation as our guide to BullMQ job priorities:
- Make handlers idempotent. Because stalled jobs can run twice, every handler should tolerate being called again — check-before-insert, use idempotency keys, or rely on unique constraints.
- Keep the event loop free. Offload CPU-bound work to worker threads or a separate service; avoid synchronous filesystem and crypto calls in the hot path.
- Report progress on long jobs. Calling
updateProgress()regularly keeps the job's heartbeat visible:
const worker = new Worker("video-processing", async (job) => {
const frames = await loadFrames(job.data.videoId);
for (let i = 0; i < frames.length; i++) {
await processFrame(frames[i]);
await job.updateProgress((i + 1) / frames.length);
}
});
- Shut down gracefully. Handle
SIGTERMand callworker.close()so in-flight jobs are finished or released instead of abandoned. - Tune
lockDurationdeliberately. If you have jobs that legitimately run for minutes, raiselockDurationto match — but remember a longer lock means slower recovery when a worker actually dies. - Watch
maxStalledCountfailures. A job that failed with "stalled more than N times" is a worker that repeatedly died on the same input; treat it as a bug report.
How to Monitor Stalled Jobs
The right operational signal is not a single stalled job — it is stalled jobs appearing regularly, or in clusters. Because stalls correlate so strongly with deployments and memory pressure, you want visibility across both time and queues.
This is where a queue-aware dashboard earns its keep. A general Redis GUI shows you raw keys like bull:emails:stalled-check; a dashboard that speaks job semantics shows stalled counts next to active and failed, so a silent worker death becomes a visible anomaly. If you are evaluating options, our roundup of the best BullMQ dashboard alternatives covers what to look for, and comparing RedisInsight's raw-key view against a queue-aware dashboard shows exactly why stalled jobs are invisible in a general Redis GUI.
- Track
stalled > 0as an anomaly, not a routine event. - Correlate stall spikes with deploy timestamps — most stall spikes are deployment-related.
- Alert on the specific "job stalled more than N times" failures, not just the failed-job count.
Summary
Stalled jobs are BullMQ's safety net when workers die mid-job: the lock expires, the job goes back to the queue, and another worker picks it up. The cost is that work may run twice, so idempotent handlers are non-negotiable. Keep your event loop free, report progress on long jobs, shut down gracefully, and monitor stalled counts — that combination turns a confusing state into an early-warning signal.
Related Articles
Best BullMQ Dashboard Alternatives in 2026: A Comprehensive Comparison
Comparing every BullMQ UI option side by side: Bull Board, Arena, Taskforce, QueueHub, and raw redis-cli. Feature matrices, pricing, pros and cons, and recommendations for every team size.
BullMQ Job Priorities: When to Use priority() and Why It Matters
BullMQ's priority() option is one of the most underused features in the library. Learn how it works internally, when it actually helps, the head-of-line blocking trade-off, and how to observe priority behavior with a queue dashboard.
QueueHub vs Bull Board: Which BullMQ Dashboard Is Right for You?
Bull Board is the most popular open-source BullMQ UI, but is it the right choice for production? We compare Bull Board's self-hosted Express middleware against QueueHub's multi-backend SaaS dashboard across installation, auth, real-time updates, and more.