Skip to content

Performance: 0.2.0 vs 0.1.0 ​

On PostgreSQL, 0.2.0 publishes 1.1 to 1.9 times faster than 0.1.0 and dispatches 1.3 to 18 times faster (one cell is slower, see below). On MongoDB it dispatches 2 to 26 times faster and publishes at the same speed. It writes about half the WAL per delivery on PostgreSQL. Every sample was checked for lost, duplicate and out-of-order deliveries: there were none.

Read the numbers as ratios on one laptop, not as capacity planning. Where a cell has a single sample the table says so.

How it was measured ​

AspectWhat was done
Same machineBoth versions ran on one machine in one run (fingerprint 6ba3daf9b9a1), against the same database images.
InterleavedFor every scenario the base (0.1.0) and the candidate (0.2.0) alternate: base first, then candidate, then candidate first, so drift of the machine (heat, other processes) hits both alike.
Fixed windowsEach sample is a warm-up plus a fixed steady-state window (publish 3 s + 15 s, dispatch from a preloaded database, mixed 4 s + 20 s). Nothing waits for a drain, so a slow side cannot hide behind a long run.
Real processesEvery publisher and every dispatcher instance is its own Node process with its own pool and bus. Handlers are no-ops, so the numbers are the cost of the bus and the store, not of your handlers.
Database countersBefore each sample the database statistics are reset; afterwards statements, WAL bytes and records, rows inserted, updated and deleted (PostgreSQL) or operations and oplog bytes (MongoDB) are divided by the events or deliveries actually counted in the database. These move when the code changes, not when the laptop is busy.
ExactnessAfter every dispatch and mixed sample the database is checked: no delivery lost, none handled twice without a lease loss, per-key order held, nothing dead. Publish samples must hold exactly events x handlers deliveries. All samples in this run were valid: 0 lost, 0 duplicate, 0 out of order.
Noise controlA fixed CPU spin is timed before each sample; a sample that ran more than 1.25 times slower than the quietest one is discarded together with its pair when two clean pairs remain.

Dispatcher configurations: defaults are the RelayBus defaults (concurrency 10, batch size 10, poll 1 s, pg pool 10, MongoDB pool 100). mid is concurrency 50, batch 200, pool 20. tuned is concurrency 200, batch 200, pool 40, poll 50 ms. Ni means N dispatcher processes.

The machine ​

PartValue
HostApple M4, 10 cores (4 performance), 16 GiB, macOS (Darwin 24.6.0)
Node.jsv24.3.0
DockerDocker Desktop, 10 CPUs and 8 GiB for the VM, server 28.5.1
PostgreSQL17.11 (postgres:17-alpine): fsync=on, synchronous_commit=on, full_page_writes=on, shared_buffers=1GB, max_wal_size=4GB, wal_level=replica
MongoDB7.0.45 (mongo:7), single-node replica set
Round tripPostgreSQL p50 0.20 ms, p99 0.35 ms; MongoDB p50 0.25 ms, p99 0.66 ms

Docker Desktop runs the databases in a Linux VM that shares the physical cores with the Node workers, and fsync goes to a virtual disk. Absolute numbers are a lower bound for a dedicated database server; the ratios are the useful part. The base is main at d9ca3be (0.1.0), the candidate is eb3fbd7 (the 0.2.0 branch); the run took 38.9 minutes.

How much to trust a number ​

A cell with three samples per side has a 95 % confidence interval and a verdict. A cell with one sample per side has neither: its ratio is indicative, and a difference below roughly 10 % cannot be told from noise. Database counters (WAL, rows written) are exact either way, which is why they, not throughput, gate a change. In the tables, a 3 in the samples column means a confirmed result and a 1 means indicative.

Publish ​

Events per second written by publishing processes, one handler unless noted. P is publishers, batch is events per publishAll call.

