·QueueHub Team·20 min read

BullMQ Troubleshooting Guide — Connection Drops, Stuck Jobs, and Common Fixes

BullMQ troubleshootingBullMQ stuck jobsBullMQ stalled jobsRedis connection dropsqueue monitoring

Your BullMQ workers were humming along fine — then jobs stopped processing. No errors. No alerts. Just silence.

The queue dashboard shows jobs piling up in "waiting" state. Workers appear to be connected, but nothing is moving. Sound familiar?

BullMQ troubleshooting is hard because Redis is the hidden middleman. Errors are often silent or cryptic, and the job lifecycle has many failure points. One misconfigured Redis eviction policy can silently delete your jobs. A missing error listener can let a worker fail without anyone noticing. A too-low command timeout can randomly crash workers even when Redis latency is under 5ms.

This guide covers every major failure mode — connection drops, stuck and stalled jobs, worker crashes, Redis memory issues, and cryptic BullMQ error codes — with real code fixes you can apply today.

💡 Troubleshoot visually with Queue Hub — see worker health, job details, and Redis memory at a glance instead of digging through Redis keys.


1. Common BullMQ Connection Issues

BullMQ depends entirely on its Redis connection. When that connection breaks, workers don't fail loudly — they go silent. Here's how to diagnose and fix every connection failure mode.

1.1 Redis Connection Drops (The "Silent Worker" Problem)

Symptom: Workers suddenly stop processing jobs. No error thrown. The queue dashboard shows jobs in "waiting" state forever.

Root cause: The Redis connection is lost; ioredis reconnects but the worker internal state doesn't recover properly.

Historical context: BullMQ v3.6.2 (February 2023) fixed a critical bug where exponential backoff retried within milliseconds, triggering a bug in ioredis that left workers connected but not processing jobs.

The old broken retry strategy:

const connection = new IORedis({
  retryStrategy: function (times: number) {
    return Math.min(Math.exp(times), 20000); // First retry: 1ms!
  },
});

The fix — enforce a minimum delay of 1 second:

const connection = new IORedis({
  retryStrategy: function (times: number) {
    return Math.max(Math.min(Math.exp(times), 20000), 1000); // Minimum 1s
  },
});

Production-ready connection setup:

import { Worker, Queue } from "bullmq";
import IORedis from "ioredis";

const connection = new IORedis({
  host: process.env.REDIS_HOST || "localhost",
  port: Number(process.env.REDIS_PORT) || 6379,
  password: process.env.REDIS_PASSWORD,
  maxRetriesPerRequest: null, // REQUIRED for workers
  retryStrategy: (times) =>
    Math.max(Math.min(Math.exp(times), 20000), 1000),
  enableOfflineQueue: true, // For workers; set false for Queue producers
  keepAlive: 30000,
});

const worker = new Worker(
  "my-queue",
  async (job) => {
    // process job
    return { processed: true };
  },
  { connection }
);

worker.on("error", (err) => {
  console.error("Worker connection error:", err);
});

Key takeaways:

  • Always attach error listeners: worker.on('error', err => logger.error(err))
  • Set enableOfflineQueue: false for producers (fail fast), true for workers (queue commands until reconnect)
  • maxRetriesPerRequest: null is required for workers — without it, workers fail immediately when commands time out

1.2 IORedis "Command Timed Out" Errors

Symptom: Random Error: Command timed out thrown in worker logs, even though Redis latency is under 5ms.

Root cause: ioredis has a default commandTimeout: 5000. BullMQ's internal loops (bzpopmin, waitForJob) occasionally exceed this due to Node.js event-loop micro-pauses (garbage collection, async congestion).

This is NOT a Redis problem — it's a client-side timeout. Increasing the Redis server timeout won't help.

Fix — increase or remove commandTimeout:

const connection = new IORedis({
  host: process.env.REDIS_HOST,
  port: Number(process.env.REDIS_PORT) || 6379,
  maxRetriesPerRequest: null,
  commandTimeout: 30000, // 30s instead of default 5s
  // Or simply omit commandTimeout entirely
});

Best practice: Set commandTimeout to at least 30s for workers. Monitor event-loop lag to catch issues early:

setInterval(() => {
  const start = process.hrtime.bigint();
  setImmediate(() => {
    const lag = Number(process.hrtime.bigint() - start) / 1e6;
    if (lag > 200) {
      console.warn(`Event loop lag: ${lag.toFixed(0)}ms`);
    }
  });
}, 1000);

