redis_blmoveBlocking atomic move between lists — connection held until source has data or timeout fires
BLMOVE atomically pops an element from one list and pushes it to another, blocking if the source list is empty. It is the modern replacement for BRPOPLPUSH and supports explicit LEFT|RIGHT direction arguments for both ends of the operation. The blocking behaviour is the same: without a timeout, connections are held open indefinitely.
Common Causes
- —Reliable queue consumers without a timeout configured
- —Blocking consumers sharing a connection pool with the main application
- —Worker processes that do not isolate blocking connections
- —Long idle periods where queues are empty but workers hold connections open
How to Fix
- 1.Always specify a finite timeout — prefer 2–10 seconds and loop at the application level
- 2.Use a dedicated Redis connection for BLMOVE consumers, separate from the main pool
- 3.Set maxRetriesPerRequest: null on the consumer connection for ioredis
- 4.Use BullMQ for production queue patterns — it manages all of this correctly
BLMOVE replaced BRPOPLPUSH in Redis 6.2
If you are migrating from BRPOPLPUSH, use
BLMOVE source dest RIGHT LEFT timeout as the direct equivalent. BLMOVE is more explicit and supports all four direction combinations.Example
typescript
// Signature: BLMOVE source destination LEFT|RIGHT LEFT|RIGHT timeout
// Pop from RIGHT of source, push to LEFT of destination (equivalent to old BRPOPLPUSH)
// BAD — no timeout, connection held indefinitely when queue is empty
const raw = await redis.blmove('jobs:pending', 'jobs:processing', 'RIGHT', 'LEFT', 0);
// GOOD — finite timeout, dedicated connection, application-level loop
const workerRedis = new Redis({
host: process.env.REDIS_HOST,
maxRetriesPerRequest: null, // required for blocking commands in ioredis
});
async function reliableWorker() {
while (true) {
const raw = await workerRedis.blmove(
'jobs:pending',
'jobs:processing',
'RIGHT',
'LEFT',
5 // seconds — application loops after timeout
);
if (!raw) continue; // timeout, no jobs yet
const job = JSON.parse(raw);
try {
await processJob(job);
await workerRedis.lrem('jobs:processing', 1, raw);
} catch (err) {
await workerRedis.lrem('jobs:processing', 1, raw);
await workerRedis.lpush('jobs:dead', raw);
}
}
}
// Direction combinations:
// RIGHT + LEFT — standard FIFO queue (source tail → dest head)
// LEFT + RIGHT — reverse
// LEFT + LEFT — both left-side
// RIGHT + RIGHT — both right-side