Scenario0.1.0 events/s0.2.0 events/sRatioSamples
PostgreSQL, P=1, batch 1 (*)~1,310~1,6501.26x1
PostgreSQL, P=1, batch 10016,74020,1531.20x1
PostgreSQL, P=8, batch 13,7034,6031.24x1
PostgreSQL, P=8, batch 10063,85373,5331.15x3
PostgreSQL, P=32, batch 1 (*)~5,150~6,2001.20x1
PostgreSQL, P=32, batch 10065,52076,1601.16x1
PostgreSQL, 4 processes x 8, batch 10059,21366,9671.13x1
PostgreSQL, P=8, batch 1, 5 handlers2,4082,8931.20x1
PostgreSQL, P=8, batch 100, 5 handlers20,98729,0601.39x3
PostgreSQL, P=8, batch 1, 1,000 distinct keys2,4823,9821.60x1
PostgreSQL, P=8, batch 1, 10 hot keys2,6153,9751.52x1
PostgreSQL, P=8, batch 1, 1 hot key9271,7871.93x1
MongoDB, P=8, batch 11,5531,5991.03x1
MongoDB, P=8, batch 10031,60031,1670.98x3
MongoDB, P=32, batch 10024,46031,3871.28x1
MongoDB, P=8, batch 100, 5 handlers10,26013,6331.33x1
MongoDB, P=8, batch 1, 10 hot keys6689121.37x1

(*) These three cells were slower than 0.1.0 on the first run of the 0.2.0 branch. The values shown were re-measured after the prepared-statement fix described in the publish path, with fewer samples than the other cells. The main 38.9-minute run, which already contained the fix, gave 1,281 to 1,696, 5,052 to 6,440 and 2,056 to 3,716 for the three cells (P=1, P=32 and the dispatch cell below).

In words: single-event publishes on PostgreSQL are about a quarter faster, large batches 15 to 20 % faster, and fan-out to five handlers up to 39 % faster, because 0.2.0 writes fewer indexes and one statement per publish. Publishing to a hot ordering key is nearly twice as fast, since the key's sequence counter is now updated in one statement. MongoDB publish is unchanged within noise.

Publish latency (p99 of one publishing transaction, PostgreSQL, P=8, batch 100): 27.3 ms to 19.8 ms (confirmed, three samples).

Dispatch on PostgreSQL ​

Deliveries completed per second, from a preloaded backlog of unkeyed deliveries unless noted. Handlers do nothing.

Scenario0.1.0 deliveries/s0.2.0 deliveries/sRatioSamples
1 process, defaults (*)~3,100~4,0001.29x1
1 process, mid3,13610,2733.28x1
1 process, tuned9,32914,3481.82x3
4 processes, defaults2,33410,0434.30x1
4 processes, mid2,80532,30611.52x1
4 processes, tuned3,24255,62917.16x1
8 processes, defaults5,47817,3713.17x1
8 processes, mid8,75045,6325.22x1
8 processes, tuned9,73662,9376.46x1
Fan-out to 5 handlers, 1 process, tuned8,22017,8382.17x1
Fan-out to 5 handlers, 4 processes, tuned2,96454,47418.38x1
1,000 distinct keys, 1 process, tuned2,3077,3323.18x1
1,000 distinct keys, 4 processes, tuned~2,690–3,300~5,200–5,3001.61–1.93x2
10 hot keys, 1 process, tuned1,1101,5651.41x1

(*) Re-measured after the prepared-statement fix, with fewer samples than the other cells; the main run gave 2,056 to 3,716.

Why 0.1.0 does not scale with processes (2,334 at four, 3,242 with 200 handlers): every delivery was updated three times, so processes spent their time rewriting the same rows and waiting for each other. 0.2.0 workers lease lanes instead of rows, so adding a process adds throughput until the database is the limit. The single-sample cells above 10x are real in direction, but their exact multiples are noisy; the three-sample cell (1 process, tuned, 1.82x, interval 1.38 to 4.29) is the confirmed one.

Dispatch on MongoDB ​

Scenario0.1.0 deliveries/s0.2.0 deliveries/sRatioSamples
1 process, defaults4859972.06x1
1 process, tuned5606,28111.12x3
4 processes, defaults5053,4516.84x1
4 processes, tuned58715,43726.31x1
Fan-out to 5 handlers, 1 process, tuned5878,59914.66x1
1,000 distinct keys, 1 process, tuned1731,95511.28x1
10 hot keys, 1 process, tuned1773471.96x1

0.1.0 issued one conditional update per claim and one per completion, about three oplog entries per delivery. 0.2.0 claims a whole batch with one write to the lane documents and completes it with one bulk delete.

Mixed load and latency ​

Publishing at a fixed rate while dispatching, measuring commit-to-handler latency (end to end). One sample per side, so indicative.

