redis_blpopBlocking left-pop — holds the connection open until data is available or timeout expires
BLPOP removes and returns the first element from one or more lists. If the list is empty it blocks the calling client until an element is pushed or the timeout expires. Blocking commands hold a connection open for their entire duration. Without a timeout this is indefinite — exhausting your connection pool when queues are idle.
Common Causes
- —Task queue workers without a timeout configured
- —Long-poll patterns using Redis lists without connection pool isolation
- —Shared connection pool used for both blocking and non-blocking operations
- —Queue consumer loops that do not account for idle periods
How to Fix
- 1.Always specify a timeout — BLPOP key [key ...] timeout; use 0 only with dedicated connections
- 2.Use a separate connection pool for blocking consumers, isolated from request-serving connections
- 3.Consider BullMQ or a purpose-built queue library that manages connection lifecycle
- 4.Use non-blocking LPOP with an application-level retry loop if connection pooling is constrained
Always set a timeout
BLPOP key 0 blocks indefinitely. If your queue goes idle, every consumer connection is held open doing nothing — stealing connections from the rest of your application. Use a finite timeout and loop at the application level.Example
typescript
// BAD — indefinite block, consumes a connection from the shared pool
const result = await redis.blpop('jobs:queue', 0);
// GOOD — bounded timeout, application-level loop
const POLL_TIMEOUT_SECONDS = 5;
async function workerLoop(redis: Redis) {
while (true) {
const result = await redis.blpop('jobs:queue', POLL_TIMEOUT_SECONDS);
if (!result) continue; // timeout — loop and try again
const [_list, payload] = result;
await processJob(JSON.parse(payload));
}
}
// GOOD — dedicated connection for blocking operations
const workerRedis = new Redis({ // separate from main app connection
host: process.env.REDIS_HOST,
port: 6379,
maxRetriesPerRequest: null, // required for blocking consumers
});
workerLoop(workerRedis);Using BullMQ (recommended for task queues)
typescript
import { Queue, Worker } from 'bullmq';
// BullMQ handles connection lifecycle, retries, and concurrency
const queue = new Queue('jobs', { connection: { host: process.env.REDIS_HOST } });
const worker = new Worker('jobs', async (job) => {
await processJob(job.data);
}, { connection: { host: process.env.REDIS_HOST }, concurrency: 5 });
// Enqueue
await queue.add('send-email', { userId: '123', template: 'welcome' });