BullMQ's Redis queue, six language ports, and the features that only ship in Pro
BullMQ - Message Queue and Batch processing for NodeJS, Python, Elixir, Rust and PHP based on Redis.
At a glance
- What is it?
- BullMQ is a TypeScript job queue on Redis, with ports for Python, Rust, Elixir, PHP and .NET living in one repository. Its build scripts, adapter requirements and compose file show a free package that covers flows and deduplication, a Pro tier for observables and batches, and a PostgreSQL path that shows up in the build scripts and compose file.
- Who is it for?
- BullMQ fits teams that already run Redis and want one queue library shared between Node and a second language, on the understanding that the MIT package stops at flows and deduplication, that the dashboard, observables, group rate limits and batches are paid add-ons, and that the PostgreSQL path visible in the build scripts has no stated upgrade story.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- taskforcesh/bullmq9,599 ★ · +374
| Repository | 2026-07-30 | 2026-11-14 |
|---|---|---|
| taskforcesh/bullmq | 9.2k | 9.6k |
374 Stars in the last 107 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
npm ships dist, and dist is where the Lua lands
package.json sets files to dist and nothing else, so what npm delivers is build output rather than the TypeScript sources. Inside that output the entry points split by module system: main is ./dist/cjs/index.js, module is ./dist/esm/index.js and types is ./dist/esm/index.d.ts, while source names ./src/index.ts for tooling that reads the repository directly. The engines field requires node >=14.18.0, so the manifest still admits Node 14 even though the local test stack runs redis:8-alpine.
The part worth understanding before you change anything is what else the build moves. copy:lua takes every file in src/commands and writes it into dist/cjs/commands and dist/esm/commands, copy:includes:lua repeats that for src/commands/includes, and copy:sql copies src/postgres/migrations and src/postgres/commands into both trees. The Python port gets the same scripts from a different source, because copy:lua:python removes the existing files under python/bullmq/commands and replaces them from rawScripts. The build also starts with clean:dist, which is rimraf dist, and carries a circular:references script that runs madge over ./src.
So part of the behaviour you depend on lives in Lua files copied into the installed package, not only in the TypeScript under src. Alongside those scripts sit .nvmrc, .husky/, commitlint.config.js and renovate.json, the files that hold the contributor toolchain in place.
One install line, then a second decision about the client
The install section gives a single line and nothing else:
$ yarn add bullmqThe language list further down the same file gives the npm equivalent as `npm install bullmq`. There is no init command, no generated config file and no schema step, so the first application code you write is a constructor call with a queue name string.
Adapter choice is the second decision and it carries its own dependency. If you use the node-redis adapter `createNodeRedisClient`, the install has to be paired with `redis >= 5.0.0`. If you use the Valkey Glide adapter `createValkeyGlideClient`, you need `@valkey/valkey-glide`. Both are exercised, and ioredis is exercised too, through separate files in the repository: vitest.node-redis.config.ts, vitest.node-redis.setup.ts, vitest.ioredis.config.ts, vitest.valkey-glide.config.ts and vitest.valkey-glide.setup.ts.
The consequence is that the installer will not catch a mismatch, because the requirement is tied to a named adapter rather than to the package. Pick the adapter, then add its client, and be aware that the version floor for the client moves on its own schedule. There is no equivalent requirement line for a Postgres client in that list, which is one reason the two backends do not feel symmetric.
One queue name, three objects that never talk directly
`Queue` produces work:
import { Queue } from 'bullmq';
const queue = new Queue('Paint');
queue.add('cars', { color: 'blue' });`Worker` consumes it, and the job name is the dispatch key rather than the payload:
import { Worker } from 'bullmq';
const worker = new Worker('Paint', async job => {
if (job.name === 'cars') {
await paintCar(job.data.color);
}
});`QueueEvents` watches the same name from outside the worker process:
import { QueueEvents } from 'bullmq';
const queueEvents = new QueueEvents('Paint');
queueEvents.on('completed', ({ jobId }) => {
console.log('done painting');
});
queueEvents.on(
'failed',
({ jobId, failedReason }: { jobId: string; failedReason: string }) => {
console.error('error painting', failedReason);
},
);The three objects share the string `Paint` and nothing else, so any rename has to happen in all three places. The failure listener in the example destructures only `jobId` and `failedReason`, which makes the id the handle you get for correlating a failure back to the payload you submitted. None of these examples sets concurrency, lock duration, attempts or backoff, and `paintCar` is called with no cancellation signal, so a worker that dies mid-job depends on the lock handling you configure rather than anything shown here.
Flow trees cross queue boundaries, dedup rows split the field
Parent-child work is a separate class, and every node names its own queue:
import { FlowProducer } from 'bullmq';
const flow = new FlowProducer();
const originalTree = await flow.add({
name: 'root-job',
queueName: 'topQueueName',
data: {},
children: [
{
name: 'child-job',
data: { idx: 0, foo: 'bar' },
queueName: 'childrenQueueName',
children: [
{
name: 'grandchild-job',
data: { idx: 1, foo: 'bah' },
queueName: 'grandChildrenQueueName',
},
{
name: 'grandchild-job',
data: { idx: 2, foo: 'baz' },
queueName: 'grandChildrenQueueName',
},
The child sits in `childrenQueueName` while the root sits in `topQueueName`, so a dependency can cross queue boundaries and the fan-out lands in a different queue than the job that spawned it. The tree shown has a root, one child and two grandchildren, and no node carries a retry count, a backoff or a priority, so a grandchild that fails has no recovery path inside the shape of the API shown here.
The comparison table then draws the boundary. Parent/Child Dependencies is marked for BullMQ and BullMQ-Pro. Deduplication (Debouncing) is marked for BullMQ-Pro, BullMQ and Bull, so the free queue shares that single row with the older library. Deduplication (Throttling) is marked for BullMQ-Pro and BullMQ. Agenda is the only queue in the table whose backend is mongo rather than redis.
Four empty rows and a dashboard you have to subscribe to
The same table has four rows where the BullMQ column is blank. Observables, Group Rate Limit, Group Support and Batches Support are marked for BullMQ-Pro and for nothing else in the table, including BullMQ itself. Parent/child dependencies and deduplication, whether debouncing or throttling, are where the free package matches the paid one.
That gap is also why there is no interface in what you install. The section headed Official FrontEnd points at Taskforce.sh and asks you to sign up there for a complete overview of all your queues, to inspect jobs and search them, to retry or promote delayed jobs, and for metrics and statistics. package.json names Taskforce.sh Inc. as the author and ships files as dist, so nothing in the MIT tarball is that interface. The bullmq dashboard question resolves to a hosted paid product rather than to something you can point at your own Redis.
For an operator the effect is concrete. Retrying a job and promoting a delayed one are described as front-end features, so if you do not subscribe, inspecting a stuck job, forcing a retry and correcting a mis-scheduled delay become code you write yourself against whatever the library exposes. Job state, flows and deduplication are what the free package covers, and visibility is treated as a commercial add-on on the maintainer's own platform. Budget for that before you plan an on-call runbook.
Six ports, six release streams, one undocumented PHP row
The language list is the fastest way to price the dependency per runtime. The commands it gives are:
npm install bullmq
pip install bullmq
cargo add bullmq-official --rename bullmq
dotnet add package BullMQThe Rust line renames the crate to `bullmq` on the way in, so your manifest and your import statements deliberately disagree. The Elixir entry is the only row that is not a command at all: it gives a dependency tuple, `{:bullmq, "~> x.x"}`, with the version class left as a placeholder.
The PHP row is the odd one. It names the `php/` directory and stops, with no package name and no version constraint, while every other row gives you something to type. If PHP is your target runtime, the directory is where you have to start reading.
Releases move quickly too: v6.5.4 shipped on 2026-11-02, v6.5.3 on 2026-10-31 and v6.5.2 on 2026-10-30, and the Node package.json version is 6.5.4. The last push landed on 2026-11-02, so all of this is moving while you read it.
The compose file proves the empty-database case, not the upgrade case
The compose file is the clearest statement of what the suites cover. Redis runs as redis:8-alpine with container_name cache on 6379, a detail that breaks scripts written against a name you assumed. A second service runs Dragonfly v2.0.0 on 6380 with its Lua behaviour pinned by environment variables:
DFLY_cluster_mode: 'emulated'
DFLY_lock_on_hashtags: 'true'
DFLY_default_lua_flags: 'allow-undeclared-keys'
HEALTHCHECK_PORT: 6379Those flags are the contract with the generated Lua scripts. Postgres is there as well, on postgres:18-alpine under the name bullmq-postgres with POSTGRES_DB bullmq_test and a pg_isready health check, and the comments spell out the commands:
yarn test:postgres
yarn test:postgres:smoke
POSTGRES_PORT=5433 yarn dc:up
POSTGRES_URL=postgres://postgres:[email protected]:5433/bullmq_test
docker-compose --profile cluster upThe comments also say storage is intentionally ephemeral, that every run migrates from scratch, and that the suites drop their schemas. So a green run tells you the migrations apply to an empty database and nothing about moving an existing schema forward, which is the case that reaches production. The cluster service sits behind the `cluster` profile with static IPs and NAT mapping, and the macOS note means contributors on that platform need the profile flag before integration tests can reach the nodes at all.
Editorial conclusion
BullMQ fits teams that already run Redis and want one queue library shared between Node and a second language, on the understanding that the MIT package stops at flows and deduplication, that the dashboard, observables, group rate limits and batches are paid add-ons, and that the PostgreSQL path visible in the build scripts has no stated upgrade story. Teams that need durable schema migrations on the broker, an included inspection UI, or broker-native routing should settle those points before committing.
Frequently asked questions
What is BullMQ for?
It is a message queue and batch processing system built on Redis, offered from one repository for Node.js, Python, Elixir, Rust, PHP and .NET. The Node package is TypeScript, published as bullmq under the MIT license with homepage https://bullmq.io.
Can BullMQ be used with PostgreSQL?
There is a PostgreSQL path in the repository: a copy:sql script copies src/postgres/migrations and src/postgres/commands into both dist output trees, and the suites run against postgres:18-alpine through vitest.postgres.config.ts. The comparison table lists redis as the backend for BullMQ and does not say which tier ships the PostgreSQL path.
how to install bullmq
The install section gives $ yarn add bullmq, and the language list gives npm install bullmq for the same package. If you use the node-redis adapter createNodeRedisClient you also need redis >= 5.0.0, and the Valkey Glide adapter createValkeyGlideClient needs @valkey/valkey-glide.
What are the key differences between RabbitMQ and BullMQ?
The comparison table does not cover RabbitMQ. It compares BullMQ-Pro, BullMQ, Bull, Kue, Bee and Agenda, marking parent-child dependencies and both deduplication rows for BullMQ, while Observables, Group Rate Limit, Group Support and Batches Support are marked for BullMQ-Pro only.
what is bullmq worker
A Worker binds a queue name to a processor function, as in new Worker('Paint', async job => { ... }), where the processor compares job.name before calling your code. The gist shows no concurrency, lock duration or retry settings on the Worker constructor.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/taskforcesh-bullmq)Star history chart for your README
[](https://hysenlabs.com/projects/taskforcesh-bullmq)Was this useful?