BullMQ code, minus Redis.
bunqueue keeps the BullMQ Queue and Worker API. Most migrations are an import change and a deleted Redis config. This page shows the diff first, then lists every real difference.
This page is for BullMQ users. It shows the full before/after up front, then walks the steps and the few API differences that actually matter.
The whole migration, one diff
Section titled “The whole migration, one diff”// Before: BullMQ + Redisimport { 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); return { sent: true };}, { connection, concurrency: 5 });
// After: bunqueue, no Redisimport { Queue, Worker } from 'bunqueue/client';
const queue = new Queue('emails', { embedded: true, dataPath: './data/bunq.db' });const worker = new Worker('emails', async (job) => { await sendEmail(job.data); return { sent: true };}, { embedded: true, concurrency: 5 });// Before: BullMQ + Redisimport { 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); return { sent: true };}, { connection, concurrency: 5 });
// After: bunqueue server + bunqueue-client, no Redisimport { Queue, Worker } from 'bunqueue-client';
const queue = new Queue('emails'); // connects to localhost:6789const worker = new Worker('emails', async (job) => { await sendEmail(job.data); return { sent: true };}, { concurrency: 5 });Staying on Node.js or Deno? Install bunqueue-client instead of bunqueue, start the server once with bunx bunqueue start --data-path ./data/bunq.db, and keep your runtime.
Embedded mode (embedded: true) ships in the Bun bunqueue package only. On Node.js and Deno, bunqueue-client connects to a bunqueue server; the Queue and Worker API is otherwise the same.
embedded: true runs the queue inside your process, backed by a local SQLite file, so there is no queue server to operate. dataPath says where that file lives.
If your producers and workers run in separate processes or machines, start a bunqueue server (bunqueue start) and replace embedded: true with connection: { host, port }. The rest of the code is identical. See Server Mode.
Step by step
Section titled “Step by step”bun remove bullmq ioredisbun add bunqueueThen, in your code:
- Change imports from
'bullmq'to'bunqueue/client'.Queue,Worker,QueueEvents, andFlowProducerall exist under the same names. - Delete the Redis connection object. Pass
embedded: trueplus adataPath, orconnection: { host, port }for server mode. - Run your test suite. Job options, events, and flows behave the same, the differences are listed below.
- Remove the Redis server from your infrastructure.
What stays the same
Section titled “What stays the same”- Classes:
Queue,Worker,QueueEvents,FlowProducer, same constructors minus the Redis connection. - Job options:
priority,delay,attempts,backoff(including the{ type, delay }object form),jobId,removeOnComplete,removeOnFail,repeat. jobIddeduplication: adding the samejobIdtwice returns the existing job instead of creating a duplicate, exactly like BullMQ.- Events:
worker.on('completed' | 'failed' | 'progress', ...)fire with the same signatures. - Job states: the full BullMQ v5 state machine, including
prioritizedandwaiting-children. - Flows:
FlowProducer.add(),addBulk(),getFlow(),queuesOptions,failParentOnFailure,removeDependencyOnFailure. The Bun package sends one broker-sidePUSHFgraph; configured SQLite commits all rows before publication, providing the same all-or-nothing creation guarantee without client-side rollback. - Per-worker rate limiting:
limiter: { max, duration }onWorkerOptions, unchanged.
// This BullMQ code runs as-is on bunqueueawait queue.add('task', data, { priority: 1, delay: 5000, attempts: 3, backoff: { type: 'exponential', delay: 1000 }, removeOnComplete: true, jobId: 'order-123',});What changes
Section titled “What changes”Backoff
Section titled “Backoff”Backoff is the wait time between retries. Both BullMQ forms work, plus a shorthand:
backoff: { type: 'exponential', delay: 1000 } // same as BullMQbackoff: { type: 'fixed', delay: 5000 } // same as BullMQbackoff: 1000 // shorthand: exponential with 1000ms baseExponential retries wait roughly 2s, 4s, 8s with a 1000ms base (delay * 2^attempts). bunqueue adds automatic jitter (a small random spread) so thousands of failed jobs do not retry at the same instant, and caps delays at 1 hour by default.
Repeatable jobs
Section titled “Repeatable jobs”BullMQ’s legacy repeat: { cron: '...' } key becomes pattern:
await queue.add('task', data, { repeat: { pattern: '0 * * * *' } }); // cron syntaxawait queue.add('task', data, { repeat: { every: 3600000 } }); // fixed intervalremoveOnComplete is boolean only
Section titled “removeOnComplete is boolean only”BullMQ accepts removeOnComplete: { age, count } for retention rules. bunqueue accepts only true or false. Use queue.clean() for age-based cleanup.
Sandboxed processors
Section titled “Sandboxed processors”BullMQ lets you pass a file path as the processor to run it in a child process. In bunqueue, move that logic into an inline processor function (recommended, production-ready):
const worker = new Worker('queue', async (job) => { // the same logic from your processor.js return result;}, { embedded: true, concurrency: 4 });An experimental SandboxedWorker (built on Bun Workers) exists if you need process isolation. See Worker vs SandboxedWorker.
Queue-level rate limiting
Section titled “Queue-level rate limiting”The per-worker limiter works as in BullMQ. bunqueue also offers a queue-level limit:
queue.setGlobalRateLimit(100); // max 100 jobs per second across all workersqueue.setGlobalRateLimit(100, 60_000); // max 100 jobs per minuteThe optional second duration argument is the window in milliseconds and is
enforced by the broker in both embedded and TCP modes. It defaults to 1,000 ms.
No Redis, no Redis Cluster
Section titled “No Redis, no Redis Cluster”bunqueue is single-instance: one server process owns the SQLite file. Many workers can connect to it over TCP, but you cannot shard the queue itself across servers like Redis Cluster. If you need that, see when BullMQ is the better pick.
Migration checklist
Section titled “Migration checklist”-
bun remove bullmq ioredis,bun add bunqueue - Imports point to
bunqueue/client - Redis connection config deleted,
embedded: true+dataPath(orconnection) added -
repeat: { cron }renamed torepeat: { pattern } -
removeOnComplete/removeOnFailobjects replaced with booleans - File-path processors converted to inline processor functions
- Tests pass
- Redis removed from infrastructure
Gotchas
Section titled “Gotchas”- Persistence is opt-in for embedded mode. No
dataPathmeans in-memory only. Server mode readsBUNQUEUE_DATA_PATHor--data-path. - One writer per SQLite file. Do not point two embedded processes at the same database file. For multi-process setups, run one server and connect workers over TCP.
SandboxedWorkeris experimental. Prefer inline processors in production.
Getting help
Section titled “Getting help”Open a GitHub issue or ask in Discussions.