# Replace BullMQ: Migrate to bunqueue in Minutes

Replace BullMQ with bunqueue in minutes: keep your Queue and Worker API, delete Redis, and choose Bun-native SQLite or PostgreSQL multi-broker storage.

Canonical: https://bunqueue.dev/guide/migration/

---

import { Tabs, TabItem } from '@astrojs/starlight/components';

<div class="bq-wrap bq-hero">
  <span class="bq-eyebrow">guide · migration</span>
  <h1 class="bq-hero-h1 bq-bench-h1">BullMQ code, minus <em>Redis.</em></h1>
  <p class="bq-hero-sub">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.</p>
</div>

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 upgrading bunqueue 2.8 to 2.9

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:

```bash
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.

## The whole migration, one diff

<Tabs syncKey="lang">
<TabItem label="Bun">

```typescript
// 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 }
);
```

</TabItem>
<TabItem label="Node.js / Deno">

```typescript
// 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: false });
const worker = new Worker(
  'emails',
  async (job) => {
    await sendEmail(job.data);
    return { sent: true };
  },
  { embedded: false, concurrency: 5 }
);
```

</TabItem>
<TabItem label="Python">

BullMQ is a Node.js library, so the before/after pairs on this page are JavaScript and TypeScript only. If the destination is Python, the target-side API is in the [Python SDK reference](https://github.com/egeominotti/bunqueue/tree/main/sdk/python).

</TabItem>
<TabItem label="PHP">

BullMQ is a Node.js library, so the before/after pairs on this page are JavaScript and TypeScript only. If the destination is PHP, the target-side API is in the [PHP SDK reference](https://github.com/egeominotti/bunqueue/tree/main/sdk/php).

</TabItem>
<TabItem label="Go">

BullMQ is a Node.js library, so the before/after pairs on this page are JavaScript and TypeScript only. If the destination is Go, the target-side API is in the [Go SDK reference](https://github.com/egeominotti/bunqueue/tree/main/sdk/go).

</TabItem>
<TabItem label="Rust">

BullMQ is a Node.js library, so the before/after pairs on this page are JavaScript and TypeScript only. If the destination is Rust, the target-side API is in the [Rust SDK reference](https://github.com/egeominotti/bunqueue/tree/main/sdk/rust).

</TabItem>
<TabItem label="Elixir">

BullMQ is a Node.js library, so the before/after pairs on this page are JavaScript and TypeScript only. If the destination is Elixir, the target-side API is in the [Elixir SDK reference](https://github.com/egeominotti/bunqueue/tree/main/sdk/elixir).

</TabItem>
</Tabs>

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

:::caution[Set a data path]
Without `dataPath` (or the `BUNQUEUE_DATA_PATH` env var), embedded mode keeps jobs in memory only and loses them on restart. Always set one in production.
:::

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](/guide/server/).

## Step by step

```bash
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.

## 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`.
- **`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

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

```typescript
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:

```typescript
// 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',
});
```

## What changes

### Backoff

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

```typescript
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.

### Repeatable jobs

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

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

### 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

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):

```typescript
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](/guide/worker/sandboxed/#worker-vs-sandboxedworker).

### Queue-level rate limiting

The per-worker `limiter` works as in BullMQ. bunqueue also offers a queue-level limit:

```typescript
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.

### No Redis required

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](/guide/databases/) and [when BullMQ is the better
pick](/guide/comparison/#when-to-use-bullmq-instead).

## Migration checklist

- [ ] `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

## Gotchas

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

## Getting help

Open a [GitHub issue](https://github.com/egeominotti/bunqueue/issues) or ask in [Discussions](https://github.com/egeominotti/bunqueue/discussions).

:::tip[Related]

- [bunqueue vs BullMQ](/guide/comparison/), features and benchmarks
- [Queue API](/guide/queue/) and [Worker API](/guide/worker/)
- [FAQ](/faq/), common migration questions
  :::