Skip to content
Get started
Get started
Replace BullMQ: Migrate to bunqueue in Minutes
guide · migration

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.

// Before: BullMQ + Redis
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);
return { sent: true };
}, { connection, concurrency: 5 });
// After: bunqueue, no Redis
import { 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 });

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.

Terminal window
bun remove bullmq ioredis
bun add bunqueue

Then, in your code:

  1. Change imports from 'bullmq' to 'bunqueue/client'. Queue, Worker, QueueEvents, and FlowProducer all exist under the same names.
  2. Delete the Redis connection object. Pass embedded: true plus a dataPath, or connection: { host, port } for server mode.
  3. Run your test suite. Job options, events, and flows behave the same, the differences are listed below.
  4. Remove the Redis server from your infrastructure.
  • 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.
  • jobId deduplication: adding the same jobId twice 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 prioritized and waiting-children.
  • Flows: FlowProducer.add(), addBulk(), getFlow(), queuesOptions, failParentOnFailure, removeDependencyOnFailure. The Bun package sends one broker-side PUSHF graph; 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 } on WorkerOptions, unchanged.
// This BullMQ code runs as-is on bunqueue
await queue.add('task', data, {
priority: 1,
delay: 5000,
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
removeOnComplete: true,
jobId: 'order-123',
});

Backoff is the wait time between retries. Both BullMQ forms work, plus a shorthand:

backoff: { type: 'exponential', delay: 1000 } // same as BullMQ
backoff: { type: 'fixed', delay: 5000 } // same as BullMQ
backoff: 1000 // shorthand: exponential with 1000ms base

Exponential 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.

BullMQ’s legacy repeat: { cron: '...' } key becomes pattern:

await queue.add('task', data, { repeat: { pattern: '0 * * * *' } }); // cron syntax
await queue.add('task', data, { repeat: { every: 3600000 } }); // fixed interval

BullMQ accepts removeOnComplete: { age, count } for retention rules. bunqueue accepts only true or false. Use queue.clean() for age-based cleanup.

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.

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 workers
queue.setGlobalRateLimit(100, 60_000); // max 100 jobs per minute

The 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.

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.

  • bun remove bullmq ioredis, bun add bunqueue
  • Imports point to bunqueue/client
  • Redis connection config deleted, embedded: true + dataPath (or connection) added
  • repeat: { cron } renamed to repeat: { pattern }
  • removeOnComplete/removeOnFail objects replaced with booleans
  • File-path processors converted to inline processor functions
  • Tests pass
  • Redis removed from infrastructure
  • Persistence is opt-in for embedded mode. No dataPath means in-memory only. Server mode reads BUNQUEUE_DATA_PATH or --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.
  • SandboxedWorker is experimental. Prefer inline processors in production.

Open a GitHub issue or ask in Discussions.