Skip to content
Get started
Get started

Bunqueue Performance Benchmarks: 630K Ops/Sec on Bun

benchmarks

Benchmarks measured,
not marketed.

Every figure on this page is the median of 3 runs of a script you can run yourself, on hardware we name, with methodology we publish, including the numbers that don’t flatter us.

630K ops/sec bulk push, embedded90K ops/sec push over TCP3.5x BullMQ bulk throughput5.5 MB install, 2 runtime deps
vs bullmq + redis

Same workload, no Redis.

TCP mode against BullMQ 5.79.3 on Redis 8.8.0, identical payloads, 10,000 iterations, same machine. Single push is parity; batching is where the wire protocol pays off.

Bulk push, 100-job batches ops/sec, higher is better

bunqueue
85,700
BullMQ
24,800

3.5x faster

Bulk push p99 latency ms, lower is better

bunqueue
6.3 ms
BullMQ
11.1 ms

1.8x lower

Single push ops/sec, higher is better

bunqueue
52,756
BullMQ
56,736

parity, batching is the difference, not raw push

measured 2026-07-08 · Apple M1 Max, 32 GB · Bun 1.3.14 · TCP mode · bench/comparison/run.ts

embedded mode

Direct SQLite, no network.

The queue runs inside your process, so a push is a function call plus a WAL write. Ideal for single-process apps, CLIs, edge and serverless.

Push ops/sec by job count

1K
40,424
5K
130,026
10K
184,251
50K
241,825

Bulk push ops/sec by job count

1K
153,206
5K
371,672
10K
630,568
50K
591,254

Process ops/sec by job count

1K
29,856
5K
67,288
10K
82,110
50K
86,893
tcp mode

Over the wire, for distributed systems.

Clients connect to a bunqueue server over TCP with msgpack framing and a 32-connection pool. Required when producers and workers are separate processes.

Push, 100 concurrent adds ops/sec by job count

1K
33,209
5K
56,537
10K
71,242
50K
90,147

Bulk push ops/sec by job count

1K
34,470
5K
61,293
10K
70,739
50K
76,384
embedded vs tcp

What the network costs.

The same operations, embedded versus over TCP. Skipping the per-call round-trip is worth 1.2x to 2.7x on single push and 4.4x to 8.9x on bulk.

Push speedup embedded ÷ TCP, by job count

1K
1.2x
5K
2.3x
10K
2.6x
50K
2.7x

Bulk push speedup embedded ÷ TCP, by job count

1K
4.4x
5K
6.1x
10K
8.9x
50K
7.7x

Peak throughput by mode ops/sec, best scale per operation

Push · embedded
241,825
Push · TCP
90,147
Bulk · embedded
630,568
Bulk · TCP
76,384
install footprint

5.5 MB, seven packages.

Throughput isn’t the only number that shrank. bunqueue ships 2 runtime dependencies, croner and msgpackr. SQLite, S3, HTTP and WebSocket are built into Bun.

Metricbunqueue 2.8.30Before (2.7.x)Change
node_modules size5.5 MB93 MB−94%
packages installed7117117 → 7
cold install time~1.7 s~6 s~3.5× faster
dependency chains

From polling to microseconds.

Jobs with dependencies (dependsOn, flows, parent-child workflows) resolve through event-driven microtask coalescing instead of a 100 ms polling loop.

single dep ~6,900x faster4-level chain ~9,800x fasterfan-out 1→5 ~3,600x faster
ScenarioBefore P50 (polling)After P50 (event-driven)P99 after
Single dep (A→B)100.05 ms14.4 µs114.2 µs
Chain dep (4 levels)300.43 ms30.7 µs67.5 µs
Fan-out (1→5)100.11 ms27.7 µs141.2 µs
Terminal window
bun run src/benchmark/dependency-latency.bench.ts
methodology

How the numbers are made.

Three operations at four scales, 1K, 5K, 10K, 50K jobs, each cell the median of 3 runs. The two modes push differently, and we say so.

Push. Embedded awaits each add() one at a time (true sequential insertion). TCP issues jobs in batches of 100 concurrent add() calls, which the client auto-batches into PUSHB frames, a single awaited add() over TCP is round-trip-bound at ~10K ops/sec:

for (let i = 0; i < scale; i++) {
await queue.add('job', PAYLOAD); // embedded: sequential
}
for (let i = 0; i < scale; i += 100) { // TCP: 100 concurrent
await Promise.all(
Array.from({ length: 100 }, () => queue.add('job', PAYLOAD))
);
}

Bulk push. Batches of 100 through addBulk:

const jobs = Array.from({ length: 100 }, (_, i) => ({
name: 'bulk-job',
data: { ...PAYLOAD, i },
}));
for (let i = 0; i < scale / 100; i++) {
await queue.addBulk(jobs);
}

Process. Push in parallel batches of 500, then drain with a single worker at concurrency: 10, one process, ten in-flight jobs. Over TCP this is bound by per-pull round-trip latency (see the caution above); embedded has no such bound.

for (let i = 0; i < scale; i += 500) {
const promises = [];
for (let j = 0; j < Math.min(500, scale - i); j++) {
promises.push(queue.add('job', PAYLOAD));
}
await Promise.all(promises);
}
while (processed < scale) {
await sleep(5);
}

Apple M1 Max (10 cores) · 32 GB RAM · SSD · macOS 26.5 · Bun 1.3.14 · 16 shards auto-detected · payload 111 bytes · 32-connection pool (TCP) · every cell the median of 3 runs. Single runs vary ±10 to 20% with warmup and background load; the 1K rows in particular reflect cold start.

reproduce

Run it on your hardware.

The only honest benchmark is one you run yourself. The full script is bench/comprehensive.ts.

Terminal window
git clone https://github.com/egeominotti/bunqueue.git
cd bunqueue && bun install
bun run start & # server, for the TCP half
bun run bench/comprehensive.ts # the numbers on this page
redis-server --daemonize yes # optional: BullMQ comparison
bun run bench/comparison/run.ts

Other benchmarks in the repo: src/benchmark/throughput.bench.ts (individual operations), src/benchmark/worker.bench.ts (realistic worker simulation), src/benchmark/stress.bench.ts (production validation), src/benchmark/million-jobs.bench.ts (high-volume integrity).

Share your results, CPU model, RAM, Bun version, OS, in GitHub Discussions.

why it’s fast

No magic, four decisions.

SQLite WAL mode

Concurrent reads and writes with memory-mapped I/O through Bun’s native SQLite bindings, no serialization layer between the queue and its store.

Auto sharding

Shard count auto-detected from CPU cores (power of 2, max 64). Queues hash to shards, so hot queues don’t contend on one lock.

TCP pipelining

Multiple commands in flight per connection with request-ID response matching, msgpack framing, and transparent client-side batching into PUSHB.

Efficient structures

4-ary min-heap for O(log₄ n) priority queues, skip lists for temporal indexing, LRU caches with hard memory bounds.

Verify it in a minute.

Install, push 10,000 jobs, watch them drain.