Redis Retry Strategy#
redisQueueRetryOptions is the central retry configuration object used by every BullMQ queue and worker in Langfuse. It is defined in packages/shared/src/server/redis/redis.ts and spread into every createNewRedisInstance() call across 30+ queue files under packages/shared/src/server/redis/.
The retryStrategy Function#
The retry function uses pure exponential backoff — no jitter:
delay = clamp(Math.exp(times), 1_000, 20_000) // milliseconds
times | Math.exp(times) | Clamped delay |
|---|---|---|
| 1 | ~2.7 ms | 1,000 ms (min) |
| 5 | ~148 ms | 1,000 ms (min) |
| 7 | ~1,097 ms | 1,097 ms |
| 10 | ~22,026 ms | 20,000 ms (max) |
| ≥10 | > 20,000 ms | 20,000 ms (max) |
Key properties :
- Retries forever — there is no
return nullbranch that would permanently abandon the connection - Minimum 1 s —
Math.max(..., 1000)prevents instant storm-retry bursts at startup - Maximum 20 s —
Math.min(..., 20000)caps the ceiling - No jitter — all queue connections will retry in lockstep after a Redis restart; this can be a source of synchronized reconnect thundering herd if many queue connections are lost simultaneously
- A
logger.warnfires oncetimes >= 5to surface persistent connectivity issues
reconnectOnError Behavior#
- Returns
falseforMOVEDerrors — these are normal ioredis cluster redirects, not real connection failures - Returns
2forREADONLYerrors — ioredis return value2means "reconnect and automatically replay the failed command." This handles read replicas that become the master during a failover
Scope: What Uses These Options#
redisQueueRetryOptions is applied to :
- Every BullMQ Queue singleton — passed as
additionalOptionstocreateNewRedisInstance({ enableOfflineQueue: false, ...redisQueueRetryOptions }) - Every BullMQ Worker connection —
WorkerManager.register()callscreateNewRedisInstance(redisQueueRetryOptions)to create each worker's dedicated Redis connection
It does not apply to the global redis singleton used for non-queue operations , which uses only defaultRedisOptions.
Base Connection Defaults (defaultRedisOptions)#
Applied to all connections regardless of queue vs. non-queue use :
maxRetriesPerRequest: null— no per-request retry cap; required by BullMQkeepAlive: 10_000ms — prevents middleboxes from killing idle connectionssocketTimeout: 30_000ms — forces reconnect if no data is received; prevents hungmoveToCompleted()from blocking concurrency slotsenableAutoPipelining— controlled byREDIS_ENABLE_AUTO_PIPELININGenv var (default"true")
Environment Variable Configuration#
The retry delay values (1000–20000 ms) and the exponential formula are hardcoded — there are no REDIS_RETRY_* environment variables . Environment variables cover connection details, TLS, cluster, and Sentinel topology but not retry timing:
| Variable | Purpose |
|---|---|
REDIS_HOST / REDIS_PORT / REDIS_AUTH | Standard connection |
REDIS_CONNECTION_STRING | Alternative DSN |
REDIS_CLUSTER_ENABLED / REDIS_CLUSTER_NODES | Redis Cluster mode |
REDIS_SENTINEL_ENABLED / REDIS_SENTINEL_NODES / REDIS_SENTINEL_MASTER_NAME | Sentinel HA mode |
REDIS_TLS_ENABLED and REDIS_TLS_* | TLS options |
REDIS_ENABLE_AUTO_PIPELINING | Pipeline batching |
Known Issue: Sentinel Mode Gap#
redisQueueRetryOptions sets retryStrategy (for data-node reconnects) and reconnectOnError, but does not set sentinelRetryStrategy — the separate ioredis callback for retrying the sentinel sweep when all sentinel addresses are temporarily unreachable .
During a Redis data-node failover, ioredis's default sentinel retry runs at 10 ms intervals. This causes a feedback loop: each failed sentinel sweep emits an error event, which triggers the outer retryStrategy timer, spawning additional concurrent connect() calls with fresh retryAttempts = 0. In severe cases this can exhaust file descriptors and leave all worker connections permanently broken .
Two mitigations have been merged:
- PR #15478 (
guardSentinelDisconnect) — suppresses BullMQ's watchdogdisconnect(reconnect=true)call when the ioredis client is in"connecting"status (the Sentinel master-resolution window), preventing it from entering a terminal"end"state - PR #15512 — adds an opt-in
?failIfQueueConsumptionStuck=truehealth check endpoint that returns HTTP 503 if no BullMQ job activity is observed forLANGFUSE_QUEUE_CONSUMPTION_STUCK_THRESHOLD_MINUTES(default 60 min), enabling Kubernetes to auto-restart wedged workers
Adding an explicit sentinelRetryStrategy with a longer backoff (e.g., Math.min(retries * 200, 10_000)) to createRedisSentinelInstance remains an open community suggestion .