<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harsh</title>
    <description>The latest articles on DEV Community by Harsh (@hashpod).</description>
    <link>https://dev.to/hashpod</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4015799%2Fe720ca41-c652-4332-b2ad-34d0452865df.png</url>
      <title>DEV Community: Harsh</title>
      <link>https://dev.to/hashpod</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hashpod"/>
    <language>en</language>
    <item>
      <title>Debugging Stalled Jobs in BullMQ: What Actually Causes Them and How to Catch Them Early published: false tags: node, redis, bullmq, backend</title>
      <dc:creator>Harsh</dc:creator>
      <pubDate>Wed, 08 Jul 2026 09:25:45 +0000</pubDate>
      <link>https://dev.to/hashpod/debugging-stalled-jobs-in-bullmq-what-actually-causes-them-and-how-to-catch-them-early-published-ein</link>
      <guid>https://dev.to/hashpod/debugging-stalled-jobs-in-bullmq-what-actually-causes-them-and-how-to-catch-them-early-published-ein</guid>
      <description>&lt;p&gt;If you've run BullMQ in production long enough, you've probably seen a job go "stalled" — and if you haven't, you will. Here's what actually causes it, why it's more dangerous than a simple failure, and how to catch it before it costs you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What "stalled" actually means&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A stalled job isn't the same as a failed job. A failed job threw an error and BullMQ knows about it. A stalled job is one BullMQ thinks is still being processed, but the worker that picked it up has gone silent — no heartbeat, no completion, nothing. From BullMQ's perspective, the job is in limbo.&lt;/p&gt;

&lt;p&gt;This usually happens because of:&lt;/p&gt;

&lt;p&gt;Worker crash mid-job — an unhandled exception, OOM kill, or container restart while a job is actively processing&lt;br&gt;
Event loop blocking — a long synchronous operation (heavy computation, a blocking file read, a bad regex) prevents the worker from sending its lock-renewal heartbeat in time&lt;br&gt;
Lock expiration under load — if your lockDuration is too short relative to how long jobs actually take, jobs can be marked stalled even when the worker is still healthily processing them&lt;br&gt;
Redis connectivity blips — a brief network partition between the worker and Redis can cause the lock renewal to fail even though the worker itself is fine&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why stalled jobs are more dangerous than failures&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A failed job is visible — it's in your failed queue, it has an error message, you can see it and retry it. A stalled job just... sits there, silently, until BullMQ's stalled-check interval notices it and either retries it or marks it failed, depending on your config. In the meantime, if that job was part of a time-sensitive flow (sending a confirmation email, processing a payment webhook, updating inventory), nothing tells you it's stuck. You find out when a customer complains, not when it happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to reduce stalled jobs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few concrete things that help:&lt;/p&gt;

