Bunqueue Performance Benchmarks: 630K Ops/Sec on Bun
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.
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
3.5x faster
Bulk push p99 latency ms, lower is better
1.8x lower
Single push ops/sec, higher is better
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
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
Bulk push ops/sec by job count
Process ops/sec by job count
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
Bulk push ops/sec by job count
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
Bulk push speedup embedded ÷ TCP, by job count
Peak throughput by mode ops/sec, best scale per operation
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.
| Metric | bunqueue 2.8.30 | Before (2.7.x) | Change |
|---|---|---|---|
node_modules size | 5.5 MB | 93 MB | −94% |
| packages installed | 7 | 117 | 117 → 7 |
| cold install time | ~1.7 s | ~6 s | ~3.5× faster |
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.
| Scenario | Before P50 (polling) | After P50 (event-driven) | P99 after |
|---|---|---|---|
| Single dep (A→B) | 100.05 ms | 14.4 µs | 114.2 µs |
| Chain dep (4 levels) | 300.43 ms | 30.7 µs | 67.5 µs |
| Fan-out (1→5) | 100.11 ms | 27.7 µs | 141.2 µs |
bun run src/benchmark/dependency-latency.bench.tsHow 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);}Environment
Section titled “Environment”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.
Run it on your hardware.
The only honest benchmark is one you run yourself. The full script is bench/comprehensive.ts.
git clone https://github.com/egeominotti/bunqueue.gitcd bunqueue && bun install
bun run start & # server, for the TCP halfbun run bench/comprehensive.ts # the numbers on this page
redis-server --daemonize yes # optional: BullMQ comparisonbun run bench/comparison/run.tsOther 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.
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.