1.3 TLS/SSL Connection Issues

Symptom: Error: connection refused or ioredis: connection error when connecting to cloud Redis providers.

Common fixes by provider:

Provider Issue Fix
Upstash Redis Requires TLS, IPv6 default Set family: 4, tls: {}
AWS ElastiCache Encryption in-transit Enable TLS, configure security group
Redis Labs TLS with client certs Provide ca, cert, key files
Azure Cache for Redis Non-standard port Use port 6380, tls: {}

Code example — TLS connection for Upstash / Azure:

const connection = new IORedis({
  host: process.env.REDIS_HOST,
  port: Number(process.env.REDIS_PORT) || 6380,
  password: process.env.REDIS_PASSWORD,
  tls: {}, // empty object enables TLS
  maxRetriesPerRequest: null,
  enableReadyCheck: false,
  family: 4, // force IPv4 (required for Upstash)
  retryStrategy: (times) =>
    Math.max(Math.min(Math.exp(times), 20000), 1000),
});

For AWS ElastiCache specifically — TLS is usually encryption-in-transit only, no client certs needed:

const connection = new IORedis({
  host: "my-cluster.xxxxx.use1.cache.amazonaws.com",
  port: 6379,
  tls: {},
  maxRetriesPerRequest: null,
});

1.4 Redis Sentinel & Cluster

Sentinel connection pattern:

const connection = new IORedis({
  sentinels: [
    { host: "sentinel-1.example.com", port: 26379 },
    { host: "sentinel-2.example.com", port: 26379 },
  ],
  name: "mymaster",
  maxRetriesPerRequest: null,
});

Cluster gotchas: BullMQ does not natively support Redis Cluster with sharding — each queue must live on a single node. Workers calling connection.duplicate() on a Cluster instance creates a new Cluster connection each time, causing connection explosion. Use standalone Redis mode instead.

Valkey compatibility: BullMQ works with Valkey (the Redis fork) — same protocol, same configuration.

1.5 Production Connection Checklist

  • maxRetriesPerRequest: null on Worker connections
  • Error listeners attached: worker.on('error', handler) and queue.on('error', handler)
  • retryStrategy minimum 1s delay (or use current BullMQ default)
  • enableOfflineQueue: true for Workers, false for Queue producers
  • TLS: empty tls: {} or proper certs depending on provider
  • IPv4 forced (family: 4) when connecting to cloud Redis
  • commandTimeout ≥ 30s (or omitted) on Worker connections
  • KeepAlive enabled (keepAlive: 30000)

2. Stuck Jobs — The Active State Hell

Jobs stuck in "waiting" or "active" state are the most common BullMQ troubleshooting scenario. Here's how to diagnose and fix each variation.

2.1 Jobs Stuck in "Waiting" State

Symptom: Jobs added to the queue but never picked up. Dashboard shows "waiting" count growing.

Common causes:

  1. No workers running — the queue has producers but no consumers
  2. Worker concurrency maxed out — all worker slots are busy
  3. Worker disconnected silently (see Section 1.1)
  4. Queue name mismatch — worker listens on a different name than the producer

Diagnosis:

// Check if any workers are active on the queue
const workers = await queue.getWorkers();
console.log(`Active workers: ${workers.length}`);

// Check active job count vs concurrency
const activeCount = await queue.getActiveCount();
console.log(`Active jobs: ${activeCount}`);

// Compare with expected concurrency
// If activeCount === max concurrency, all slots are busy

2.2 Jobs Stuck in "Active" State (The "Ghost Job")

Symptom: Jobs sit in "active" state for hours. No progress. The rest of the queue is blocked.

Root cause: A worker processed the job but died before moving it to completed or failed. Redis still holds the lock.

Stalled job detection is the safety net:

  • BullMQ (v2.0+) has built-in stall detection — no separate QueueScheduler needed
  • Default stalledInterval: 30s check
  • Default maxStalledCount: 1 (moves to failed set after one stall)

Configure stall detection:

const worker = new Worker(
  "orders",
  async (job) => {
    return processOrder(job.data);
  },
  {
    connection,
    lockDuration: 30000, // Lock expires after 30s
    stalledInterval: 15000, // Check every 15s
    maxStalledCount: 2, // Allow 2 stalls before failing
  }
);

