# bunqueue vs BullMQ: Reproducible Benchmarks

Reproducible benchmarks comparing bunqueue and BullMQ. Bulk push is 3.5x faster with 1.8x lower p99 latency, and single push is parity.

Canonical: https://bunqueue.dev/blog/benchmarks-vs-bullmq/

---

import { Aside } from '@astrojs/starlight/components';
import BqIcon from '@components/BqIcon.astro';

<div class="bq-wrap bq-hero">
  <span class="bq-eyebrow">blog · benchmarks</span>
  <h1 class="bq-hero-h1 bq-bench-h1">bunqueue vs BullMQ, <em>measured.</em></h1>
  <p class="bq-hero-sub">Performance claims without numbers are just marketing. These are reproducible benchmarks of bunqueue TCP against BullMQ with Redis on identical workloads, both over real network connections.</p>
</div>

## Test Environment

All numbers below come from one run, measured 2026-07-08 on an Apple M1 Max (32 GB) with Bun 1.3.14, against BullMQ 5.79.3 on Redis 8.8.0. 10,000 iterations, identical 100-byte payloads, and the full script is [`bench/comparison/run.ts`](https://github.com/egeominotti/bunqueue/blob/main/bench/comparison/run.ts) in the repository:

```typescript
// Identical payload for both systems
const PAYLOAD = { data: 'x'.repeat(100) };
```

Both use TCP connections (bunqueue TCP mode, BullMQ via Redis TCP). Embedded mode benchmarks are excluded since BullMQ has no equivalent.

## Results Summary

<div class="bq-cards">
  <div class="bq-card">
    <BqIcon name="spark" />
    <h3>3.5x Faster Bulk</h3>
    <p><b>85,700 vs 24,800 ops/sec</b> - Bulk push (100 jobs per batch)</p>
  </div>
  <div class="bq-card">
    <BqIcon name="speed" />
    <h3>1.8x Lower p99</h3>
    <p><b>6.3ms vs 11.1ms</b> - Bulk push p99 latency</p>
  </div>
  <div class="bq-card">
    <BqIcon name="stack" />
    <h3>Single Push: Parity</h3>
    <p><b>52,756 vs 56,736 ops/sec</b> - BullMQ slightly ahead, effectively a tie</p>
  </div>
  <div class="bq-card">
    <BqIcon name="file" />
    <h3>94% Smaller Install</h3>
    <p><b>5.5 MB vs 93 MB</b>, 7 packages, no Redis server required</p>
  </div>
</div>

## Push Throughput

Single job push measures the raw ingestion speed. Both systems push 10,000 jobs from 32 concurrent producer loops:

```typescript
// Same workload for both systems, 32 concurrent producers
const queue = new Queue('bench', {
  connection: { host: 'localhost', port: 6789 }, // Redis :6379 for BullMQ
});
let issued = 0;
await Promise.all(
  Array.from({ length: 32 }, async () => {
    while (issued++ < 10_000) {
      await queue.add('task', PAYLOAD);
    }
  })
);
```

| Operation   | bunqueue     | BullMQ       | Ratio      |
| ----------- | ------------ | ------------ | ---------- |
| Single push | 52,756 ops/s | 56,736 ops/s | **parity** |

This one is a tie, and in this run BullMQ actually comes out slightly ahead. We are not going to spin that: Redis is extremely fast at single-command operations, and one job per request gives the wire protocol nothing to batch. If your workload is strictly one awaited `add()` at a time, bunqueue does not make it faster.

## Bulk Push

Bulk operations show the biggest difference because bunqueue's msgpack protocol batches efficiently:

```typescript
// bunqueue - native bulk
const jobs = Array.from({ length: 100 }, (_, i) => ({
  name: 'task',
  data: { id: i },
}));
await queue.addBulk(jobs);

// BullMQ - also supports addBulk
await queue.addBulk(jobs);
```

| Scale          | bunqueue     | BullMQ       | Ratio    |
| -------------- | ------------ | ------------ | -------- |
| 100 jobs/batch | 85,700 ops/s | 24,800 ops/s | **3.5x** |

The gap comes from encoding the entire batch in a single msgpack frame, while BullMQ issues multiple Redis commands (even with pipelines). This is the number that matters in practice, because bunqueue's client auto-batches concurrent `add()` calls into bulk commands transparently, so real workloads hit this path without code changes.

## Latency Under Load

Throughput averages hide tail behavior, so the bench also records per-batch p99 latency on the bulk path:

| Metric                   | bunqueue | BullMQ | Ratio          |
| ------------------------ | -------- | ------ | -------------- |
| Bulk push p99 (100 jobs) | 6.3ms    | 11.1ms | **1.8x lower** |

<Aside type="note">
  End-to-end processing throughput depends far more on worker count and worker concurrency than on
  the queue itself, so we do not publish a single headline number for it here. The [benchmarks
  page](/guide/benchmarks/) has the full analysis, including the numbers that do not flatter us.
</Aside>

## Embedded Mode (Bonus)

For single-process applications, embedded mode eliminates the network entirely. Peak throughput on the same machine, measured with [`bench/comprehensive.ts`](https://github.com/egeominotti/bunqueue/blob/main/bench/comprehensive.ts) (median of 3 runs, best scale per operation):

| Operation   | Embedded peak |
| ----------- | ------------- |
| Single push | 241,825 ops/s |
| Bulk push   | 630,568 ops/s |

Bulk push peaks around 630K ops/sec at the 10K-job scale. There is no BullMQ column here because BullMQ has no embedded mode, which is exactly the point.

:::note[Historical in-memory result]
`bench/comprehensive.ts` does not pass a `dataPath`, so this Embedded chart is
an in-memory Queue result, not an SQLite write-through result. The current
[engineering benchmark](/guide/benchmarks/) reports internal in-memory,
public on-disk Embedded, TCP producer, worker drain, durability and Workflow
Engine workloads separately.
:::

## Run the Benchmarks Yourself

All benchmarks are included in the repository:

```bash
git clone https://github.com/egeominotti/bunqueue
cd bunqueue && bun install

bun run start &                  # bunqueue server (TCP mode)
redis-server --daemonize yes     # Redis for the BullMQ half

bun run bench/comparison/run.ts  # the numbers on this page
```

<Aside type="tip">
  The only honest benchmark is one you run on your own hardware. Results vary significantly based on
  CPU, disk speed, and OS configuration. We encourage you to verify these numbers yourself.
</Aside>

## Install Footprint

Throughput is one axis; what you ship is another. We measured `bun add bunqueue` in a clean project as of 2.8.1:

| Metric              | bunqueue | Before 2.8.1 | Change           |
| ------------------- | -------- | ------------ | ---------------- |
| `node_modules` size | 5.5 MB   | 93 MB        | **−94%**         |
| packages installed  | 7        | 117          | **−94%**         |
| cold install time   | ~1.7 s   | ~6 s         | **~3.5× faster** |

The savings came from making `@modelcontextprotocol/sdk` an optional peer dependency (loaded lazily only by the `bunqueue-mcp` bin), dropping `zod` and the HTTP stack from the install path, and removing `bun` from `peerDependencies`. That 2.8.1 measurement included both `croner` and `msgpackr`; current releases use Bun's native cron parser and ship only `msgpackr` as a runtime dependency. The historical footprint above has not been recomputed.

## What the Numbers Mean

bunqueue does not win everywhere, and we would rather say so than publish a chart that hides it. Single push is parity, because Redis is very fast and there is no protocol trick that beats it one command at a time. The wins are concentrated where batching applies:

1. **Bulk encoding** - a 100-job batch travels as one msgpack frame instead of many Redis commands, 3.5x the throughput with 1.8x lower p99
2. **Automatic batching** - the client transparently coalesces concurrent add() and ack calls into bulk commands, so real workloads hit the fast path without code changes
3. **Operational simplicity** - no Redis server to deploy, monitor, back up, and scale

These measurements describe bunqueue's memory/SQLite single-broker path, while
BullMQ can leverage Redis clustering. bunqueue now also offers a separate
PostgreSQL 15–18 multi-broker backend, but this benchmark did not measure it and
must not be quoted as PostgreSQL performance. For applications that fit on one
SQLite server, the measured path retains the lower operational complexity shown
here.