ScenarioAccepted events/s 0.1.0 to 0.2.0p99 latency 0.1.0p99 latency 0.2.0
PostgreSQL, 1,000/s, defaults1,000 / 1,000991 ms10.5 ms
PostgreSQL, 1,000/s, tuned1,001 / 1,00059.9 ms9.8 ms
PostgreSQL, 5,000/s, tuned2,522 / 4,099114 ms524 ms
MongoDB, 1,000/s, tuned1,000 / 1,00018.1 s389 ms
MongoDB, 5,000/s, tuned1,756 / 1,74121.0 s8.0 s

At 1,000 events per second both versions keep up and 0.2.0 delivers in about a hundredth of the time on defaults. At 5,000 per second on a laptop both versions are past saturation and shed load; on PostgreSQL 0.2.0 accepts 1.6 times more events but its p99 is higher than 0.1.0's at that higher load. See known issues.

What the database does per delivery ​

These counters are exact, not statistical. PostgreSQL dispatch, per delivery:

Counter0.1.00.2.0
deliveries rows updated20
deliveries rows deleted01
lanes rows updatedn/a (no lanes)about 0.05 (one lane row per batch)
event_completions rows insertedn/a1
WAL bytes (1 process, tuned)2,3361,325 (0.57x)
WAL bytes (4 processes, tuned)2,5011,316 (0.53x)
WAL bytes (fan-out 5, 1 process)2,300576 (0.25x)
WAL records (1 process, tuned)-0.38x of 0.1.0
WAL bytes (1 process, defaults)2,2403,129 (1.40x, see below)

Publish, per event: WAL 1,758 to 1,490 bytes (0.85x), 14.2 to 11.2 WAL records, statements 5 to 4.1 for a single-event publish. MongoDB dispatch writes 0.17 to 0.72 times the oplog entries and 0.22 to 0.61 times the oplog bytes.

Why the benchmark gate failed, and why that is expected. The gate flags deliveries.delPerUnit as "regressed from zero" in every dispatch cell, and deletePerUnit on MongoDB: 0.1.0 never deleted a delivery, 0.2.0 deletes each one on completion instead of updating it twice. That is the intended change, not a regression; the counter that matters is the total WAL and write volume, which fell. The one real counter regression is WAL per delivery with defaults on one process (1.40x): the completion inserts an event_completions row whose foreign-key check locks the event row, and the first change to a page after a checkpoint writes the whole page to WAL. It fades as pages are touched again within a checkpoint cycle and did not reduce throughput; see the publish path.

Write amplification and dead tuples ​

In 0.1.0 a delivery row was updated three times (claim, lease renewals, completion). Each update of an indexed column such as status writes a new row version plus an entry in every index, and leaves a dead tuple for autovacuum. Measured on 0.1.0: 0 % HOT updates and a steady stream of dead tuples and dead index entries. In 0.2.0 the same delivery is inserted once and deleted once; claiming and renewing update only one of 256 lane rows per batch, and those updates are 100 % HOT. A delete leaves one dead tuple per delivery instead of three and no dead index entries from updates. The tables are created with autovacuum_vacuum_scale_factor = 0.02; alert on n_dead_tup for sustained thousands of deliveries per second. The full explanation, with pageinspect evidence and diagrams, is in Write amplification in the delivery path.

Where it is not better ​

  • Keyed dispatch on four processes with 1,000 distinct keys: the first full run showed 0.66x, but that came from one unusually fast 0.1.0 sample (7,072/s against 2,690–3,300/s in the re-runs). Two re-runs with 0.1.0 measured alongside put 0.2.0 at 1.61x and 1.93x, so the table shows the re-run range; it is not a regression.
  • MongoDB batch-100 publish: 31,600 to 31,167 events/s (0.98x, three samples): no change, and a higher p99 for the transaction (79 ms to 126 ms, interval inconclusive).
  • Mixed load near saturation: p99 can be higher than 0.1.0's, see above.
  • Single sample cells may move by 10 % or more in either direction on a re-run. The directions are consistent with the exact counters, which is why they are listed.

0.5.0 vs 0.4.1 ​

0.5.0 adds catalog routes, wire names and transactions at an isolation level; without isolationLevel a transaction runs the statements of 0.4.1. The numbers agree: publish and dispatch on PostgreSQL are within noise of 0.4.1, and the statements per event and per delivery are identical. Every sample was valid.