worker.on("stalled", (jobId) => {
  console.warn(`Job ${jobId} stalled!`);
  // Send alert to monitoring
});

worker.on("failed", (job, err) => {
  if (err && err.message && err.message.includes("stalled")) {
    console.error(`Job ${job.id} failed due to repeated stalls`);
  }
});

Manual cleanup for stuck active jobs:

// Force-move a stuck active job back to waiting
const job = await queue.getJob(jobId);
if (job && (await job.isActive())) {
  await job.retry(); // Moves active -> waiting
}

2.3 Why Jobs Stall (Deep Dive)

Jobs stall when the worker can't renew its lock before expiry. Common causes:

  1. CPU-bound operations blocking the event loop — the worker can't renew the lock because the event loop never returns
  2. Infinite loopswhile(true) or never-resolving promises
  3. Unhandled rejections that swallow errors — the job never calls moveToCompleted or moveToFailed
  4. Worker crash without graceful shutdownprocess.exit() or SIGKILL
  5. Overloaded Redis — too many keys expiring simultaneously causes latency spikes

2.4 Preventing Stalls for Long-Running Jobs

Use skipLockRenewal: false (default) — BullMQ auto-renews locks while the processor runs.

Manually extend locks for very long operations:

const lockExtender = setInterval(async () => {
  try {
    await job.extendLock(job.token!, 30000);
  } catch (err) {
    clearInterval(lockExtender);
  }
}, 25000); // Extend 5s before expiry

try {
  // Long processing...
} finally {
  clearInterval(lockExtender);
}

Use sandboxed processors for CPU-heavy work:

// Separate process — won't block the event loop
const worker = new Worker("paint", __dirname + "/painter.js", {
  connection,
  useWorkerThreads: true,
});

Chunk large jobs and update progress:

async function process(job: Job) {
  const items = job.data.items;
  for (let i = 0; i < items.length; i++) {
    await processItem(items[i]);
    await job.updateProgress(
      Math.round(((i + 1) / items.length) * 100)
    );
  }
}

3. Worker Crashes & Recovery Patterns

3.1 What Happens When a Worker Crashes?

  1. Worker process dies (SIGKILL, uncaught exception, OOM)
  2. Redis connection from that worker closes
  3. Active job locks expire (default: 30s since last renewal)
  4. Another worker (or the stalled checker) picks up the job from waiting
  5. If maxStalledCount is exceeded, the job moves to failed

3.2 Graceful Shutdown — The Most Important Pattern

Without graceful shutdown, deployed workers drop active jobs on deploy or restart.

let worker: Worker;
let queue: Queue;
let connection: IORedis;

async function gracefulShutdown(signal: string) {
  console.log(`Received ${signal}, shutting down gracefully...`);

  // Stop accepting new jobs, wait for active jobs to finish
  await worker.close();

  // Close the Queue instances too
  await queue.close();

  // Close Redis connections
  await connection.quit();

  process.exit(0);
}

process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));

Key detail: worker.close() waits for ALL active processors to finish. If jobs take longer than 30s, consider also cancelling in-flight jobs.

3.3 Crash-Safe Job Processing with Checkpointing

For jobs that process many items, save progress so a restarted worker can resume:

class CheckpointProcessor {
  private redis: IORedis;

  constructor(redis: IORedis) {
    this.redis = redis;
  }

  async process(job: Job, items: unknown[]) {
    const checkpointKey = `checkpoint:${job.id}`;
    const raw = await this.redis.get(checkpointKey);
    const startIndex = raw
      ? JSON.parse(raw).processedCount
      : 0;

    for (let i = startIndex; i < items.length; i++) {
      await processItem(items[i]);

      // Save checkpoint every 10 items
      if ((i + 1) % 10 === 0) {
        await this.redis.setex(
          checkpointKey,
          86400,
          JSON.stringify({ processedCount: i + 1 })
        );
      }
    }

    await this.redis.del(checkpointKey); // Clear checkpoint on success
  }
}

3.4 Worker Crash Monitoring & Auto-Restart

Use process supervisors (PM2, systemd, Docker restart policies, Kubernetes). Track crash frequency and alert:

process.on("uncaughtException", (err) => {
  console.error("Uncaught exception:", err);
  // Log to monitoring before exiting
  logger.fatal({ err }, "Worker crashed");
  process.exit(1); // Let supervisor restart
});

process.on("unhandledRejection", (reason) => {
  console.error("Unhandled rejection:", reason);
  logger.fatal({ reason }, "Unhandled rejection");
  process.exit(1);
});