&lt;p&gt;Tune lockDuration and stalledInterval to match your actual job runtime, not a guess. If your jobs typically take 30 seconds, a 30-second lock is too tight — give yourself real margin.&lt;br&gt;
Avoid blocking the event loop inside job processors. Move CPU-heavy work to worker threads or a separate process if you can. Node's single-threaded event loop means a bad synchronous chunk of code can silently prevent heartbeats from firing.&lt;br&gt;
Set maxStalledCount deliberately. The default retry behavior for stalled jobs can mask a recurring problem if you don't watch for jobs that stall repeatedly — that's usually a sign of a systemic issue (e.g. a specific job type that's too slow), not bad luck.&lt;br&gt;
Log job start and heartbeat events, not just completion and failure, so you have a timeline to look at after the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catching it before it becomes an incident&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The hardest part of stalled jobs isn't fixing them once you know — it's finding out in time. BullMQ emits events for this (stalled, failed, active, completed) via its QueueEvents class, but most teams either don't wire up alerting on these events at all, or only find out by checking the dashboard reactively.&lt;/p&gt;

&lt;p&gt;This is actually the problem we built Qcanary to solve — it's a small agent that listens to these BullMQ events locally in your own worker process and sends an alert (Slack, email, webhook) the moment something stalls or fails repeatedly, without needing access to your Redis instance directly. If you're currently finding out about stalled jobs from a customer instead of an alert, that gap is worth closing one way or another, whether that's this or building your own listener on QueueEvents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stalled jobs are a symptom, not a root cause — they almost always point to either a lock/timing mismatch or blocking code in your processor. Fix the underlying cause where you can, but also make sure you find out when it happens instead of waiting for it to surface downstream. A silent stall in a critical path is far more expensive than a loud failure.&lt;/p&gt;

&lt;p&gt;What's the worst stalled-job incident you've dealt with? Curious to hear how other teams have caught (or missed) these in production.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>debugging</category>
      <category>monitoring</category>
      <category>node</category>
    </item>
    <item>
      <title>A practical guide to monitoring BullMQ queues with an agent-based approach that keeps Redis credentials inside your infrastructure.</title>
      <dc:creator>Harsh</dc:creator>
      <pubDate>Sun, 05 Jul 2026 16:09:58 +0000</pubDate>
      <link>https://dev.to/hashpod/a-practical-guide-to-monitoring-bullmq-queues-with-an-agent-based-approach-that-keeps-redis-40de</link>
      <guid>https://dev.to/hashpod/a-practical-guide-to-monitoring-bullmq-queues-with-an-agent-based-approach-that-keeps-redis-40de</guid>
      <description>&lt;p&gt;devopscover_image: &lt;a href="https://qcanary.dev/screenshots/dashboard-overview.png" rel="noopener noreferrer"&gt;https://qcanary.dev/screenshots/dashboard-overview.png&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Background jobs are the invisible backbone of your Node.js app. When they fail, they fail silently. &lt;/p&gt;

&lt;p&gt;BullMQ is a great default for Node.js background jobs. It's fast, Redis-backed, and flexible enough for emails, reports, webhooks, and retry-heavy workflows. But once BullMQ is in production, a critical question shows up quickly:&lt;/p&gt;

&lt;p&gt;How do you monitor queues without giving another service direct access to Redis?&lt;/p&gt;

&lt;p&gt;Many hosted queue dashboards ask for your Redis URL. That works, but it comes with heavy security tradeoffs:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔑 Credential exposure. Your production Redis URL gets stored by a third party.
🔥 Firewall changes. You have to open port 6379 or set up VPC peering—just for a dashboard.
📋 Compliance surface. SOC 2 and zero-trust policies require a vendor risk assessment for that access.
🗄️ Full data access. Redis has no row-level security. Once someone has the URL, they have everything.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This post walks through an alternative: using BullMQ's built-in QueueEvents emitter to monitor queues without ever sharing a Redis credential.&lt;br&gt;
The Problem With Direct Redis Monitoring&lt;/p&gt;

&lt;p&gt;A traditional hosted queue monitor connects directly to Redis and inspects queue state. That's convenient for building dashboards, but uncomfortable for production systems.&lt;/p&gt;

&lt;p&gt;Depending on your Redis permissions and deployment model, a Redis connection can expose far more than queue metrics — keys, job state, retry metadata, delayed jobs, and sometimes payload data. Even when a vendor is trustworthy, the credential becomes another secret to audit, rotate, and protect.&lt;/p&gt;

&lt;p&gt;The better model is simple:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Keep Redis private. Send only the monitoring events you actually need.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;How BullMQ QueueEvents Changes the Game&lt;/p&gt;

&lt;p&gt;BullMQ emits lifecycle events as jobs move through a queue. QueueEvents is the BullMQ primitive for subscribing to those changes inside your own process:&lt;/p&gt;

&lt;p&gt;completed → failed → active → waiting → stalled → delayed → drained&lt;/p&gt;

&lt;p&gt;Instead of polling Redis from a hosted service, you can subscribe from inside your runtime and translate these lifecycle events into monitoring records.&lt;/p&gt;

&lt;p&gt;This eliminates:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; ✅ Inbound network access to Redis
 ✅ Redis credential sharing with vendors
 ✅ The need to inspect arbitrary Redis keys
 ✅ Firewall changes and VPC peering
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Monitoring data is buffered locally and sent over standard outbound HTTPS.&lt;br&gt;
The Agent Pattern in Practice&lt;/p&gt;

&lt;p&gt;Here's how it works in a real application. First, install the agent:&lt;br&gt;
bash&lt;/p&gt;

&lt;p&gt;npm install @qcanary/agent&lt;/p&gt;

&lt;p&gt;Then attach it to your existing BullMQ queues:&lt;br&gt;
typescript&lt;/p&gt;

&lt;p&gt;import { Queue } from "bullmq";&lt;br&gt;
import { QueueMonitor } from "@qcanary/agent";&lt;/p&gt;

&lt;p&gt;const emailQueue = new Queue("email", {&lt;br&gt;
  connection: { host: "127.0.0.1", port: 6379 },&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;const monitor = new QueueMonitor({&lt;br&gt;
  apiKey: process.env.QCANARY_API_KEY,&lt;br&gt;
  queues: [emailQueue],&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;await monitor.start();&lt;/p&gt;

&lt;p&gt;The agent does a few things under the hood:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attaches to QueueEvents as a local subscriber — your Redis connection never leaves the process.
Buffers events into batches (100 events or 5 seconds, whichever comes first).
Ships only lightweight metadata over HTTPS: job ID, queue name, status, duration, error message.
Retries with exponential backoff on failure.
Drops oldest events if the buffer fills — monitoring should never slow down your workers.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;No job payloads. No Redis keys. No connection strings. Just events.&lt;br&gt;
Setting Up Your First Alert&lt;/p&gt;

&lt;p&gt;Dashboards are useful. Alerts are what make monitoring operational.&lt;/p&gt;

&lt;p&gt;A good first alert is failure rate:&lt;br&gt;
text&lt;/p&gt;

&lt;p&gt;Queue: email&lt;br&gt;
Condition: failure rate &amp;gt; 5%&lt;br&gt;
Window: 10 minutes&lt;br&gt;
Destination: Slack&lt;/p&gt;

&lt;p&gt;One failed job may be noise. A sustained 5% failure rate over 10 minutes usually means something changed: a provider outage, expired credentials, a bad deploy, or a slow dependency.&lt;/p&gt;

&lt;p&gt;Other useful alerts to configure:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; No activity — catch queues that silently stopped receiving work.
 Queue depth — catch growing backlogs before users feel the delay.
 Job duration — catch slow jobs and external dependency regressions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Comparing the Approaches&lt;br&gt;
Aspect&lt;/p&gt;

&lt;p&gt;Sharing Redis URL&lt;/p&gt;

&lt;p&gt;Agent-Based (QueueEvents)&lt;br&gt;
Credential exposure Redis URL stored by vendor  None — Redis stays local&lt;br&gt;
Network changes Open port 6379 or VPC peering   None — outbound HTTPS only&lt;br&gt;
Compliance impact   Vendor risk assessment required None — no shared infrastructure&lt;br&gt;
Data sent   Direct Redis access (everything)    Only lifecycle metadata&lt;br&gt;
Payload visibility  Vendor can read all job data    Only metadata — payloads stay local&lt;br&gt;
Setup time  10 minutes + network config 5 minutes, no network config&lt;/p&gt;

&lt;p&gt;What to Monitor First&lt;/p&gt;

&lt;p&gt;Start with high-impact queues:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Email delivery
 Billing webhooks
 User notifications
 Scheduled reports
 Data imports and exports
 Third-party sync jobs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Keep thresholds conservative at first. After a week of history, tune them based on normal production behavior.&lt;br&gt;
What's Next for QCanary? (Expanding the Ecosystem)&lt;/p&gt;

&lt;p&gt;Right now, QCanary is built specifically for BullMQ because of its clean event model. But we know the Node.js ecosystem is diverse.&lt;/p&gt;

&lt;p&gt;We are actively expanding our agent to support the original Bull library, so legacy Node.js applications can secure their monitoring without migrating their queue infrastructure. Furthermore, we are laying the groundwork to support queue systems in other languages, bringing this same zero-trust, agent-based observability to Python, Go, and beyond. &lt;/p&gt;

&lt;p&gt;If you're using Bull or another queue system, we'd love to hear from you!&lt;br&gt;
When Sharing Redis Still Makes Sense&lt;/p&gt;

&lt;p&gt;The agent pattern isn't always the right call:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Local development — credential sharing doesn't matter on localhost.
 Full payload inspection needed — if you need to see job data in the dashboard, the agent can't help (it intentionally excludes payloads).
 Existing VPC peering — if the network boundary is already established, sharing Redis adds no new attack surface.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;For everyone else — production deployments with VPCs, compliance requirements, or zero-trust policies — the agent pattern eliminates an entire class of security risk.&lt;br&gt;
Try It Free&lt;/p&gt;

&lt;p&gt;QCanary provides real-time dashboards, Slack/email/webhook alerts, and job history for BullMQ — all without ever seeing your Redis URL.&lt;/p&gt;

&lt;p&gt;Free tier: 1 project, 3 queues, 10K events/day, 3-day history.&lt;br&gt;
Setup: Install the agent, attach to your queues, events start flowing.&lt;/p&gt;

&lt;p&gt;→ qcanary.dev&lt;br&gt;
→ qcanary.dev/docs&lt;/p&gt;

&lt;p&gt;Found this useful? Follow me for more Node.js infrastructure content. Questions? Drop them in the comments below.&lt;/p&gt;

</description>
      <category>node</category>
      <category>bullmq</category>
      <category>redis</category>
    </item>
  </channel>
</rss>
