#4608·bullmq

[Bug]: getJobs Lua script (since 5.81.1) blocks Redis for seconds on unbounded ranges default 0-1 turned getDelayed() into a stop-the-world call

Author: PushpenderSaini0Created Aug 22, 2026Updated Sep 2, 2026
Labelsbug

Version

v5.81.3

Platform

NodeJS

What happened?

Summary

The getJobs Lua script introduced in 5.81.1 (fix for #4300, "retrieve jobs in same transaction under getJobs") reads the id range and every job's hash (HGETALL) atomically inside a single script. With the unchanged default range start = 0, end = -1, any call like queue.getDelayed() / queue.getJobs(['completed']) on a large state set now hydrates the entire set inside one script execution, holding Redis' single execution thread for the whole duration.

In our production this blocked Redis/Valkey for >5s, which crossed busy-reply-threshold and caused a full outage: every other client (including Socket.IO pub/sub sharing the instance) received BUSY Valkey is busy running a script for ~6 minutes of degraded service.

Previously (≤5.81.0), the same call was ids-in-script + client-side pipelined HGETALLs slow for big sets, but interleaved with other traffic, so Redis stayed responsive.

Incident data

  • Upgrade path: 5.79.3 → 5.81.3, deployed 2 days before the incident. No app code changed.
  • Redis (Valkey) reported script SHA 2c21b4165511c6c23dffe537928638b4262e5752 still running after 5.3s — this is exactly the SHA1 of the getJobs script content shipped in 5.81.3.
  • Trigger call: queue.getDelayed() (no args → 0..-1) on a queue with a large delayed backlog (delay-heavy workflow jobs; the delayed sets dominate our Redis memory).
  • Redis RSS spiked from ~1.6 GiB to ~4.4 GiB while the script ran (the full result set of job hashes materialized at once), then dropped back.
  • All other clients got BUSY replies; 6,500+ HTTP 5xx across our APIs in ~75 seconds.
  • Rolling back to 5.81.0 (last version without the script) removes the behavior; we verified the script content is byte-identical from 5.81.1 through 6.2.0, so v6 has the same characteristics.

Why this is a problem

Redis Lua atomicity means monopolizing the server. The #4300 fix converted the worst case of an unbounded getJobs from "this caller is slow" (N pipelined round-trips, fair-scheduled against other clients) to "the entire datastore is unavailable for every client" (single O(N) script). The bug it fixed occasional undefined entries when a job is removed between the id read and hydration was recoverable by callers; the new failure mode is not.

The unbounded default end = -1 makes this easy to hit: every getter (getDelayed, getCompleted, getFailed, getJobs) still defaults to the full range, and code that was merely inefficient on ≤5.81.0 becomes an availability hazard after a patch upgrade, with a one-line changelog entry giving no signal to re-audit callers.

Suggested remedies (any of these would help)

  1. Cap or chunk unbounded ranges inside getJobs: when end = -1 (or the window exceeds some N, e.g. 1000), fall back to the pre-5.81.1 two-step/pipelined path, or iterate the script over bounded chunks. Keeps the #4300 atomicity fix for the bounded/paginated case it was designed for.
  2. Deprecate the unbounded default on the public getters, or emit a runtime warning when an unbounded getJobs runs against a set larger than some threshold.
  3. Document the change prominently (docs + a note in the v5 changelog): getJobs-family calls are now atomic and must be bounded in production.

Environment

  • BullMQ 5.81.3 (also verified against 6.2.0), Node.js, ioredis
  • Valkey (Render Key Value), shared with Socket.IO pub/sub

Workaround for anyone hitting this

Pin bullmq to exact 5.81.0 and/or bound every getJobs/getDelayed/getCompleted/getFailed call to an explicit small range. If you were scanning a state set to find one job (our case), track the job id instead and use queue.getJob(id).

How to reproduce.

Reproduction sketch

  1. Add ~200k delayed jobs with non-trivial data (a few KB each).
  2. Call queue.getDelayed() (or any getJobs with default range).
  3. Observe the script holding the Redis execution thread for multiple seconds (SLOWLOG / busy-reply-threshold BUSY replies to other clients), and server memory ballooning by roughly the serialized size of the whole set.

Relevant log output

bash

Code of Conduct

  • I agree to follow this project's Code of Conduct