BullMQ Jobs Stuck in Waiting State on Render Background Workers

BullMQ jobs stuck waiting on Render is one of the quieter failure modes in this genre, because there's genuinely nothing wrong to find in your application logs. Jobs get added to the queue successfully. They sit in the waiting list. They stay there indefinitely, with no error, no crash, no retry attempts failing loudly — because the piece of infrastructure that's supposed to be watching for them was never actually running as something capable of watching.
Short answer: the most common cause isn't a bug in your worker code at all — it's that the worker was never deployed as a distinct, continuously running Render service in the first place. BullMQ workers need a process that's actually executing new Worker(...) somewhere reachable, and it's easy to write that code, commit it, deploy your web service, and never notice that nothing on Render is actually running it.

Why Jobs Enqueue Fine But Never Move to Active on Render
Adding a job to a BullMQ queue and having a worker actually process it are two completely independent things — the Queue producer succeeds the moment Redis accepts the job, regardless of whether anything is listening on the other end. If your BullMQ Worker instantiation lives in the same file or the same service as your web API, and Render's start command for that web service only ever runs your HTTP server's entry point, the worker code you wrote is sitting in your repository, correctly written, and never executed by anything.
Render's own documentation on Background Workers describes exactly the service type built for this: a process that "runs continuously... but doesn't receive any incoming network traffic," specifically meant for polling a task queue. A web service and a background worker are two separate deployable things on Render, and BullMQ needs the second one running independently, not folded quietly into the first.
(If you've been re-reading your Worker constructor call looking for a bug in the configuration — it might be fine. The question isn't whether the code is correct. It's whether anything on Render is actually executing it right now.)
Fix 1: Deploy the Worker as Its Own Render Background Worker Service
The direct fix is defining the worker as a genuinely separate service, not a code path inside your web service:
1# render.yaml
2services:
3 - name: api
4 type: web
5 runtime: node
6 buildCommand: npm install && npm run build
7 startCommand: node dist/main.js
8
9 - name: job-worker
10 type: worker
11 runtime: node
12 buildCommand: npm install && npm run build
13 startCommand: node dist/worker.js1// src/worker.ts — this file's only job is to run the BullMQ worker continuously
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 maxRetriesPerRequest: null,
12 },
13);
14
15worker.on('completed', (job) => console.log(`Job ${job.id} completed`));
16worker.on('failed', (job, err) => console.error(`Job ${job?.id} failed:`, err));type: worker in the blueprint gives you a genuinely separate, continuously running Render service — no HTTP port expected, no health check treating "not serving requests" as unhealthy, just a process whose entire job is polling the queue. Render's blueprint specification documents every field available for both service types if your setup needs more than what's shown here.

Fix 2: Confirm the Worker Service Actually Has the Redis Connection
Render scopes environment variables per service — a REDIS_URL you set on your web service doesn't automatically exist on a separately deployed worker service unless you configure it there too. A worker silently connecting to the wrong Redis instance, or failing to connect at all, produces the exact same symptom as no worker running: jobs sit in waiting, and nothing in the worker's own limited logs points at the actual cause unless you're specifically checking which Redis instance it's talking to.
1# Both services need this pointing at the SAME Redis instance
2REDIS_HOST=your-redis-instance.render.com
3REDIS_PORT=6379Set this explicitly on both the api and job-worker service definitions in Render's dashboard (or in render.yaml under each service's envVars) — assuming it carries over between services because they're in the "same project" is exactly the assumption that produces this bug.
Fix 3: Verify the Queue Name Matches Exactly
BullMQ's producer and consumer are connected purely by a matching queue name string — nothing enforces that the name you're adding jobs to and the name your worker is listening on are actually the same:
1// producer — in the web service
2const queue = new Queue('email-queue', { connection });
3await queue.add('welcome-email', { userId: user.id });1// worker — must be listening on the identical string
2const worker = new Worker('email-queue', async (job) => { /* ... */ }, { connection });'email-queue', 'emailQueue', and 'email_queue' are three different queues as far as BullMQ and Redis are concerned. BullMQ's own queues documentation confirms the queue name is the entire link between producer and consumer — there's no additional binding step to misconfigure, which means a string mismatch is both the likeliest cause and the easiest one to fix once you're actually looking for it. A typo-level mismatch here produces jobs that enqueue successfully on one queue while a worker waits patiently, correctly configured, on a completely different one.

The Opinion Part
Here's the position worth stating plainly: a background worker isn't a feature you add to your web service, it's a separate architectural commitment — its own deployment, its own environment variables, its own failure modes independent of whether your API is healthy. Treating it as "some code that also runs" instead of "a service that needs to be running" is exactly how a queue silently accumulates jobs while every dashboard you're watching shows green. The fix here costs one extra service definition in a YAML file. The cost of skipping it is a queue that quietly does nothing while looking, from every angle except the right one, like it's working fine.
Conclusion
If BullMQ jobs are stuck in waiting on Render, check first whether a worker is actually deployed as its own running service at all — the single most common cause isn't a bug, it's a missing deployment. Confirm both services share the identical Redis connection details, and verify the queue name string matches exactly between wherever jobs are added and wherever the worker is listening.
If the worker is confirmed running and connected correctly but you're seeing lock errors instead of a fully stuck queue, our BullMQ missing lock guide covers that adjacent failure mode, and if the same IPv6-versus-IPv4 mismatch that trips up BullMQ on Railway's private network turns out to be involved here too, our dedicated guide on that covers the connection-level fix directly.
Deploy the worker as what it actually is, point it at the right Redis instance, match the queue name exactly, and watch a queue that was quietly doing nothing start actually doing something.
Frequently Asked Questions
The single most common cause is that no worker process is actually running to consume the queue at all. It's easy to write the BullMQ Worker code inside the same codebase as your web API and assume it runs automatically alongside it — but Render's web services only run what their start command specifies, and if that command boots your API and nothing else, the worker code you wrote simply never executes anywhere.
It's a distinct Render service type built specifically for processes that don't handle HTTP traffic — exactly the shape of a BullMQ worker, which polls a queue rather than serving requests. Deploying your BullMQ worker code as its own Background Worker service, separate from your web API, is the setup Render's own documentation recommends for this use case, and it's what actually guarantees the worker process is running continuously.
The most common secondary cause is a Redis connection mismatch — Render scopes environment variables per service, so a REDIS_URL configured on your web service doesn't automatically exist on a separately deployed worker service unless you explicitly set it there too. A worker connecting to the wrong Redis instance, or no instance at all, will run without errors while never actually seeing the jobs your web service added.
Yes, and it's a surprisingly common typo-level bug. BullMQ's Queue (producer) and Worker (consumer) are connected purely by matching queue name strings — if the producer adds jobs to 'email-queue' and the worker is listening on 'emailQueue' or 'email_queue', BullMQ has no way to know these were meant to be the same queue, and jobs accumulate on one side while a worker waits patiently on a completely different one.
Yes — it's a hard requirement for any BullMQ Worker regardless of platform, not something Render-specific. Without it, ioredis's default retry behavior conflicts with how BullMQ's blocking commands work, and the worker can fail to process jobs in a way that looks identical to the architecture issues covered here. If the fixes in this article don't resolve it, that setting is worth checking next.
