Build1 publisher3 min readPublished
The queue failure that costs you is silent, and it is what BullMQ tutorials skip
A dev.to walkthrough on NestJS and BullMQ starts where demos stop: retries with jitter, idempotency keys, and jobs that look done but are stuck.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- A dev.to post titled "Building Resilient Background Jobs in NestJS with BullMQ" by menard_codes opens by saying background jobs look simple right up until one dies silently in production and nobody notices for three days.
- The post gives examples: a job sending confirmation emails stops running, a job syncing inventory data quietly falls behind, and nobody gets an error because from the queue's perspective nothing crashed; the job just failed and nobody was watching.
- The post states that most BullMQ tutorials stop at "job added, job processed", which is fine for a demo but not what happens in a real system.
- The post states that in production external APIs time out, workers restart mid-job, and retries without the right safeguards can make things worse rather than better.
- The post says it covers five patterns: retries that do not cause a thundering herd, idempotency so retries do not duplicate side effects, dead-letter queues for jobs that keep failing, concurrency limits that protect your database, and how to catch jobs that are "done" but still stuck.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A dev.to post by menard_codes on background jobs in NestJS opens with the failure mode that actually costs money: a job dies quietly in production and nobody notices for three days [1]. That matters because the queue does not consider it an incident. A confirmation email job stops running, an inventory sync falls behind, and no error surfaces, because from the queue's point of view nothing crashed [2].
The author's complaint about the genre is fair. Most BullMQ tutorials stop at "job added, job processed" [3], which is fine for a demo and useless once external APIs time out and workers restart mid-job [4]. The setup itself is four lines of install and a module import: `@nestjs/bullmq`, `bullmq`, `ioredis`, a `BullModule.forRoot` connection, a registered queue, and a processor extending `WorkerHost` [6]. Nothing after that is boilerplate.
Start with the retry, because the naive version is actively dangerous. Setting `attempts: 5` and moving on means BullMQ retries as fast as it can, so five instant retries from every failed job can turn a downstream blip into an outage [7][8]. Exponential backoff with a 1000 ms base spaces those to 1s, 2s, 4s, 8s, 16s [9]. The part usually left out is that this only delays the herd: if thousands of jobs fail in the same second, they all retry in near-lockstep [10]. The post's custom strategy caps the base at 30000 ms and adds up to 30 percent random jitter, returning `base + jitter` [11]. Do the arithmetic on it: the cap engages at `attemptsMade` 5, where the raw value would be 32000 ms [12], and the widest single delay becomes 39 seconds [13]. That is your spread, and it is worth knowing before you tell an on-call engineer how long a backlog will take to drain.
Retries also assume the job is safe to run twice, and that assumption breaks when the first attempt succeeded downstream but the worker crashed before reporting success [14]. The remedy shown is an idempotency key: a Prisma-backed `hasRun`/`markComplete` pair, with the key built from the work itself, `send-email:${userId}:${templateId}`, checked at the top of `process` so a retry returns as a no-op [15]. Read the ordering carefully. The check precedes the side effect, and the completion record is written after it, which leaves a window where the email is sent and the key is not yet recorded. That is a smaller window than the one you started with, not a closed one.
Now the honest limitation. The post names five patterns, including dead-letter queues for jobs that keep failing, concurrency limits that protect your database, and detection for jobs that are "done" but still stuck [5]. The excerpt supplied here truncates mid-code inside the idempotency processor, before any of those three are shown [16]. So the interesting questions stay open: what the concurrency cap is tied to in practice, who reads the dead-letter queue, and what threshold marks a job as stalled.
Those three are where silent failure actually lives. A retry policy with jitter and an idempotency table make individual jobs survivable; they do nothing about a worker holding a lock on a job it will never finish, or a dead-letter queue nobody has a dashboard for. If you are copying these patterns, the thing to add is the alert on queue depth and on dead-letter arrivals. Watch whether the full post specifies a stalled-job threshold and ties worker concurrency to a concrete connection pool size, because a concurrency number chosen without reference to database capacity is a guess wearing a config file.