BullMQ Troubleshooting Guide — Connection Drops, Stuck Jobs, and Common Fixes
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: falsefor producers (fail fast),truefor workers (queue commands until reconnect) maxRetriesPerRequest: nullis 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: nullon Worker connections - Error listeners attached:
worker.on('error', handler)andqueue.on('error', handler) -
retryStrategyminimum 1s delay (or use current BullMQ default) -
enableOfflineQueue: truefor Workers,falsefor 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:
- No workers running — the queue has producers but no consumers
- Worker concurrency maxed out — all worker slots are busy
- Worker disconnected silently (see Section 1.1)
- 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:
- CPU-bound operations blocking the event loop — the worker can't renew the lock because the event loop never returns
- Infinite loops —
while(true)or never-resolving promises - Unhandled rejections that swallow errors — the job never calls
moveToCompletedormoveToFailed - Worker crash without graceful shutdown —
process.exit()or SIGKILL - 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?
- Worker process dies (SIGKILL, uncaught exception, OOM)
- Redis connection from that worker closes
- Active job locks expire (default: 30s since last renewal)
- Another worker (or the stalled checker) picks up the job from waiting
- If
maxStalledCountis 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:
removeOnFailconfigured (metadata removed before retry logic fires)attemptsset to 1 (default) or exhausted- Job threw
UnrecoverableError— will NOT retry backoffstrategy 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:
- Check for CPU-blocking operations in the processor
- Increase
lockDurationfor long-running jobs - Add manual lock extension (see Section 2.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:
- Connection check: Are workers connected to Redis? Check
worker.on('error')logs. - Redis reachability:
redis-cli PINGfrom the worker host. - Eviction policy:
redis-cli CONFIG GET maxmemory-policy— must benoeviction. - Memory usage:
redis-cli INFO memory— isused_memorynearmaxmemory? - Active workers:
queue.getWorkers()— are any workers running? - Working queue names: Do producer and consumer use the same queue name?
- Stalled jobs: Check stalled event logs —
worker.on('stalled'). - Lua script errors: Check for undefined environment variables being passed to BullMQ.
- Graceful shutdown: Is
worker.close()called on SIGTERM? - 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
noevictionon Redis — this single setting prevents the most common category of mysteriously lost jobs - Implement graceful shutdown —
worker.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
QueueHub vs Arena: The BullMQ Arena Dashboard Compared
Arena (mixplex/arena) was the original BullMQ web UI — but years of minimal maintenance have left it behind. We compare Arena's self-hosted dashboard against QueueHub's hosted alternative across UI quality, metrics, job operations, and deployment.
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.
Queue Monitoring Best Practices: Alerting, Dashboards & Metrics for BullMQ
Learn how to monitor BullMQ and BeeQueue queues in production. A comprehensive guide covering key queue health metrics, dashboard design patterns, alerting strategies, and worker health checks.