Skip to content
Get started
Get started
Replace BullMQ: Migrate to bunqueue in Minutes
View Markdown
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.

bunqueue 2.9 replaces Croner with Bun 1.4’s native cron parser. Standard five-field schedules, the documented leading-seconds six-field form, and the published shortcuts continue to work. Croner-only extensions such as L, W, #, +, ?, and seven-field years do not.

Before stopping a 2.8 broker, inventory persisted schedules:

Terminal window
bunqueue cron list
bunqueue cron delete legacy-last-day
# Recreate it with a supported schedule through your normal cron add/API path.

Do this while 2.8 is still running for both SQLite and PostgreSQL deployments. On startup, 2.9 validates the complete persisted collection before advancing any missed schedule. If one unsupported definition remains, startup fails with its escaped name and schedule plus remediation guidance; this prevents a silent omission or a due-timer retry loop. Do not hand-edit PostgreSQL cron payloads: replace or delete them through the 2.8 public API.

Interval definitions are also checked before any scheduler, deduplication, or database mutation. repeatEvery must be a positive safe integer in milliseconds. Invalid legacy or corrupt rows fail startup with the cron name and remediation guidance. When both a valid calendar schedule and interval are present, the calendar schedule continues to take precedence for backward compatibility.

// 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) requires the Bun runtime. On Node.js and Deno, bunqueue-client connects to a bunqueue server with embedded: false; the Queue and Worker implementation and API are shared with bunqueue/client.

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; Pro-oriented code may use the QueuePro, WorkerPro, and QueueEventsPro aliases plus the JobPro type.
  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 every row before publishing local state, while PostgreSQL commits the complete graph in one database transaction; both provide all-or-nothing creation without client-side rollback.
  • Per-worker rate limiting: limiter: { max, duration } on WorkerOptions, unchanged.

BullMQ Pro-oriented APIs in the Bun package

Section titled “BullMQ Pro-oriented APIs in the Bun package”

The Pro aliases point directly at bunqueue’s native implementations:

import { QueuePro, WorkerPro, QueueEventsPro } from 'bunqueue/client';
import type { JobPro } from 'bunqueue/client';

No extra package, connection, or license is required. The Bun client supports:

  • job groups with round-robin fairness, priority/FIFO lanes, atomic maxSize, pause/resume, backlog and priority queries, per-group concurrency/rate defaults, local overrides, and manual worker.rateLimitGroup() cooldowns;
  • native processor batches through batch: { size, minSize?, timeout?, groupAffinity? }, job.getBatch(), and selective member.setAsFailed(error);
  • cooperative active-job cancellation through the processor’s second-argument AbortSignal; and
  • structural Observable processor results without requiring RxJS.

These features use the same public contract in embedded memory/SQLite and TCP server mode. PostgreSQL 15–18 makes group capacity, ordering, pause, limits, and manual deadlines authoritative across brokers. BullMQ Pro telemetry and its NestJS integration are intentionally outside this compatibility layer; use bunqueue’s existing metrics/events and framework-neutral Worker lifecycle instead.

Ordinary BullMQ job code remains 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 uses in-memory storage when no persistence backend is configured. A single broker can opt into SQLite persistence while many workers connect over TCP. If the broker tier must scale, standalone servers can instead share a PostgreSQL 15–18 database/namespace (18.6 recommended). This is not Redis Cluster and does not copy its topology or Redis-specific semantics; see storage backends and 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
  • For multi-broker server mode, PostgreSQL 15–18 URL/namespace and unique broker IDs configured
  • 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 or brokers at the same file. Multiple clients use one SQLite server; multiple active brokers use PostgreSQL 15–18; 18.6 is recommended.
  • SandboxedWorker is experimental. Prefer inline processors in production.

Open a GitHub issue or ask in Discussions.