4. Queue Lifecycle Debugging

4.1 Jobs Stuck in "Delayed" State

Cause: The job has a delay option set, or is waiting for a parent job to complete.

Diagnosis:

const delayedJobs = await queue.getDelayed();
for (const job of delayedJobs) {
  console.log(`Job ${job.id} delayed until:`, job.delay);
}

4.2 Jobs Stuck in "Failed" (But Should Have Retried)

Causes:

  • removeOnFail configured (metadata removed before retry logic fires)
  • attempts set to 1 (default) or exhausted
  • Job threw UnrecoverableError — will NOT retry
  • backoff strategy not configured

Proper retry setup:

await queue.add(
  "email",
  { to: "user@example.com" },
  {
    attempts: 5,
    backoff: {
      type: "exponential",
      delay: 2000, // 2s, 4s, 8s, 16s, 32s
    },
  }
);

Using UnrecoverableError to skip retries for invalid data:

import { UnrecoverableError } from "bullmq";

async function processJob(job: Job) {
  if (!job.data.userId) {
    throw new UnrecoverableError("Invalid data — no retry");
  }
  // Process the job...
}

4.3 Jobs Stuck in "Paused" State

Cause: The queue was explicitly paused (via API or dashboard).

Fix:

await queue.resume(); // Or pause(): await queue.pause()

4.4 Missing Workers — No One Processing the Queue

Diagnosis: queue.getWorkers() returns an empty array.

