BullMQ Jobs Not Processing on Railway — Private Network IPv6 Issue

BullMQ jobs not processing on Railway is a genuinely unsettling bug to debug, because every obvious signal says everything is fine. The worker starts. The Redis connection logs as successful. There's no error, no crash, no stack trace pointing anywhere useful. And yet jobs sit in the queue indefinitely, like mail piling up in a box nobody's actually checking, while the worker sits there reporting a clean bill of health.
Short answer: ioredis, the Redis client BullMQ is built on, defaults to IPv4 lookups, while Railway's private network hostnames resolve over IPv6 by default — and the fix is a single connection option, family: 0, telling ioredis to accept either. The initial connection can appear to succeed despite the mismatch, which is exactly why this bug is so much more confusing than a normal connection failure.

Why BullMQ Workers Connect But Never Receive Jobs on Railway
Railway's private network resolves internal service-to-service hostnames — the .railway.internal addresses meant to keep traffic off the public internet — over IPv6 by default. ioredis, the Redis client library BullMQ depends on internally, assumes IPv4 unless told otherwise. That mismatch doesn't always produce an obvious connection failure; depending on the exact DNS and TCP behavior involved, a worker can genuinely connect to Redis while the specific pub/sub mechanism BullMQ relies on to notify workers of new jobs never quite lines up correctly — leaving you with a worker that looks connected and a queue that never drains.
(If this feels familiar, it's because it's the same underlying IPv6-versus-IPv4 mismatch behind NestJS microservices getting ECONNREFUSED on Railway's private network — Railway is being consistent about defaulting to IPv6 internally, and a lot of Node's ecosystem quietly assumes IPv4 everywhere.)
The family: 0 Fix for ioredis and BullMQ
Railway's own documentation is direct about the fix, and it differs slightly depending on whether you're configuring a plain ioredis client or BullMQ specifically:
1// A plain ioredis client — family=0 as a query parameter on the connection string
2import Redis from 'ioredis';
3
4const redis = new Redis(process.env.REDIS_URL + '?family=0');1// BullMQ — family: 0 set explicitly inside the connection object
2import { Queue, Worker } from 'bullmq';
3
4const redisURL = new URL(process.env.REDIS_PRIVATE_URL!);
5
6const connection = {
7 family: 0,
8 host: redisURL.hostname,
9 port: Number(redisURL.port),
10 username: redisURL.username,
11 password: redisURL.password,
12};
13
14const queue = new Queue('emails', { connection });
15const worker = new Worker('emails', async (job) => { /* process job */ }, { connection });family: 0 tells the underlying DNS resolution to look up both IPv4 (A records) and IPv6 (AAAA records) for the Redis hostname, rather than assuming one or the other — exactly the dual-stack behavior Railway's IPv6-by-default private network needs. BullMQ's own connections documentation confirms the connection object is passed straight through to ioredis, so any valid ioredis option — including this one — applies the same way regardless of which BullMQ construct you're configuring it on. Both the queue producer and the worker need this option set identically; a mismatch between the two, where one connects with family: 0 and the other doesn't, can produce the same "one side looks fine, the other doesn't process anything" symptom you started with.

Verifying the Fix With a Minimal Repro
Because this bug can present as a technically-successful connection, don't trust the "connected" log line as proof the fix worked. Confirm a job actually moves through the full cycle:
1// verify.ts — confirms a job is actually added and actually processed, not just that Redis "connects"
2import { Queue, Worker } from 'bullmq';
3
4const connection = { family: 0, host: process.env.REDIS_HOST, port: 6379 };
5
6const queue = new Queue('verify-queue', { connection });
7const worker = new Worker(
8 'verify-queue',
9 async (job) => {
10 console.log('Processed job:', job.id, job.data);
11 },
12 { connection },
13);
14
15worker.on('completed', (job) => console.log(`Job ${job.id} completed successfully`));
16
17await queue.add('test-job', { message: 'if you see this processed, the fix worked' });If the completed event fires within a few seconds, the fix is genuinely working end to end — not just reporting a clean connection while quietly failing at the one thing BullMQ actually needs to do.

The Opinion Part
Here's the pattern worth naming, because it's the same one underneath most of the Railway-specific bugs in this genre: a platform being internally consistent about a sensible default (IPv6 across its private network) collides with an ecosystem of libraries that quietly assumed the older default (IPv4) would always hold. Neither side is wrong — Railway's choice is a reasonable, modern one, and ioredis's IPv4 default matches most of the internet's install base historically. The fix is never "Railway should change" or "ioredis should change"; it's checking, explicitly, whether the two things you're connecting actually agree on the protocol before assuming a "connected" log line means what you think it means. That's a five-minute check that saves an afternoon of watching a queue that refuses to drain for reasons that were never in your business logic at all.
Conclusion
If BullMQ jobs are piling up in the queue on Railway despite a worker that appears connected, check family: 0 before anything else — it's a single connection option that resolves an IPv4-versus-IPv6 mismatch Railway's private network exposes by default. Set it identically on both the queue producer and the worker, and verify the fix with a real job that actually completes rather than trusting a connection log that can look healthy while nothing is actually flowing.
If you're also hitting the equivalent mismatch on a direct NestJS microservices TCP transport rather than BullMQ specifically, our dedicated guide on that variant covers the same underlying fix applied to a different library, and if the same worker is also failing to run at all on a serverless platform rather than Railway, our background jobs on Vercel guide covers that separate, architectural version of "why isn't this job running."
Add the one option both sides were missing, watch the queue actually drain, and get back to whatever your workers were supposed to be doing all along.
Frequently Asked Questions
Because ioredis — the Redis client BullMQ is built on — defaults to IPv4 lookups, while Railway's private network hostnames resolve over IPv6 by default. The initial connection can succeed because of how DNS resolution and TCP handshakes interact, giving you a worker that logs 'connected' and appears healthy, while the specific channel BullMQ uses to notify workers of new jobs never actually establishes correctly.
family: 0 tells ioredis's underlying DNS resolution to look up both IPv4 (A records) and IPv6 (AAAA records) for the Redis hostname, rather than assuming IPv4 only. Railway's own documentation confirms this exact setting as the fix — for a plain ioredis client it's set as a family=0 query parameter on the connection string, and for BullMQ specifically it's set as family: 0 inside the connection object passed to Queue and Worker.
Yes, structurally identical — it's the same underlying mismatch between Railway's IPv6-default private network and Node libraries that assume IPv4 by default. The specific fix differs slightly by library: NestJS's TCP microservices transport needs the listening side configured to bind on both protocols, while ioredis and BullMQ need the family: 0 option set explicitly in their own connection configuration.
Don't trust the 'connected' log line alone — write a small script that adds a real job to the queue and confirms a worker actually picks it up and completes it within a reasonable window. A connection can technically succeed while the specific pub/sub channel BullMQ relies on to notify workers of new jobs silently fails, which is exactly the gap between 'looks connected' and 'is actually processing.'
It would sidestep the IPv6 mismatch, but at the cost of routing internal worker-to-Redis traffic out to the public internet and back, which is both slower and counts against public bandwidth. Configuring family: 0 correctly on the private network keeps traffic internal and fast, and it's a single connection option rather than an architectural downgrade — worth fixing properly rather than working around.
