Redis "MaxRetriesPerRequestError" on Upstash — What Actually Causes It

Redis MaxRetriesPerRequestError on Upstash reads exactly like what it sounds like — a connection that gave up after too many failed attempts, implying Upstash itself is unreachable or struggling. With BullMQ specifically, that's almost never what's actually happening. The error usually fires immediately on worker startup, against a completely healthy Upstash instance, because BullMQ enforces a specific connection requirement that has nothing to do with network reachability at all.
Short answer: BullMQ requires maxRetriesPerRequest: null on any Redis connection passed to a Worker or QueueEvents instance, and throws this exact error if that setting is missing — regardless of whether Upstash is actually reachable. Add the setting to the connection you're handing BullMQ, but don't apply it globally to every Redis client in your app, since unlimited retries are the wrong default for anything that isn't specifically a BullMQ worker connection.

Why BullMQ Throws This Error Even Against a Healthy Upstash Instance
By default, ioredis retries a failed command up to 20 times before giving up and throwing MaxRetriesPerRequestError — a sensible default for a client that should eventually stop hammering a genuinely dead server. BullMQ's own connections documentation explains why this default conflicts with how BullMQ actually works: Worker and QueueEvents rely on Redis's blocking commands internally, and BullMQ creates duplicated connections specifically to support that blocking behavior correctly. Those connections need maxRetriesPerRequest set to null — meaning no retry cap at all — and BullMQ will throw an explicit exception rather than silently misbehave if you hand it a connection configured with the default limit instead.
(If this error showed up the moment your worker started, rather than partway through a busy afternoon — that timing is the tell. A genuine Upstash outage doesn't usually announce itself on the very first connection attempt of a brand-new deploy.)
Fix: Set maxRetriesPerRequest: null on BullMQ's Connection Specifically
The direct fix is explicit, on whatever connection object you're passing into BullMQ's constructors:
1// queue.ts and worker.ts — the connection object BullMQ actually uses
2import { Queue, Worker } from 'bullmq';
3
4const connection = {
5 host: process.env.UPSTASH_REDIS_HOST,
6 port: 6379,
7 password: process.env.UPSTASH_REDIS_PASSWORD,
8 tls: {},
9 maxRetriesPerRequest: null,
10};
11
12const queue = new Queue('notifications', { connection });
13const worker = new Worker('notifications', async (job) => { /* ... */ }, { connection });With maxRetriesPerRequest: null set, BullMQ's internal blocking connections behave the way they're designed to, and the exception disappears — not because Upstash suddenly became more reachable, but because the connection is finally configured the way BullMQ requires. Upstash's own BullMQ integration guide confirms this exact setting as the documented, expected configuration for using their Redis with BullMQ specifically, not a workaround specific to this one error.

When This Setting Is Dangerous: Don't Apply It Globally
Here's the detail worth being deliberate about, because it's easy to fix the error by setting maxRetriesPerRequest: null on a shared Redis client used everywhere in your application, not realizing what that actually changes for everything else using that same client. null means unlimited retries — exactly right for BullMQ's blocking worker connections, and exactly wrong for a Redis client also handling caching, session lookups, or rate limiting elsewhere in your app. A cache read that would normally fail fast during a real Redis outage instead retries indefinitely, turning a quick, visible error into a request that silently hangs.
1// redis-clients.ts — two distinct clients, scoped to what actually needs unlimited retries
2import Redis from 'ioredis';
3
4// Dedicated connection for BullMQ only — unlimited retries is correct here
5export const bullmqConnection = {
6 host: process.env.UPSTASH_REDIS_HOST,
7 port: 6379,
8 password: process.env.UPSTASH_REDIS_PASSWORD,
9 tls: {},
10 maxRetriesPerRequest: null,
11};
12
13// Separate client for caching/rate-limiting — should fail fast, not retry forever
14export const appRedis = new Redis({
15 host: process.env.UPSTASH_REDIS_HOST,
16 port: 6379,
17 password: process.env.UPSTASH_REDIS_PASSWORD,
18 tls: {},
19});Keeping the two scoped separately means BullMQ's workers get the unlimited-retry behavior they actually require, while the rest of your application keeps failing fast — and loudly — when Redis genuinely has a problem, rather than everything in your app silently inheriting BullMQ's specific connection needs.

The Opinion Part
Here's the pattern worth naming, because it's a small, specific example of a larger habit worth having: a fix that resolves an error message isn't automatically the right fix, and "does the exception go away" is a much lower bar than "did I actually understand what this setting does everywhere it's applied." Setting maxRetriesPerRequest: null globally makes the error disappear the same way setting it correctly on a scoped connection does — the difference only shows up later, during a real Upstash blip, when one version fails fast and the other quietly hangs every request touching Redis. Scoping the fix to exactly what needs it costs one extra Redis client instantiation. Not scoping it costs a debugging session six months from now that starts with "why did everything just hang instead of erroring."
Conclusion
If BullMQ is throwing MaxRetriesPerRequestError against Upstash, it's very likely not a connectivity problem at all — it's BullMQ's hard requirement for maxRetriesPerRequest: null on the connection you're passing to Worker and QueueEvents. Add it to that specific connection object, and resist the urge to set it globally across every Redis client in your app; scope it to BullMQ alone so the rest of your application keeps failing fast when Redis genuinely has a problem.
If jobs are enqueuing fine but a worker still isn't picking them up after this fix, our BullMQ jobs stuck waiting guide covers the deployment-level causes of that separate symptom, and if you're chasing a similar-looking connection issue on Railway specifically, our IPv6 private networking guide covers that platform-specific variant.
Scope the setting to where it belongs, and let an error that was never really about Upstash's reachability stop showing up for a problem it was never actually describing.
Frequently Asked Questions
By default, ioredis retries a failed command up to 20 times before throwing MaxRetriesPerRequestError, which normally signals a genuinely unreachable Redis server. With BullMQ specifically, the error usually has nothing to do with Upstash being unreachable at all — it's BullMQ enforcing a hard requirement that maxRetriesPerRequest be set to null on any connection passed to a Worker or QueueEvents instance, and throwing an explicit exception if it isn't.
Worker and QueueEvents both rely on Redis's blocking commands internally, and BullMQ creates duplicated connections specifically to support that blocking behavior correctly. ioredis's default retry limit conflicts with how those blocking connections are meant to behave, so BullMQ requires null — meaning unlimited retries on those specific connections — rather than the default capped retry count.
No, and this is the detail worth being deliberate about. null is required and safe on the connection object passed specifically to BullMQ's Worker and QueueEvents, but applying it globally to a shared ioredis client also used for caching, session storage, or rate limiting means those unrelated operations will also retry indefinitely instead of failing fast during a real Redis outage — turning a quick, visible failure into requests that hang silently.
Usually not. If the error appears immediately on worker startup rather than intermittently under load, it's almost always the missing maxRetriesPerRequest: null configuration rather than an actual Upstash outage or network problem. Check your connection configuration before assuming Upstash itself is having an issue.
Create two distinct ioredis client instances — one configured with maxRetriesPerRequest: null and passed only to BullMQ's Queue, Worker, and QueueEvents constructors, and a separate one with normal retry limits for caching, rate limiting, or anything else in your application that should fail fast rather than retry indefinitely when Redis is genuinely unreachable.