Common causes:

  • Worker process not deployed (serverless platforms like Vercel, AWS Lambda don't support long-lived workers)
  • Worker crashed and auto-restart failed
  • Worker connecting to wrong Redis instance or queue name

Fix: Deploy workers as long-running services (ECS, K8s, Docker with restart: always). Monitor with Queue Hub live worker view.


5. Redis Memory & Performance Issues

5.1 The Eviction Policy Trap (#1 Cause of Mysterious Job Loss)

Symptom: Jobs disappear from the queue. "Missing lock" errors appear. Worker stops processing.

Root cause: Redis maxmemory-policy is set to volatile-lru, allkeys-lru, or another eviction policy. Redis evicts BullMQ internal keys (locks, job data) to free memory.

Fix — set to noeviction:

# redis.conf
maxmemory-policy noeviction
redis-cli CONFIG SET maxmemory-policy noeviction

Why noeviction is required: BullMQ relies on keys with TTL (locks, rate limiting counters). Eviction policies remove these, breaking queue operations. BullMQ emits a warning at startup: IMPORTANT! Eviction policy is <policy>. It should be "noeviction".

For AWS ElastiCache: Create a custom parameter group. AWS default groups use volatile-lru.

When Redis runs out of memory with noeviction: Writes fail with OOM command not allowed when used memory > 'maxmemory'. Monitor Redis memory and set alerts.

5.2 Job Data Overload

Symptom: Redis memory grows unbounded, causing latency and OOM errors.

Causes:

  • Default behavior keeps all completed and failed jobs forever
  • Large job payloads stored in Redis (e.g., Base64-encoded files)
  • No retention policy configured

Fix — auto-removal of jobs:

await queue.add("email", data, {
  removeOnComplete: {
    age: 24 * 3600, // Keep completed jobs for 24h
    count: 1000, // Keep max 1000 completed jobs
  },
  removeOnFail: {
    age: 7 * 24 * 3600, // Keep failed jobs for 7 days
    count: 5000,
  },
});

Better — set global defaults on the queue:

const queue = new Queue("email", {
  connection,
  defaultJobOptions: {
    removeOnComplete: { age: 86400, count: 100 },
    removeOnFail: { age: 604800, count: 1000 },
  },
});

5.3 Redis Persistence & Data Loss

Symptom: After Redis restart, all jobs are gone.

Root cause: Redis not configured for persistence (RDB snapshots or AOF).

Production recommendation: Enable AOF with appendfsync everysec. Note that AOF impacts write performance — benchmark before enabling. For cloud Redis, ensure persistence is enabled in your Redis plan.

5.4 Memory Monitoring

Queue Hub shows Redis memory usage and queue storage breakdown. For manual checks:

redis-cli INFO memory
redis-cli INFO stats | grep evicted_keys

6. Common BullMQ Error Codes & Fixes

6.1 "Missing lock for job {jobId}. moveToFinished"

Meaning: The worker lost the lock on the job it was processing and can't move it to completed or failed.

Common causes:

  • CPU-bound operation blocked the event loop for more than 30s
  • Redis connection dropped and the lock expired
  • Another worker or API call removed or moved the job
  • Redis eviction removed the lock key (see Section 5.1)

Fixes:

  1. Check for CPU-blocking operations in the processor
  2. Increase lockDuration for long-running jobs
  3. Add manual lock extension (see Section 2.4)
  4. Verify Redis maxmemory-policy: noeviction

Handle gracefully:

worker.on("error", (err) => {
  if (err.message.includes("Missing lock")) {
    logger.warn("Job lock lost — likely restarted or evicted");
  }
});

6.2 "job stalled more than allowable limit"

Meaning: The job has stalled and exceeded maxStalledCount. It was moved to the failed set.

Fix: Increase maxStalledCount to allow more retries, or fix the underlying cause (CPU blocking, event loop congestion, worker crashes).

const worker = new Worker("queue", processor, {
  maxStalledCount: 3, // Default is 1
});

6.3 "ERR Error running script ... Lua redis() command arguments must be strings or integers"

Meaning: BullMQ's internal Lua script received undefined, null, or a non-string parameter.

Root cause: An environment variable is undefined, empty string, or the wrong type was passed to a BullMQ method.

Fix — validate environment variables before use:

const queueName = process.env.QUEUE_NAME;
if (!queueName) {
  throw new Error("QUEUE_NAME is not defined or empty");
}
const queue = new Queue(queueName, { connection });

6.4 "Command timed out" (ioredis)

See Section 1.2 for full diagnosis. Increase commandTimeout to 30s or remove it entirely.

const connection = new IORedis({
  commandTimeout: 30000, // Increase from default 5000ms
});

6.5 "IMPORTANT! Eviction policy is volatile-lru. It should be noeviction"

Meaning: BullMQ detected at startup that Redis has the wrong eviction policy.

Fix:

redis-cli CONFIG SET maxmemory-policy noeviction

Make it permanent in redis.conf or update your cloud Redis parameter group.

6.6 "Possible EventEmitter memory leak detected"

Meaning: Multiple BullMQ instances are sharing the same Redis connection without proper listener management.

Fix: Avoid creating too many listeners on a shared connection, or increase the limit:

import IORedis from "ioredis";

const connection = new IORedis();
connection.setMaxListeners(50);

6.7 Error Code Reference Table

Error Pattern Likely Cause Severity Quick Fix
Missing lock for job Lock expired (CPU/network/eviction) High Increase lockDuration, check event loop
job stalled more than allowable limit Repeated stalls High Increase maxStalledCount, fix root cause
Command timed out ioredis timeout too low Medium Increase commandTimeout to 30s
Lua script error Bad env var passed to BullMQ High Validate all env vars before use
Eviction policy warning Redis eviction enabled Critical Set maxmemory-policy noeviction
connection refused Wrong host/port, TLS required High Check connection config, enable TLS
OOM command not allowed Redis memory full Critical Increase maxmemory, add job retention
uncaughtException Bug in processor code High Add try-catch, use process supervisor

7. How Queue Hub Helps Troubleshoot Visually

7.1 The Problem with Traditional Debugging

  • Grepping Redis keys: redis-cli KEYS bull:* — unscalable, blocks Redis
  • Writing ad-hoc scripts to requeue jobs
  • No visibility into worker health or connection state
  • Missed silent failures (worker stops, connection drops)

7.2 Live Overview Dashboard

See throughput charts (jobs processed per minute), processing time metrics (p50/p95/p99 latency), and active/failed/waiting job counts across all queues. A sudden drop in throughput combined with a rising waiting count signals a worker or connection issue.

7.3 Job Detail Inspector

Open any job to see:

  • data — what the job was supposed to process
  • result — what the processor returned
  • error — full stack trace of failure
  • logs — console output from the processor
  • progress — last reported progress percentage
  • timeline — when it was added, started processing, completed, or failed

7.4 Live Worker View Per Queue

See every worker instance connected to each queue, with their status and concurrency. If worker count drops from 5 to 0 but jobs are still being added, you have a deployment problem.

7.5 Multi-Backend Support

Queue Hub supports BullMQ AND BeeQueue (legacy Bull) in the same dashboard, with backends including local Redis, TLS, Valkey, AWS ElastiCache, Upstash, and Redis Labs. Agent tunneling enables private Redis access without exposing Redis to the internet.

7.6 Quick Comparison

Task Without Queue Hub With Queue Hub
Find stuck jobs redis-cli ZRANGE bull:queue:active 0 -1 Click "Active" filter in job table
Retry all failed jobs Script: ZRANGE... for each... Click "Retry All" button
See worker count Check deployment logs Live worker view per queue
Inspect job error Add logging, redeploy Open job detail, see error immediately
Monitor throughput Build Grafana dashboard Built-in throughput charts
Clean old jobs Script with removeOnComplete Bulk clean by age or count

8. Quick Reference: Debugging Checklist

When jobs stop processing, run through this checklist:

  1. Connection check: Are workers connected to Redis? Check worker.on('error') logs.
  2. Redis reachability: redis-cli PING from the worker host.
  3. Eviction policy: redis-cli CONFIG GET maxmemory-policy — must be noeviction.
  4. Memory usage: redis-cli INFO memory — is used_memory near maxmemory?
  5. Active workers: queue.getWorkers() — are any workers running?
  6. Working queue names: Do producer and consumer use the same queue name?
  7. Stalled jobs: Check stalled event logs — worker.on('stalled').
  8. Lua script errors: Check for undefined environment variables being passed to BullMQ.
  9. Graceful shutdown: Is worker.close() called on SIGTERM?
  10. BullMQ version: Are you on the latest version? (Especially > v3.6.2 for the connection fix.)

9. Production Configuration Template

import { Queue, Worker, QueueEvents } from "bullmq";
import IORedis from "ioredis";

// Shared connection config
const connection = new IORedis({
  host: process.env.REDIS_HOST || "localhost",
  port: Number(process.env.REDIS_PORT) || 6379,
  password: process.env.REDIS_PASSWORD,
  db: Number(process.env.REDIS_DB) || 0,
  maxRetriesPerRequest: null,
  enableOfflineQueue: true,
  keepAlive: 30000,
  commandTimeout: 30000,
  retryStrategy: (times: number) =>
    Math.max(Math.min(Math.exp(times), 20000), 1000),
  ...(process.env.REDIS_TLS === "true" && {
    tls: {} as Record<string, never>,
    family: 4,
  }),
});

// Queue with retention defaults
const queue = new Queue("production-queue", {
  connection,
  defaultJobOptions: {
    attempts: 5,
    backoff: { type: "exponential", delay: 2000 },
    removeOnComplete: { age: 86400, count: 1000 },
    removeOnFail: { age: 604800, count: 5000 },
  },
});

// Worker with stall protection
const worker = new Worker(
  "production-queue",
  async (job) => {
    // Your processing logic
    return await processJob(job);
  },
  {
    connection,
    concurrency: 10,
    lockDuration: 60000,
    stalledInterval: 30000,
    maxStalledCount: 2,
  }
);

// Error handling
worker.on("error", (err) =>
  console.error("Worker error:", err)
);
worker.on("stalled", (jobId) =>
  console.warn("Job stalled:", jobId)
);
worker.on("failed", (job, err) =>
  console.error("Job failed:", job?.id, err?.message)
);
queue.on("error", (err) =>
  console.error("Queue error:", err)
);

// Graceful shutdown
async function shutdown(signal: string) {
  console.log(`Shutting down on ${signal}...`);
  await worker.close();
  await queue.close();
  await connection.quit();
  process.exit(0);
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));

Conclusion

BullMQ is battle-tested at scale, but its failure modes are subtle and usually involve Redis misconfiguration or event-loop mismanagement. Most issues fall into three buckets: connection problems, stall/stuck job problems, and Redis configuration problems.

Here's your takeaway checklist:

  • Configure noeviction on Redis — this single setting prevents the most common category of mysteriously lost jobs
  • Implement graceful shutdownworker.close() on SIGTERM prevents dropped active jobs during deployments
  • Attach error listeners — silent failures are the hardest to debug
  • Validate environment variables — undefined queue names cause cryptic Lua script errors
  • Set job retention policies — keep Redis memory under control

Proactive monitoring catches these issues before they become outages. Queue Hub gives you a live dashboard to see worker health, inspect job details, and manage queues — all without writing a single Redis command.

Try Queue Hub at queuehub.tech — the visual way to troubleshoot, monitor, and manage your BullMQ queues in production.

Related Articles