Machine fingerprint 2c551bf14f01 (the setup has changed since the runs above), candidate adadab2 against v0.4.1, interleaved. Publish was measured with 6 samples per side, dispatch with 3. The machine was busy (load average 5 to 28 during the publish run), which is why single pairs move by up to 30 % either way. Throughput is candidate / base, the median of the per-pair ratios:

ScenarioRatioInterval (95 %)Verdict
PostgreSQL publish, P=8, 1 event per call0.9980.85 to 1.05inconclusive
PostgreSQL publish, P=8, 100 per call1.0060.84 to 1.20inconclusive
PostgreSQL publish, 100 per call, 5 handlers0.9210.84 to 1.18inconclusive
PostgreSQL dispatch, 1 instance, tuned1.019-no change
PostgreSQL dispatch, 4 instances, tuned1.014-within noise
PostgreSQL dispatch, 10 hot keys, tuned0.964-within noise

Database counters: publish counters (18 per cell) are all within 3 %, statements per event 1.000x. On the dispatch cells statements per delivery are 0.997x to 1.001x, WAL bytes per delivery 0.98x to 1.02x, and lanes updates per delivery 1.09x (1 instance), 1.08x (4 instances) and 1.00x (hot keys), inside the 15 % that this counter is gated at (it follows the timing of concurrent claims). The dispatch intervals are left out: that run stopped at the MongoDB cells, so its report was not written and the ratios above are computed from its stored samples.

MongoDB was not measured. On this machine the harness could not load the MongoDB template of the 0.4.1 side: a 500-event transaction of the loader ran past the 5 s transaction budget, three times. The candidate side was never reached, so this says nothing about 0.5.0 on MongoDB; the MongoDB store has no change in this release.

Changes to the gate ​

From this release the quick gate generates the same event ids and keys on both sides (a seeded generator per publisher), so the lane layout no longer differs between base and candidate; the dispatch cells measure a fixed amount of work instead of a fixed time; counters of interleaved runs are judged per pair; and lanes updates per delivery gate at 15 % instead of 3 %. Identical code now passes against itself (two runs), and a deliberate regression (one extra lanes update per completion batch) fails it at +97 % (1 instance) and +85 % (4 instances).

Reports ​

The raw samples and environment are in benchmarks/results/runs/:

  • 2026-10-27T10-16-43-144Z: the publish cells, 6 pairs (report written, gate PASS).
  • 2026-10-27T10-05-19-668Z: the PostgreSQL dispatch cells, 3 pairs (stopped at the MongoDB cells, no report).

0.4.0 vs 0.3.1 ​

