Pluggable Redis Clients in BullMQ
Pluggable Redis Clients in BullMQ
Historically, BullMQ has depended on the popular Redis client ioredis.
This client has been working very reliably for us, and we can only be grateful to its authors for
their exceptional work over the years.
From time to time, however, some users wondered why we didn't also support node-redis,
which is probably the second most popular Redis client for Node.js. On top of that, with the increased popularity of Bun,
yet another Redis client appeared (Bun's native Redis client), so we felt it was time to do something about it and
provide support for these other clients.
Starting with version v5.77.0, BullMQ talks to Redis through a
small adapter interface called IRedisClient, and three drivers are supported out of the box:
- ioredis, the long standing default, used automatically when you pass connection options the same way you always have.
- node-redis (a.k.a.
@redis/clientv5), wrapped throughcreateNodeRedisClient. - Bun's built-in Redis client, wrapped through
createBunRedisClient.
The adapter interface gives BullMQ a single place to express the operations it actually needs (Lua script registration, transactions, pipelines, hash and stream commands, key scans, blocking pops) without caring which driver implements them. Each adapter then translates that interface to the underlying client's idioms.
In this article we will walk through how the new adapters work, how to opt in to each one, and what to keep in mind about dependencies.
A note on dependencies and backwards compatibility
Because we wanted to release this feature without breaking backwards compatibility, BullMQ still depends on ioredis
even if you intend to use one of the other adapters. In future versions we will rework this so that no dependency
on any specific Redis client is required, only the one you actually choose to use.
- If you keep using
ioredis, nothing changes.ioredisis still BullMQ's direct dependency and you do not have to install anything extra. - If you want to use
node-redis, installredis@^5in your own project. BullMQ declares it as a peer dependency for the adapter. - If you want to use Bun's built-in
RedisClient, you only need to be running on Bun. The class is part of the Bun runtime itself, there is nothing to install.
Using ioredis (the default)
Nothing has changed. If you were already doing this, keep doing it:
import { Queue, Worker } from "bullmq";
const connection = { host: "localhost", port: 6379 };
const queue = new Queue("emails", { connection });
const worker = new Worker(
"emails",
async (job) => {
await sendEmail(job.data);
},
{ connection },
);
You can also pass a pre built ioredis instance, which is the recommended pattern when you want to
share connection options across multiple queues:
import IORedis from "ioredis";
import { Queue, Worker } from "bullmq";
const connection = new IORedis({
host: "localhost",
port: 6379,
maxRetriesPerRequest: null, // required for blocking workers
});
const queue = new Queue("emails", { connection });
const worker = new Worker(
"emails",
async (job) => {
await sendEmail(job.data);
},
{ connection },
);
Using node-redis (v5)
BullMQ ships a thin wrapper, createNodeRedisClient, that exposes a node-redis raw client through the IRedisClient interface:
import { createClient } from "redis"; // your dependency
import { Queue, Worker, createNodeRedisClient } from "bullmq";
const raw = createClient({ url: "redis://localhost:6379" });
const connection = createNodeRedisClient(raw);
const queue = new Queue("emails", { connection });
const worker = new Worker(
"emails",
async (job) => {
await sendEmail(job.data);
},
{ connection },
);
A few things worth knowing:
- The raw client is created and configured by your application, exactly the way
node-redis's own docs recommend. BullMQ never reaches intocreateClientfor you. - You do not need to call
await raw.connect()yourself. BullMQ will connect the client lazily the first time it needs it (see the note on connection lifecycle below). You can still call it if you prefer to fail fast at startup; it is harmless to do so before passing the client to BullMQ. - RESP3 is fully supported. If you want it, pass
RESP: 3when you create the client:
const raw = createClient({
url: "redis://localhost:6379",
RESP: 3,
});
Using Bun's built-in RedisClient
Bun 1.3 introduced a native RedisClient written in Zig. It is RESP3 only, has no Lua defineCommand style API,
and uses a single send(command, args) method for arbitrary Redis commands. The createBunRedisClient
adapter bridges all of that:
import { RedisClient } from "bun";
import { Queue, Worker, createBunRedisClient } from "bullmq";
const raw = new RedisClient("redis://localhost:6379");
const connection = createBunRedisClient(raw);
const queue = new Queue("emails", { connection });
const worker = new Worker(
"emails",
async (job) => {
await sendEmail(job.data);
},
{ connection },
);
You run this exactly like any other Bun program:
bun run worker.ts
There is no extra dependency to install. Bun.RedisClient is part of the runtime. Note that this only works under Bun,
running the same code under Node.js will fail because the RedisClient class does not exist there.
A note on connection lifecycle
None of the examples above call connect() on the raw client, and that is on purpose: BullMQ opens the
connection for you. The mechanism is different for each driver (ioredis auto-connects in its constructor,
createBunRedisClient does the same, and for node-redis BullMQ calls raw.connect() itself before
issuing the first command), but from the application's point of view the result is the same.
You can still call await raw.connect() yourself before passing the client to BullMQ if you want to
fail fast at startup when Redis is unreachable. BullMQ will then see an already connected client and
skip its own connect step.
Letting BullMQ create the clients for you
If you let BullMQ manage connections (by passing plain connection options instead of a pre built client),
it normally instantiates an ioredis client internally. You can override that behaviour globally by setting RedisConnection.clientFactory:
import { createClient } from "redis";
import { Queue, RedisConnection, createNodeRedisClient } from "bullmq";
RedisConnection.clientFactory = (opts) => {
const raw = createClient({
socket: { host: opts.host, port: opts.port },
username: opts.username,
password: opts.password,
database: opts.db,
});
return createNodeRedisClient(raw);
};
const queue = new Queue("emails", {
connection: { host: "myredis.taskforce.run", port: 32856 },
});
The factory receives the merged connection options BullMQ would normally hand to ioredis, and must return an IRedisClient.
From that point on, every time BullMQ needs a new connection (for Queue, Worker, QueueEvents, FlowProducer, blocking duplicates, etc.)
it will go through your factory.
Inside the factory, do not call await raw.connect() yourself. The factory is expected to return a
constructed but not yet connected client, and BullMQ will open the connection lazily as described in
the lifecycle section above. With node-redis in particular, calling connect() here would race with
BullMQ's own connect attempt and the second one would throw a Socket already opened error.
The same pattern works for Bun:
import { RedisClient } from "bun";
import { Queue, RedisConnection, createBunRedisClient } from "bullmq";
RedisConnection.clientFactory = (opts) => {
const host = opts?.host ?? "localhost";
const port = opts?.port ?? 6379;
const raw = new RedisClient(`redis://${host}:${port}`);
return createBunRedisClient(raw);
};
This is the cleanest way to flip an existing codebase from ioredis to one of the new adapters: set the factory once at startup,
and every existing new Queue(name, { connection: { host, port } }) keeps working.
When does each option make sense?
A few rules of thumb, with the understanding that "it depends on your workload" is the only fully correct answer:
-
Stay on
ioredisif you rely on its Cluster or Sentinel implementation, its specific TLS knobs, or any of its events and options that BullMQ's existing ecosystem already documents. It is also the safest choice while running on Node.js: the most battle tested path through BullMQ. -
Try
node-redisif you prefer its API, want RESP3 today on Node.js, or want to keep your application stack on a single Redis client (node-redisis now whatredisofficially recommends). -
Try
createBunRedisClientif your service already runs on Bun and you want to drop one dependency (ioredis) in favour of the runtime built in.
You can also mix: nothing prevents one service from using ioredis and another from using createNodeRedisClient,
against the same Redis instance and against the same queues.