DEV Community

Cover image for How to get alerted when a BullMQ job fails (before your users do)
Swapnil Jaiswal
Swapnil Jaiswal

Posted on

How to get alerted when a BullMQ job fails (before your users do)

If you run BullMQ in production, you've probably had this moment: a customer tells you their payment/email/export never went through, you go digging, and you find the failed job sitting in Redis — recorded perfectly, retried exactly as configured, and completely silent. Nothing crashed.
No process exited non-zero. Nobody got paged.

That's not a bug. It's how a good queue is supposed to behave. This post is about turning that silence back into an alert, with three concrete approaches and their trade-offs.

Why BullMQ failures are silent by default

To a queue, failed is a normal lifecycle state, right next to completed and waiting. When a job throws and exhausts its attempts, BullMQ moves it into the failed set and picks up the next job. Your worker is healthier for handling it gracefully — which is exactly why none of your usual signals (a dead process, an HTTP 500, a crash loop) ever fire.

Two things make it worse:

  • Retries hide the early warning. A job with attempts: 5 fails four times quietly before it fails "for real." By the time it lands in the failed set, the queue's been unhealthy for minutes.
  • The failed event only fires where you're listening. Scale to four worker pods and each one only sees its own failures. Redeploy and the listener is gone until the new process boots.

Approach 1: a failed listener (the 15-minute version)

The instinct is to attach a listener:

import { Worker } from 'bullmq'

const worker = new Worker('payments', processPayment)

worker.on('failed', (job, err) => {
  console.error(job?.id, err.message)
  // ...post to Slack?
})
Enter fullscreen mode Exit fullscreen mode

This is better than nothing, but it has three problems every production setup hits:

  1. It only runs in this process. No aggregate view across pods.
  2. It logs, and logs aren't alerts. A console.error goes into a stream nobody tails at 3am.
  3. It restarts with the process. Jobs that fail during a redeploy are recorded in Redis but never emitted to your handler.

If you go this route, at minimum use QueueEvents instead of worker.on — it reads lifecycle events over Redis pub/sub, so it sees every worker's jobs:

import { QueueEvents } from 'bullmq'

const events = new QueueEvents('payments')
events.on('failed', ({ jobId, failedReason }) => {
  // count it, don't just log it — and alert on the *rate*, not a single failure
})
Enter fullscreen mode Exit fullscreen mode

The hard part isn't catching the event. It's everything after: counting failures over a rolling window, deciding what's a real incident vs. noise, grouping 1,000 identical errors into one page, and routing to a human with a cooldown so you're not paged in a loop.

(This is the exact problem we dug into in why BullMQ jobs fail silently)

Approach 2: Prometheus + Grafana

If you already run a metrics stack, you can export queue metrics (there are community exporters like bull-monitor, or roll your own from QueueEvents) and build Grafana alerts.

Good for: full control, unified with your other dashboards.
Cost: days of wiring exporters and tuning alert rules, and you still build failure grouping yourself. Worth it if you have the platform team; heavy if you don't.

Approach 3: a hosted monitor

Dashboards like Taskforce.sh (from the BullMQ team) give you a polished UI with metrics and email alerts by connecting to your Redis. If you want zero-Redis-access and root-cause failure clustering with alerts to Slack/PagerDuty/webhooks, that's what we built PipeRadar for. It's an SDK you drop into your worker — it hooks the same QueueEvents and pushes lightweight metadata out over HTTPS, so it never connects to your Redis or reads your job payloads:

import { PipeRadar } from '@piperadar/bullmq'

const pr = PipeRadar({ apiKey: 'pr_live_...' })
pr.watch(paymentQueue)   // failure rate, latency, backlog & alerts — done
Enter fullscreen mode Exit fullscreen mode

It counts completions and failures per minute, fingerprints failures into incidents (so 1,000 identical errors are one alert, not a thousand), keeps history, and pages you on a rate change. There's a free tier, no credit card.

Which should you pick?

  • Just need to stop the bleeding today? QueueEvents + a Slack webhook, alert on a failure count.
  • Already have Grafana and a platform team? Export metrics and build the rules there.
  • Want alerting + grouping + history without wiring it yourself? A hosted monitor.

Whatever you choose, the principle is the same: watch from outside the worker, alert on the rate (not a single failure), and group identical failures so a retry storm is one page.


I write about running BullMQ in production at piperadar.dev/blog — including a deeper dive on why BullMQ jobs fail silently and the complete monitoring guide.

Top comments (0)