Benchmarks measured,
not mixed.
Every number names the operation, persistence boundary, topology, scale, sample count and integrity check. A public on-disk add is not an internal in-memory batch, and producer ingestion is not worker drain.
Read the label before the number
Section titled “Read the label before the number”The current campaign ran natively on an AMD Ryzen 9 9950X3D
(16 cores / 32 threads), 59 GiB RAM, Linux 7.0.0-14, CPU governor
performance, Bun 1.3.14, revision
af027f04d2064b701ee2243eac737d74b8d87706. Docker was used only for the
functional sandbox and was stopped before performance measurement.
These labels are not interchangeable:
| Label | What it means |
|---|---|
| Internal in-memory | Direct QueueManager batch API, no dataPath, no SQLite write |
| Embedded on-disk | Public Queue API and a fresh SQLite dataPath |
| TCP on-disk | Public client over localhost MessagePack to a broker with its own SQLite file |
| Buffered | Normal write-behind persistence, with the documented ≤10 ms hard-crash window |
| Durable | durable:true; the timed call waits for synchronous persistence |
| Workflow | A complete multi-node execution, not one queue operation |
Queue engine
Section titled “Queue engine”Representative throughput
Section titled “Representative throughput”All rows below are medians. They describe different workloads and should not be divided into an “Embedded is N× TCP” claim.
| Operation | Samples | Median | Variability / tail | Integrity |
|---|---|---|---|---|
| Internal in-memory batched push, 1M jobs | 21 | 729,395 jobs/s | 693,001–747,943; CV 1.94% | 21M/21M IDs and payloads |
| Internal in-memory batched process, 1M jobs | 21 | 541,712 jobs/s | 523,286–558,036; CV 1.40% | pull + completed event |
| Internal in-memory full lifecycle, 1M jobs | 21 | 311,915 jobs/s | 303,674–316,556; CV 1.21% | clean final state |
| TCP pipelined individual push | 21 | 80,978 ops/s | 78,331–83,806; CV 2.06% | fresh broker/database |
TCP PUSHB, 50K jobs | 21 | 158,779 jobs/s | 146,496–163,501; CV 3.25% | fresh broker/database |
| TCP no-work worker drain, concurrency 50 | 21 | 17,256 jobs/s | p05 16,455; p95 17,864; CV 2.86% | 420K/420K exactly once |
The 1M-job runner uses 16 queues and workers, removes completed jobs, and checks the complete ID set both when pulled and on the completed event. It is the engine’s maximum-throughput path, not the public Queue API.
Public API with on-disk SQLite
Section titled “Public API with on-disk SQLite”bench:pushbulk ran eight fresh campaigns; the first was discarded. Each cell
is already a median of three repetitions. This is the median of seven campaign
medians for the final 50K cell:
| Mode | add() | addBulk() |
|---|---|---|
| Embedded, on-disk | 147,818 jobs/s | 186,384 jobs/s |
| TCP, on-disk | 127,476 jobs/s | 87,319 jobs/s |
This runner grows one database through 1K, 5K, 10K and 50K cells. The 50K row
is deliberately a sustained/grown-database result. The isolated 159K TCP
PUSHB row above is the clean-database bulk result.
Latency and synchronous durability
Section titled “Latency and synchronous durability”After the TCP throughput campaign, 5,000 sequential localhost adds measured 14 µs p50 / 292 µs p99.
For durable:true, every fresh process timed 2,000 sequential operations after
50 warm-ups:
| Mode | Runs | Throughput median | p05 / p95 | Latency p50 / p95 / p99 |
|---|---|---|---|---|
| Embedded | 21 | 60,835 ops/s | 53,698 / 63,002 | 15 / 25 / 59 µs |
| TCP | 21 | 27,191 ops/s | 21,590 / 29,089 | 30 / 57 / 120 µs |
All 43,050 operations per mode, including warm-ups, were present in queue counts.
Worker concurrency
Section titled “Worker concurrency”A single TCP worker drained 10K preloaded jobs from a fresh broker/database in each sample:
| Concurrency | Median drain |
|---|---|
| 32 | 17,607 jobs/s |
| 48 | 17,541 jobs/s |
| 64 | 17,191 jobs/s |
| 96 | 15,830 jobs/s |
| 128 | 15,094 jobs/s |
| 192 | 14,081 jobs/s |
The useful knee for this localhost no-work processor is 32–48. More concurrency is slower. Producer ingestion and worker drain are separate capacity limits.
Workflow Engine
Section titled “Workflow Engine”bun run bench:workflow uses the public Engine facade and four graphs:
| Scenario | Graph | Required terminal state |
|---|---|---|
| Linear | validate → transform → persist | completed; 3 steps |
| Parallel | prepare → 3 inline parallel steps → join | completed; 5 steps |
| Compensation | reserve → charge → intentional failure | failed; rollback completed; 2 reversals |
| Signal | request → wait for approval → finish | waited and signalled exactly once; completed |
Every sample has a new process, queue, workflow SQLite file, and name. TCP adds a new broker process, broker database, and dynamic ports. The final tuning sweep selected Embedded concurrency 128 and TCP concurrency 64.
Single-engine saturation
Section titled “Single-engine saturation”| Mode / scenario | Runs × executions | Median | p05 / p95 | CV | Run-median p95 latency |
|---|---|---|---|---|---|
| Embedded linear | 21 × 1,000 | 2,700 wf/s | 2,570 / 2,792 | 2.86% | 342.8 ms |
| Embedded parallel | 7 × 500 | 2,118 wf/s | 1,997 / 2,173 | 2.70% | 219.8 ms |
| Embedded compensation | 7 × 500 | 2,055 wf/s | 2,005 / 2,118 | 2.05% | 225.6 ms |
| Embedded signal | 7 × 500 | 1,928 wf/s | 1,883 / 1,961 | 1.39% | 84.7 ms resume |
| TCP linear | 21 × 1,000 | 3,187 wf/s | 3,075 / 3,261 | 1.77% | 284.3 ms |
| TCP parallel | 7 × 500 | 2,456 wf/s | 2,368 / 2,528 | 2.48% | 186.4 ms |
| TCP compensation | 7 × 500 | 2,239 wf/s | 2,218 / 2,384 | 2.87% | 200.9 ms |
| TCP signal | 7 × 500 | 2,234 wf/s | 2,204 / 2,306 | 1.72% | 64.9 ms resume |
These are saturated-batch workflow:started-event-to-terminal latencies,
including time behind other executions after the lifecycle event begins.
Throughput starts before the first Engine.start() call. Neither metric is
idle single-workflow service latency.
TCP can be faster here because the broker and Workflow Store run in separate processes with separate SQLite files. Embedded uses two connections to the same file. The result does not say that network transport is cheaper.
Signal phase medians:
| Mode | Park | Resume | Park p95 | Resume p95 |
|---|---|---|---|---|
| Embedded | 3,205 wf/s | 4,837 wf/s | 139.1 ms | 84.7 ms |
| TCP | 3,518 wf/s | 6,149 wf/s | 123.0 ms | 64.9 ms |
Across both modes the measured single-engine campaign reconciled 63,000 executions, 189,000 step completions, 56,000 successful workflows, 7,000 intentional failures and 14,000 compensation outcomes.
The default TCP safety cap
Section titled “The default TCP safety cap”The broker protects each protocol client with a default 10,000-request, 60-second sliding window. A workflow expands into multiple queue commands:
| Linear executions | Default result |
|---|---|
| 3,000 | 4,005 wf/s; 0.749 s |
| 3,500 | 58 wf/s; 60.159 s; p95 60.088 s |
At the plateau, persisted state showed 896 workflows waiting for their last
node, 896 broker jobs waiting and 34 active. At window turnover, ACK batches
reported Rate limit exceeded, then every workflow completed. Lowering TCP
frame timeout, command timeout or worker cleanup to 5 seconds did not move the
plateau. Raising only RATE_LIMIT_MAX_REQUESTS=1000000 restored 3,500
executions to a 3,855 wf/s median across three fresh runs.
That is why default and tuned results are separate. In production, size the protocol limit from expected command expansion and monitor rate-limit hits.
Horizontal tuned capacity
Section titled “Horizontal tuned capacity”The scale runner launched independent engines behind a sub-millisecond common
barrier: 5,000 executions per instance, one discarded warm-up and three
measured campaigns. TCP used RATE_LIMIT_MAX_REQUESTS=1000000.
| Mode / instances | Median | Speedup | Efficiency | Peak sampled CPU / RSS |
|---|---|---|---|---|
| Embedded ×1 | 3,194 wf/s | 1.00× | 100% | 77% / 233 MiB |
| Embedded ×4 | 10,579 wf/s | 3.31× | 82.8% | 317% / 894 MiB |
| Embedded ×8 | 19,379 wf/s | 6.07× | 75.8% | 652% / 1,745 MiB |
| Embedded ×12 | 25,873 wf/s | 8.10× | 67.5% | 1,028% / 2,549 MiB |
| TCP ×1 | 4,207 wf/s | 1.00× | 100% | 156% / 352 MiB |
| TCP ×4 | 11,738 wf/s | 2.79× | 69.8% | 671% / 1,371 MiB |
| TCP ×8 | 17,407 wf/s | 4.14× | 51.7% | 1,497% / 2,626 MiB |
| TCP ×12 | 17,496 wf/s | 4.16× | 34.7% | 2,415% / 3,861 MiB |
TCP is effectively saturated at ×8 on this shared host. The runner checked 750,000 measured plus 250,000 warm-up workflow executions; every child passed the same integrity scan.
Maximum host scale for queue jobs
Section titled “Maximum host scale for queue jobs”The separate queue-engine scale test used much larger internal workloads:
| Topology | Median aggregate | Correctness | Peak resources |
|---|---|---|---|
| 12 Embedded instances × 1M jobs | 2,008,704 jobs/s lifecycle | 36M / 36M | ~2,513% CPU; 37.4 GiB RSS; no swap |
| 12 TCP broker/client pairs × 50K | 46,937 jobs/s drain | 1.8M / 1.8M | ~2,249% CPU; 5.2 GiB RSS |
The Embedded integrity sets intentionally retain 12 million IDs at once and dominate RSS. TCP gained about 2.7× over one pair, not 12×, because every pair shares one scheduler, loopback stack and storage device.
Reproduce
Section titled “Reproduce”git clone https://github.com/egeominotti/bunqueue.gitcd bunqueuebun install
# Core queue runnersbun run src/benchmark/million-jobs.bench.tsBENCH_N=50000 BENCH_RUNS=21 bun run bench:tcp
# The published push/bulk Embedded row is on-disk. Use a fresh path and point# BENCH_HOST/BENCH_PORT at a separately started fresh SQLite broker.BUNQUEUE_DATA_PATH=/tmp/bunqueue-bench-embedded.db \BENCH_HOST=127.0.0.1 BENCH_PORT=16794 \bun run bench:pushbulk
# Workflow Engine, both modes and all scenariosBENCH_OUTPUT=/tmp/workflow.json bun run bench:workflow
# Tuned host scaleRATE_LIMIT_MAX_REQUESTS=1000000 \BENCH_OUTPUT=/tmp/workflow-scale.json \bun run bench:workflow:scaleRun without BUNQUEUE_EMBEDDED=1; that test variable forces clients into
Embedded mode and would invalidate a TCP label.
The bare bun run bench:pushbulk command does not configure an Embedded
dataPath; its Embedded half is in-memory. It must not be used to reproduce an
on-disk row unless BUNQUEUE_DATA_PATH points to a fresh SQLite file. The TCP
half likewise requires a fresh broker/database at the explicitly reported
endpoint.
The runners and report are in the repository:
bench/workflow-engine.tsbench/workflow-engine/scale.tsdocs/benchmarks/native-engineering-2026-07-30.mddocs/benchmarks/native-engineering-2026-08-02.mddocs/features/benchmarks.md
Functional evidence
Section titled “Functional evidence”Before native measurement, the isolated sandbox passed 6,250 unit/model tests, 430 TCP integration tests and 273 Embedded integration tests, with zero failures. A mixed-suite memory-growth signal was followed by three focused TCP chaos soaks: 90K jobs, 81 worker kill/reconnect cycles, flat post-cold p99, bounded 4.1–4.3 MB WAL and post-GC collection bounds all passed.
Historical BullMQ comparison
Section titled “Historical BullMQ comparison”The July 8 Apple M1 Max comparison remains available as a dated, separate
campaign: bunqueue TCP bulk push 85,700 jobs/s versus BullMQ 24,800, while
individual concurrent push was 52,756 versus 56,736. It used
bench/comparison/run.ts, BullMQ 5.79.3 and Redis 8.8.0. Do not combine those
values with the Ryzen campaign above.
Read bunqueue vs BullMQ or the original benchmark article.