BullMQ Queue Backing Up on AWS Lambda — Why Serverless Doesn't Fit

BullMQ queue backing up on AWS Lambda looks, at first glance, like a scaling problem — more jobs than the current worker capacity can handle, growing indefinitely. It usually isn't. The far more common cause is that whatever's supposed to be draining the queue was never a real, continuously running BullMQ worker in the first place — it's a Lambda function, triggered periodically or per-job, that briefly exists, does a little work, and disappears again before the next trigger, which is fundamentally not how BullMQ's Worker is built to operate.
Short answer: BullMQ's Worker class depends on a continuous polling loop using Redis's blocking commands, expecting to run in a process that stays alive indefinitely — a Lambda invocation is the structural opposite, terminating after each trigger, which means a "worker" living inside a Lambda handler is never actually draining the queue the way a real worker would. The fix isn't tuning Lambda concurrency or timeout settings. It's running the worker somewhere built to stay running.

Why a Lambda-Triggered "Worker" Isn't a Real Worker Loop
BullMQ's own Worker documentation is direct about the model: a Worker relies internally on Redis's blocking commands and holds an open connection specifically so it can continuously pull the next job the moment one becomes available. That model assumes a process that starts once and keeps running. AWS's own documentation on the Lambda execution environment confirms the opposite by design: a Lambda invocation runs for the duration of a single trigger — a scheduled event, an API call, a queue message — and then the execution environment can be frozen or torn down entirely. A Worker instantiated inside that handler gets cut off mid-loop the instant the invocation ends, regardless of whether it was in the middle of processing something or waiting for the next job to arrive.
(If you've been increasing the Lambda function's timeout hoping it'll eventually "catch up" on the backlog — that treats the symptom as a duration problem. It's not. The worker was never continuously running between invocations, no matter how long a single invocation is allowed to last.)
The Actual Mismatch: Concurrency and Duration
The deeper issue isn't really about Lambda's timeout limit — it's that BullMQ's architecture and Lambda's execution model solve for opposite things. BullMQ's Worker wants exactly one long-lived process continuously polling a queue. Lambda wants many short-lived, independent, parallel invocations, each handling one discrete unit of work and then disappearing. Forcing a continuous-polling model into a discrete-invocation platform means the queue only ever gets processed in disconnected bursts — whatever a single invocation manages to grab before its own execution ends — rather than the steady, continuous drain a real worker provides. If job creation outpaces those bursts even slightly, queue depth climbs and never recovers, because the thing meant to be draining it was never actually running most of the time.

The Fix: Move the Worker to Something That Runs Continuously
The worker doesn't need to be large or expensive — it needs to be continuously running, which is the one property Lambda structurally can't provide. A small AWS Fargate task, a lightweight EC2 instance, or an always-on service on Railway or Render all satisfy this:
1// worker.ts — deployed to a continuously running Fargate task, not a Lambda function
2import { Worker } from 'bullmq';
3
4const worker = new Worker(
5 'email-queue',
6 async (job) => {
7 await sendEmail(job.data);
8 },
9 {
10 connection: { host: process.env.REDIS_HOST, port: 6379 },
11 concurrency: 10,
12 },
13);
14
15worker.on('completed', (job) => console.log(`Job ${job.id} completed`));This process starts once when the task or instance boots, and keeps polling Redis continuously from then on — exactly matching what Worker is built to do, with no artificial invocation boundary cutting it off between jobs. Your API and any genuinely stateless request-response logic can stay on Lambda without conflict; only the worker itself, which needs to run continuously by definition, moves somewhere built for that.

When SQS, Not BullMQ, Is the Right Fit for Lambda
If Lambda specifically is a firm architectural constraint rather than a choice made without realizing the mismatch, the honest answer is that BullMQ probably isn't the right queueing tool for that constraint at all. SQS's model — one Lambda invocation triggered per message, or per small batch — fits Lambda's discrete-invocation execution shape naturally, because it was designed around exactly that pairing rather than assuming a persistent polling process. Choosing BullMQ and then trying to make it work inside Lambda solves the wrong problem; choosing the queueing technology that actually matches the compute model you've committed to is the more direct fix.
The Opinion Part
Here's the position worth stating plainly, and it's the same theme running through most of the "doesn't work on Vercel/Lambda" bugs in this genre: BullMQ is an excellent choice when you have — or are willing to run — a persistent worker process, and a genuinely poor fit for a team that's standardized entirely on serverless functions and doesn't want to run anything continuously. Neither choice is wrong on its own. The mistake is picking BullMQ for its features and then discovering the persistent-process requirement is non-negotiable only after the queue backlog forces the conversation. Decide upfront whether your architecture has room for one small, continuously running thing — because job queues, background workers, and admin dashboards watching them all tend to need exactly that, no matter which specific library you reach for.
Conclusion
If a BullMQ queue keeps growing on AWS Lambda, the backlog isn't a capacity problem to scale your way out of — it's an architecture mismatch between BullMQ's continuous-polling worker model and Lambda's discrete, short-lived invocations. Move the worker to a small, continuously running service (Fargate, EC2, or an always-on platform), keep Lambda for the genuinely stateless parts of your stack, and reconsider SQS specifically if Lambda-only compute is a hard constraint you're not willing to move away from.
If the worker itself is already running continuously somewhere and you're instead debugging job locks, our BullMQ missing lock guide covers that separate, more common failure mode, and if you're hitting the same architectural wall on Vercel specifically rather than Lambda, our background jobs on Vercel guide covers the equivalent mismatch on that platform.
Give the worker a place to actually keep running, and watch a queue depth graph that's been climbing for weeks finally start heading back toward zero.
Frequently Asked Questions
Because BullMQ's Worker class is built around a continuous polling loop using Redis's blocking commands, expecting to run in a process that stays alive indefinitely. A Lambda invocation is the opposite by design — it runs for the duration of a single trigger, then terminates. A Worker instantiated inside a Lambda handler gets cut off the moment that invocation ends, mid-loop, rather than running the way it's actually built to.
You can, but it doesn't behave like a real BullMQ worker — it behaves like a single, brief poll that grabs whatever's immediately available and then disappears again until the next scheduled trigger. If jobs arrive faster than your schedule's interval, or if a single invocation can't clear the current backlog before its own time limit hits, the queue depth climbs steadily because the periodic poll was never actually keeping pace with real-time job creation.
Something built to run continuously — a small AWS Fargate task, an EC2 instance, or a Railway/Render always-on service — where the Worker process starts once and keeps polling Redis indefinitely, exactly matching what BullMQ's architecture expects. This doesn't need to be large or expensive; it needs to be continuously running, which is the one property Lambda structurally can't provide.
Not universally — Lambda pairs well with a queueing model built around discrete, triggered invocations, like SQS, where each message maps naturally to one Lambda execution. The mismatch here is specifically with BullMQ's own worker model, which assumes a persistent polling process. If you're committed to Lambda for background work, SQS's invocation-per-message model fits its execution shape far better than trying to run a BullMQ Worker inside it.
Check whether the worker process is ever actually running continuously at all. If your BullMQ worker only exists inside short-lived Lambda invocations, the backlog is architectural, not a capacity issue — no amount of Lambda concurrency fixes a queue that's being polled in disconnected bursts rather than drained continuously. If a genuinely persistent worker is still falling behind, that's a real capacity question worth scaling for.