0.4.0 adds features (a pool for ORM stores, wake-up at the caller's commit, new Kafka options) and leaves the delivery path alone, and the numbers agree: publish and dispatch are within noise of 0.3.1 on PostgreSQL and MongoDB, and the statements per delivery are identical. Every sample was valid (6 of 6 per cell).

Same machine as above (fingerprint f2d136dc9ad0), candidate a3b56bc against v0.3.1, interleaved, 3 samples per side. Throughput is candidate / base:

ScenarioRatioInterval (95 %)Verdict
PostgreSQL publish, P=8, 1 event per call1.0110.96 to 1.05no change
PostgreSQL publish, P=8, 100 per call1.0051.00 to 1.37inconclusive
PostgreSQL publish, 100 per call, 5 handlers1.0330.55 to 1.88inconclusive
MongoDB publish, P=8, 100 per call0.9840.97 to 1.17within noise
PostgreSQL dispatch, 1 instance, tuned1.0621.00 to 1.47inconclusive
PostgreSQL dispatch, 4 instances, tuned1.0480.91 to 1.13inconclusive
PostgreSQL dispatch, 10 hot keys, tuned0.9960.99 to 1.00no change
MongoDB dispatch, 1 instance, tuned1.0090.97 to 1.20inconclusive

Database counters per delivery: one delivery delete and one completion insert, as in 0.3.1. The PostgreSQL dispatch cells show lanes updates per delivery at 0.97x (1 instance) and 0.92x (4 instances) and WAL bytes at 0.95x and 0.88x; hot keys show WAL bytes at 1.04x and WAL records at 1.03x. Publish counters (18 per PostgreSQL cell, 6 per MongoDB cell) are all within 3 %.

About the two counter flags ​

The benchmark gate flagged the 10 hot keys cell for WAL bytes (4.0 %) and WAL records (3.1 %) per delivery, so the run is reported as a gate failure. We do not read that as a regression, for this reason: the gate compares counters of two databases that the harness generates randomly, one preloaded for each side, and each side dispatches its own. To see how large that difference is on identical code, the harness ran v0.3.1 against itself (A/A, 2026-10-25T18-59-07-978Z). That run also failed the gate, with lanes.updPerUnit 9.5 % higher (1.09x) on a side that has no change at all. A separate candidate run of the same two dispatch cells (2026-10-25T18-55-40-858Z) passed: lanes.updPerUnit 0.90x and 18 counters within 3 % on the hot keys cell.

So in the one A/A run the counter gate moved by 9.5 % on identical code, well above its 3 % threshold; that is one run, not a measured noise floor. What this does and does not show: a 3 to 4 % difference in one cell is inside that floor and cannot be told from noise with 3 samples, and the statements per delivery, which do not depend on the preloaded data, are identical. It does not prove that WAL per delivery is exactly the same, only that nothing here separates it from 0.3.1.

Reports ​

The raw reports, samples and environment of the three runs are in benchmarks/results/runs/:

  • 2026-10-25T19-02-48-248Z: the interleaved run of all the cells above (the quick gate set).
  • 2026-10-25T18-59-07-978Z: the A/A run, v0.3.1 against v0.3.1, two dispatch cells.
  • 2026-10-25T18-55-40-858Z: the candidate against v0.3.1 on the same two dispatch cells.

0.3.0 vs 0.2.0 ​

0.3.0 is a code-quality release, and the numbers say it cost nothing: publish and MongoDB are unchanged within noise, most PostgreSQL dispatch cells are within about 10 to 20 % either way (which one sample cannot resolve), and keyed dispatch on PostgreSQL is 1.78 times faster in two cells. Every sample was checked for lost, duplicate and out-of-order deliveries: 0 of each.

Same machine, same method and same database images as above (fingerprint 0c08a14e7a55). Two runs feed the tables:

  • Full run, all cells, 39 minutes, candidate a8587ff against v0.2.0, interleaved. Each cell has 1 sample per side, except where the table says 3. Every sample was valid except the two of PostgreSQL fan-out to 5 handlers on 4 processes (the preloaded backlog drained 0.5 s into the window on both sides), which are left out.
  • Targeted run for the publish and mixed-load cells, candidate 5575bc4, 2 samples per side. The full run was taken before two publish fixes (a13fb7e, 2fb3cde: the transaction budget no longer costs a statement per publish), so these cells were measured again. The 0.2.0 side of this run reused the stored 0.2.0 samples; it was not measured alongside the candidate, so machine drift is not cancelled out.

Publish on PostgreSQL (targeted run, 2 samples) ​

Scenario0.2.0 events/s0.3.0 events/sRatioStatements per event, 0.2.0 / 0.3.0
1 publisher, 1 event per call1,5431,5731.024.07 / 4.07
1 publisher, 100 events per call18,03017,9601.001.04 / 1.04
32 publishers, 1 event per call6,2356,1110.984.02 / 4.02
32 publishers, 100 events per call69,58068,4930.981.03 / 1.03
8 publishers, 1 event per call, 1,000 distinct keys3,7894,1451.094.03 / 4.02

All intervals include 1.0, so none of these is a change.

A second targeted run (2026-10-25T05-12-40-754Z, candidate 13c2a13, 2 samples per side) measured the remaining publish cells after the fixes:

ScenarioRatio 0.3.0 / 0.2.0
PostgreSQL, 8 publishers, 1 event per call1.00
PostgreSQL, 8 publishers, 100 events per call1.05
PostgreSQL, 4 processes x 8 publishers, 100 events per call (ceiling)0.97
PostgreSQL, 8 publishers, 1 event per call, 5 handlers1.07
PostgreSQL, 8 publishers, 100 events per call, 5 handlers1.02
PostgreSQL, 8 publishers, 1 event per call, 10 hot keys0.98
PostgreSQL, 8 publishers, 1 event per call, 1 hot key1.09
MongoDB, 8 publishers, 1 event per call1.02
MongoDB, 8 publishers, 100 events per call0.94
MongoDB, 8 publishers, 100 events per call, 5 handlers0.98
MongoDB, 8 publishers, 1 event per call, 10 hot keys1.06

Every publish cell is now between 0.94 and 1.09 of 0.2.0, with two samples per side, so publishing is unchanged in 0.3.0.

Dispatch on PostgreSQL (full run) ​

Scenario0.2.0 deliveries/s0.3.0 deliveries/sRatioSamples
1 process, defaults4,3734,0870.931 / 1
1 process, mid11,14010,8900.981 / 1
1 process, tuned15,61314,2550.983 / 3
4 processes, defaults15,66113,9630.891 / 1
4 processes, mid34,27840,8031.191 / 1
4 processes, tuned53,84456,8311.061 / 1
8 processes, defaults18,56119,2741.041 / 1
8 processes, mid47,15850,2421.071 / 1
8 processes, tuned59,81452,8650.881 / 1
Fan-out to 5 handlers, 1 process, tuned20,85020,7010.991 / 1
1,000 distinct keys, 1 process, tuned4,2004,1330.981 / 1
1,000 distinct keys, 4 processes, tuned4,4537,9311.781 / 1
10 hot keys, 1 process, tuned2,0221,9550.971 / 1
Routed, 1,000 distinct keys, 5 ms ack, 1 process3,2675,8271.781 / 1
Routed, 10 hot keys, 5 ms ack, 1 process7857871.001 / 1

The two 1.78 results are the ones this release is meant to help: with many keys and routes to one destination, per-key order now holds inside one handler instead of across several, so more keys can proceed at once. They are single samples, so the multiple is indicative; the direction is the same in two different cells. The 0.88 and 0.89 cells (8 processes tuned, 4 processes defaults) are as likely noise as a slowdown; one sample cannot tell, and a repeat run with three samples is the way to find out. Database counters, which do not depend on machine noise: WAL bytes per delivery were 8 to 11 % higher in four cells (1 process tuned with three samples, 4 processes defaults and mid, 8 processes tuned) and 5 to 8 % lower in five others. The benchmark gate flags the higher ones, which is why 0.3.0 is not described as faster per write.

Dispatch on MongoDB (full run) ​

Scenario0.2.0 deliveries/s0.3.0 deliveries/sRatioSamples
1 process, defaults1,1491,1611.011 / 1
1 process, tuned6,4316,2280.983 / 3
4 processes, defaults4,4704,5641.021 / 1
4 processes, tuned21,77122,4521.031 / 1
Fan-out to 5 handlers, 1 process, tuned7,0317,4521.061 / 1
1,000 distinct keys, 1 process, tuned2,0892,1311.021 / 1
10 hot keys, 1 process, tuned3473491.011 / 1

MongoDB is unchanged. MongoDB publish was also within noise (0.94 to 0.98 in five cells, one of them with three samples and an inconclusive interval).

Mixed load ​

ScenarioAccepted events/s 0.2.0 to 0.3.0p99 latency 0.2.0p99 latency 0.3.0Samples
PostgreSQL, 1,000/s, defaults1,000 / 1,0009.9 ms10.4 ms1 / 1
PostgreSQL, 1,000/s, tuned1,000 / 1,0009.0 ms14.4 ms1 / 1
PostgreSQL, 5,000/s, tuned4,133 / 4,032not meaningfulnot meaningful2 / 2
MongoDB, 1,000/s, tuned1,000 / 1,000474 ms377 ms1 / 1
MongoDB, 5,000/s, tuned(saturated)7.5 s8.1 s1 / 1

At 5,000 per second both versions are past saturation and the p99 is dominated by queueing. The same cell gave 79 ms against 10.6 s in the full run and 2.0 s against 104 ms in the targeted run, so its p99 ratios (0.05 to 134) say nothing about the code. Accepted throughput, 0.98, is the number to read. The 14.4 ms against 9.0 ms at 1,000 per second is one sample.

Trust ​

Same rule as above: one sample per side cannot separate a difference below about 10 % from noise, and where a ratio is outside that, the cell is called out. The exact counters (statements per publish, rows written, exactness) are the part that does not depend on the laptop. To check any cell on your hardware, run pnpm bench:full --base v0.2.0 --only <cell> with more samples.

Deep dives ​

Reproduce it ​

sh
pnpm install --frozen-lockfile
pnpm bench:full --base v0.1.0 --candidate v0.2.0 --interleave

Requires Docker and Node 24. Output (report.md, comparison.json, samples.jsonl, env.json) goes to benchmarks/results/runs/<timestamp>/. benchmarks/README.md describes the harness, the cells and how verdicts are decided.

Released under the MIT License.