Performance,
in full.
Creation, edits, synchronization and storage. Explore the complete recorded results for Pulsify, json-joy, Yjs and Loro, with every win and every trade-off in view.
Real workloads. Shared fixtures. Open methodology.
Explore the resultsPulsify · json-joy · Yjs V2 / V1 · Loro
02 / Protocol & connectors35casesCodecs, acknowledged sync & heap diagnostics
03 / Database comparison6engines957,000 checkpointed requests, failures included
Timings and payloads: lower is better. Throughput: higher is better. Unmeasured or interrupted cases are never treated as zero.
01 / CRDT engines
The complete comparison
The same editable document in every engine: metadata, an atomic string, a nested map and a list of editable objects. All 18 primary scenarios, including the less flattering ones.
Apple M4 · bun-1.4.4 · 5 warmups + 25 measured samples · 2026-11-04
Revision 3bfc576 · clean source Source data
| Scenario | PulsifyNative hybrid v3 | json-joy18.28.0 | Yjs V213.6.33 | Loro1.17.1 · Node WASM | Yjs V113.6.33 |
|---|---|---|---|---|---|
| Create document10 editable items | 11.01Bestp95 17.93 | 12.43p95 17.18 | 15.60p95 23.35 | 57.29Worstp95 103.46 | —Not measured |
| Create + encode10 editable items | 13.49p95 17.23 | 12.70Bestp95 20.32 | 20.16p95 24.20 | 103.15Worstp95 147.92 | 16.70p95 24.00 |
| Create document1,000 editable items | 263.02Bestp95 338.15 | 450.12p95 529.10 | 323.90p95 348.65 | 3,073.38Worstp95 3,231.48 | —Not measured |
| Create + encode1,000 editable items | 318.83Bestp95 361.73 | 718.06p95 763.02 | 505.60p95 589.35 | 4,741.62Worstp95 5,072.54 | 543.52p95 608.44 |
| First export1,000 editable items | 76.04Bestp95 137.54 | 293.67p95 342.88 | 210.13p95 312.04 | 1,711.96Worstp95 1,857.42 | 265.58p95 373.46 |
| Repeated export1,000 editable items | 23.87Bestp95 44.21 | 267.42Worstp95 304.33 | 191.25p95 269.08 | 76.79p95 101.58 | 237.58p95 322.54 |
| Read cached field10 editable items | 0.0188p95 0.0275 | 0.0077Bestp95 0.0089 | 0.0131p95 0.0144 | 0.12Worstp95 0.13 | —Not measured |
| Commit scalar edit0 prior edits | 0.29Bestp95 0.34 | 0.39p95 0.45 | 1.17p95 1.43 | 1.34Worstp95 1.49 | —Not measured |
| Commit scalar edit1,000 prior edits | 0.19Bestp95 0.24 | 0.30p95 0.35 | 0.94p95 1.01 | 1.25Worstp95 1.32 | —Not measured |
| Commit scalar edit5,000 prior edits | 0.16Bestp95 0.21 | 0.30p95 0.35 | 0.93p95 1.06 | 1.26Worstp95 1.35 | —Not measured |
| Append editable object10 editable items | 1.24p95 1.82 | 1.06Bestp95 1.35 | 1.59p95 2.02 | 4.31Worstp95 4.60 | —Not measured |
| Edit + encode10 editable items | 1.64Bestp95 2.39 | 2.70p95 3.51 | 4.85p95 5.30 | 8.39Worstp95 10.31 | 2.26p95 2.64 |
| Incremental round trip10 editable items | 3.45Bestp95 4.04 | 4.53p95 5.58 | 7.73p95 8.52 | 15.90Worstp95 17.70 | 3.95p95 4.83 |
| Incremental round trip1,000 editable items | 12.42Bestp95 15.75 | 154.69Worstp95 169.19 | 26.67p95 30.63 | 47.15p95 66.70 | 21.85p95 30.44 |
| Load + materialize10 editable items | 13.35Bestp95 14.83 | 13.93p95 16.02 | 15.55p95 16.88 | 46.24Worstp95 49.07 | 15.97p95 17.18 |
| Load + materialize1,000 editable items | 367.44Bestp95 418.94 | 659.94p95 730.13 | 547.02p95 592.10 | 1,624.77Worstp95 1,791.50 | 635.10p95 709.71 |
| Two-peer offline merge100 offline edits per peer | 146.00p95 203.54 | 90.54p95 110.71 | 78.63p95 109.04 | 239.29Worstp95 273.83 | 62.50Bestp95 81.37 |
| Concurrent map conflict16 concurrent peers | 62.54Bestp95 85.29 | 63.37p95 99.37 | 99.87p95 122.71 | 392.75Worstp95 461.21 | 76.46p95 121.58 |
Medians of per-sample averages; p95 appears below each median. A dash means that variant was not measured. Small differences, especially below 1 µs, can be JIT or scheduling noise.
Transferred payloads
Bytes per measured operationCPU speed and network cost are different trade-offs. Offline merge counts both deltas; the 16-peer conflict counts the updates imported by one receiver. Network framing is excluded.
| Scenario | PulsifyNative hybrid v3 | json-joy18.28.0 | Yjs V213.6.33 | Loro1.17.1 · Node WASM | Yjs V113.6.33 |
|---|---|---|---|---|---|
| Create + encode10 editable items | 284.00Best | 439.00 | 449.00 | 1,030.00Worst | 593.00 |
| Create + encode1,000 editable items | 17,027.00Best | 36,766.00 | 30,012.00 | 49,733.00Worst | 47,879.00 |
| First export1,000 editable items | 17,027.00Best | 36,766.00 | 30,012.00 | 49,733.00Worst | 47,879.00 |
| Repeated export1,000 editable items | 17,027.00Best | 36,766.00 | 30,012.00 | 49,733.00Worst | 47,879.00 |
| Edit + encode10 editable items | 104.00Worst | 58.79 | 33.44 | 86.57 | 23.54Best |
| Incremental round trip10 editable items | 104.00Worst | 63.88 | 32.94 | 86.00 | 22.96Best |
| Incremental round trip1,000 editable items | 104.00Worst | 70.60 | 34.20 | 88.00 | 25.20Best |
| Load + materialize10 editable items | 284.00Best | 439.00 | 449.00 | 1,030.00Worst | 593.00 |
| Load + materialize1,000 editable items | 17,027.00Best | 36,766.00 | 30,012.00 | 49,733.00Worst | 47,879.00 |
| Two-peer offline merge100 offline edits per peer | 176.00 | 139.00 | 97.00 | 1,624.00Worst | 75.00Best |
| Concurrent map conflict16 concurrent peers | 1,728.00Worst | 1,024.00 | 512.00 | 1,536.00 | 336.00Best |
The engines retain different histories. Pulsify retains exact rows at the producer and can send condensed scalar state; json-joy uses structural snapshots and deltas, Yjs uses default GC, and Loro uses history-bearing snapshots and updates.
What does the public API cost?
Same commit and journal pathNative json-joy, Pulsify’s direct setter and Pulsify’s public proxy, measured separately. Local writes do not encode binary data; tests verify identical exported bytes for the two Pulsify paths.
| Scenario | json-joy18.28.0 | Pulsify directCoreModel.set | Pulsify publicPublic proxy |
|---|---|---|---|
| Read cached fieldCached nested field | 0.008p95 0.0089 | 0.0074Bestp95 0.0083 | 0.0216Worstp95 0.0262 |
| Commit scalar edit0 prior edits | 0.31Worstp95 0.35 | 0.24Bestp95 0.36 | 0.26p95 0.33 |
| Commit scalar edit1,000 prior edits | 0.29Worstp95 0.34 | 0.18p95 0.22 | 0.18Bestp95 0.24 |
| Commit scalar edit5,000 prior edits | 0.28Worstp95 0.31 | 0.15Bestp95 0.18 | 0.16p95 0.18 |
Public minus direct includes setter checks and conversions as well as proxy traps. Small or negative differences between runs can reflect measurement noise.
All 95 measurements Iterations, median, p95, min, max, ops/s and payloads
Timing columns show µs/op. The p95 is a percentile of sample averages, not individual-event tail latency. Highlights compare participants in the same scenario and metric.
| Scenario / participant | Iterations | medianµs/op | p95 avg.µs/op | minµs/op | maxµs/op | Ops/s | Bytes/op |
|---|---|---|---|---|---|---|---|
create/10Pulsify | 20 | 11.01 | 17.93 | 7.45 | 18.83 | 90,823 | — |
create/10json-joy | 20 | 12.43 | 17.18 | 9.63 | 20.37 | 80,429 | — |
create/10Yjs V2 | 20 | 15.60 | 23.35 | 10.35 | 26.91 | 64,120 | — |
create/10Loro | 20 | 57.29 | 103.46 | 48.54 | 136.14 | 17,454 | — |
create+encode/10Pulsify | 20 | 13.49 | 17.23 | 10.71 | 25.40 | 74,143 | 284.00 |
create+encode/10json-joy | 20 | 12.70 | 20.32 | 10.80 | 20.46 | 78,766 | 439.00 |
create+encode/10Yjs V2 | 20 | 20.16 | 24.20 | 15.40 | 25.77 | 49,592 | 449.00 |
create+encode/10Loro | 20 | 103.15 | 147.92 | 88.38 | 157.17 | 9,694 | 1,030.00 |
create+encode/10Yjs V1 | 20 | 16.70 | 24.00 | 12.83 | 26.32 | 59,873 | 593.00 |
create/1000Pulsify | 2 | 263.02 | 338.15 | 235.21 | 344.17 | 3,802 | — |
create/1000json-joy | 2 | 450.12 | 529.10 | 432.23 | 606.06 | 2,222 | — |
create/1000Yjs V2 | 2 | 323.90 | 348.65 | 303.83 | 358.04 | 3,087 | — |
create/1000Loro | 2 | 3,073.38 | 3,231.48 | 2,987.17 | 3,518.46 | 325 | — |
create+encode/1000Pulsify | 2 | 318.83 | 361.73 | 292.08 | 392.10 | 3,136 | 17,027.00 |
create+encode/1000json-joy | 2 | 718.06 | 763.02 | 691.73 | 778.02 | 1,393 | 36,766.00 |
create+encode/1000Yjs V2 | 2 | 505.60 | 589.35 | 471.69 | 608.96 | 1,978 | 30,012.00 |
create+encode/1000Loro | 2 | 4,741.62 | 5,072.54 | 4,588.08 | 5,164.54 | 211 | 49,733.00 |
create+encode/1000Yjs V1 | 2 | 543.52 | 608.44 | 516.83 | 608.83 | 1,840 | 47,879.00 |
export-first/1000Pulsify | 1 | 76.04 | 137.54 | 61.58 | 181.00 | 13,151 | 17,027.00 |
export-first/1000json-joy | 1 | 293.67 | 342.88 | 265.21 | 348.38 | 3,405 | 36,766.00 |
export-first/1000Yjs V2 | 1 | 210.13 | 312.04 | 185.83 | 312.58 | 4,759 | 30,012.00 |
export-first/1000Loro | 1 | 1,711.96 | 1,857.42 | 1,658.63 | 1,909.42 | 584 | 49,733.00 |
export-first/1000Yjs V1 | 1 | 265.58 | 373.46 | 229.17 | 390.75 | 3,765 | 47,879.00 |
export-repeat/1000Pulsify | 1 | 23.87 | 44.21 | 17.42 | 47.42 | 41,885 | 17,027.00 |
export-repeat/1000json-joy | 1 | 267.42 | 304.33 | 234.12 | 408.17 | 3,739 | 36,766.00 |
export-repeat/1000Yjs V2 | 1 | 191.25 | 269.08 | 167.17 | 278.21 | 5,229 | 30,012.00 |
export-repeat/1000Loro | 1 | 76.79 | 101.58 | 69.42 | 112.96 | 13,022 | 49,733.00 |
export-repeat/1000Yjs V1 | 1 | 237.58 | 322.54 | 211.58 | 323.67 | 4,209 | 47,879.00 |
read-count/10Pulsify | 10,000 | 0.0188 | 0.0275 | 0.0162 | 0.0514 | 53,074,049 | — |
read-count/10json-joy | 10,000 | 0.0077 | 0.0089 | 0.005 | 0.009 | 129,520,257 | — |
read-count/10Yjs V2 | 10,000 | 0.0131 | 0.0144 | 0.0118 | 0.0146 | 76,603,698 | — |
read-count/10Loro | 10,000 | 0.12 | 0.13 | 0.12 | 0.13 | 8,045,052 | — |
mutate/history-0Pulsify | 200 | 0.29 | 0.34 | 0.25 | 0.38 | 3,501,094 | — |
mutate/history-0json-joy | 200 | 0.39 | 0.45 | 0.35 | 0.50 | 2,564,103 | — |
mutate/history-0Yjs V2 | 200 | 1.17 | 1.43 | 1.06 | 1.55 | 853,333 | — |
mutate/history-0Loro | 200 | 1.34 | 1.49 | 1.26 | 2.53 | 747,896 | — |
mutate/history-1000Pulsify | 200 | 0.19 | 0.24 | 0.13 | 0.25 | 5,351,171 | — |
mutate/history-1000json-joy | 200 | 0.30 | 0.35 | 0.25 | 0.36 | 3,377,865 | — |
mutate/history-1000Yjs V2 | 200 | 0.94 | 1.01 | 0.86 | 1.07 | 1,067,618 | — |
mutate/history-1000Loro | 200 | 1.25 | 1.32 | 1.21 | 1.33 | 802,810 | — |
mutate/history-5000Pulsify | 200 | 0.16 | 0.21 | 0.14 | 0.28 | 6,130,268 | — |
mutate/history-5000json-joy | 200 | 0.30 | 0.35 | 0.25 | 0.42 | 3,337,951 | — |
mutate/history-5000Yjs V2 | 200 | 0.93 | 1.06 | 0.86 | 1.07 | 1,071,191 | — |
mutate/history-5000Loro | 200 | 1.26 | 1.35 | 1.20 | 1.60 | 792,603 | — |
list-append/10Pulsify | 100 | 1.24 | 1.82 | 0.97 | 1.86 | 807,539 | — |
list-append/10json-joy | 100 | 1.06 | 1.35 | 0.94 | 1.48 | 945,626 | — |
list-append/10Yjs V2 | 100 | 1.59 | 2.02 | 1.41 | 2.05 | 628,105 | — |
list-append/10Loro | 100 | 4.31 | 4.60 | 4.18 | 4.90 | 231,951 | — |
edit+encode/10Pulsify | 100 | 1.64 | 2.39 | 1.30 | 2.60 | 610,687 | 104.00 |
edit+encode/10json-joy | 100 | 2.70 | 3.51 | 2.51 | 3.62 | 370,771 | 58.79 |
edit+encode/10Yjs V2 | 100 | 4.85 | 5.30 | 4.49 | 5.64 | 206,274 | 33.44 |
edit+encode/10Loro | 100 | 8.39 | 10.31 | 7.13 | 13.85 | 119,160 | 86.57 |
edit+encode/10Yjs V1 | 100 | 2.26 | 2.64 | 2.04 | 2.78 | 442,233 | 23.54 |
delta-roundtrip/10Pulsify | 50 | 3.45 | 4.04 | 3.09 | 4.15 | 289,926 | 104.00 |
delta-roundtrip/10json-joy | 50 | 4.53 | 5.58 | 4.06 | 5.66 | 220,547 | 63.88 |
delta-roundtrip/10Yjs V2 | 50 | 7.73 | 8.52 | 7.11 | 9.20 | 129,394 | 32.94 |
delta-roundtrip/10Loro | 50 | 15.90 | 17.70 | 14.79 | 24.49 | 62,893 | 86.00 |
delta-roundtrip/10Yjs V1 | 50 | 3.95 | 4.83 | 3.48 | 4.83 | 253,111 | 22.96 |
delta-roundtrip/1000Pulsify | 5 | 12.42 | 15.75 | 10.15 | 16.86 | 80,483 | 104.00 |
delta-roundtrip/1000json-joy | 5 | 154.69 | 169.19 | 145.37 | 175.37 | 6,464 | 70.60 |
delta-roundtrip/1000Yjs V2 | 5 | 26.67 | 30.63 | 23.79 | 31.19 | 37,488 | 34.20 |
delta-roundtrip/1000Loro | 5 | 47.15 | 66.70 | 39.67 | 76.80 | 21,209 | 88.00 |
delta-roundtrip/1000Yjs V1 | 5 | 21.85 | 30.44 | 19.62 | 31.22 | 45,767 | 25.20 |
initial-load+materialize/10Pulsify | 10 | 13.35 | 14.83 | 10.70 | 14.86 | 74,929 | 284.00 |
initial-load+materialize/10json-joy | 10 | 13.93 | 16.02 | 11.29 | 17.32 | 71,813 | 439.00 |
initial-load+materialize/10Yjs V2 | 10 | 15.55 | 16.88 | 14.13 | 18.77 | 64,326 | 449.00 |
initial-load+materialize/10Loro | 10 | 46.24 | 49.07 | 42.11 | 49.29 | 21,626 | 1,030.00 |
initial-load+materialize/10Yjs V1 | 10 | 15.97 | 17.18 | 14.47 | 17.63 | 62,598 | 593.00 |
initial-load+materialize/1000Pulsify | 2 | 367.44 | 418.94 | 348.98 | 430.48 | 2,722 | 17,027.00 |
initial-load+materialize/1000json-joy | 2 | 659.94 | 730.13 | 635.13 | 756.31 | 1,515 | 36,766.00 |
initial-load+materialize/1000Yjs V2 | 2 | 547.02 | 592.10 | 520.25 | 637.27 | 1,828 | 30,012.00 |
initial-load+materialize/1000Loro | 2 | 1,624.77 | 1,791.50 | 1,585.46 | 1,950.35 | 615 | 49,733.00 |
initial-load+materialize/1000Yjs V1 | 2 | 635.10 | 709.71 | 619.10 | 728.35 | 1,575 | 47,879.00 |
offline-merge/100-per-peerPulsify | 1 | 146.00 | 203.54 | 104.08 | 222.37 | 6,849 | 176.00 |
offline-merge/100-per-peerjson-joy | 1 | 90.54 | 110.71 | 67.75 | 112.08 | 11,045 | 139.00 |
offline-merge/100-per-peerYjs V2 | 1 | 78.63 | 109.04 | 62.58 | 112.67 | 12,719 | 97.00 |
offline-merge/100-per-peerLoro | 1 | 239.29 | 273.83 | 217.42 | 318.33 | 4,179 | 1,624.00 |
offline-merge/100-per-peerYjs V1 | 1 | 62.50 | 81.37 | 49.62 | 128.83 | 16,000 | 75.00 |
upstream-map-conflict/16-peersPulsify | 1 | 62.54 | 85.29 | 41.58 | 98.63 | 15,989 | 1,728.00 |
upstream-map-conflict/16-peersjson-joy | 1 | 63.37 | 99.37 | 48.21 | 129.12 | 15,779 | 1,024.00 |
upstream-map-conflict/16-peersYjs V2 | 1 | 99.87 | 122.71 | 75.08 | 125.71 | 10,013 | 512.00 |
upstream-map-conflict/16-peersLoro | 1 | 392.75 | 461.21 | 368.13 | 480.21 | 2,546 | 1,536.00 |
upstream-map-conflict/16-peersYjs V1 | 1 | 76.46 | 121.58 | 65.29 | 127.12 | 13,079 | 336.00 |
proxy-overhead/read-countjson-joy | 10,000 | 0.008 | 0.0089 | 0.0078 | 0.0091 | 125,131,388 | — |
proxy-overhead/read-countPulsify direct | 10,000 | 0.0074 | 0.0083 | 0.0072 | 0.0083 | 135,060,304 | — |
proxy-overhead/read-countPulsify public | 10,000 | 0.0216 | 0.0262 | 0.021 | 0.0272 | 46,198,339 | — |
proxy-overhead/mutate-history-0json-joy | 200 | 0.31 | 0.35 | 0.29 | 0.41 | 3,265,306 | — |
proxy-overhead/mutate-history-0Pulsify direct | 200 | 0.24 | 0.36 | 0.22 | 0.40 | 4,181,214 | — |
proxy-overhead/mutate-history-0Pulsify public | 200 | 0.26 | 0.33 | 0.23 | 0.34 | 3,905,563 | — |
proxy-overhead/mutate-history-1000json-joy | 200 | 0.29 | 0.34 | 0.27 | 0.35 | 3,431,003 | — |
proxy-overhead/mutate-history-1000Pulsify direct | 200 | 0.18 | 0.22 | 0.13 | 0.31 | 5,485,614 | — |
proxy-overhead/mutate-history-1000Pulsify public | 200 | 0.18 | 0.24 | 0.15 | 0.26 | 5,633,803 | — |
proxy-overhead/mutate-history-5000json-joy | 200 | 0.28 | 0.31 | 0.26 | 0.31 | 3,617,160 | — |
proxy-overhead/mutate-history-5000Pulsify direct | 200 | 0.15 | 0.18 | 0.14 | 0.18 | 6,629,761 | — |
proxy-overhead/mutate-history-5000Pulsify public | 200 | 0.16 | 0.18 | 0.14 | 0.19 | 6,434,385 | — |
What each scenario measures Timed work and exclusions
create/10- Create an editable 10-item graph; cleanup is outside timing.
create+encode/10- Create an editable 10-item graph and export a transferable initial state; cleanup is outside timing.
create/1000- Create an editable 1000-item graph; cleanup is outside timing.
create+encode/1000- Create an editable 1000-item graph and export a transferable initial state; cleanup is outside timing.
export-first/1000- Export a prepared 1000-item document for the first time; construction, import verification and cleanup are outside timing.
export-repeat/1000- Export a prepared 1000-item document after one untimed export, without any edit; construction, import verification and cleanup are outside timing.
read-count/10- Read a cached editable nested count; initial container lookup is outside timing.
mutate/history-0- One committed count update through each library's default local API; Pulsify retains its native patch without encoding. Encoding is measured separately in edit+encode.
mutate/history-1000- One committed count update through each library's default local API; Pulsify retains its native patch without encoding. Encoding is measured separately in edit+encode.
mutate/history-5000- One committed count update through each library's default local API; Pulsify retains its native patch without encoding. Encoding is measured separately in edit+encode.
list-append/10- Append a new editable two-field object to the list and commit once per append (adapted from B1.8; objects replace upstream's numbers).
edit+encode/10- Capture a version, commit one count edit, encode its incremental update; verify all encoded packets on a peer outside timing.
delta-roundtrip/10- Version-vector capture + edit + delta encoding + peer import + peer count read; synchronous CPU-only, without transport or acknowledgement.
delta-roundtrip/1000- Version-vector capture + edit + delta encoding + peer import + peer count read; synchronous CPU-only, without transport or acknowledgement.
initial-load+materialize/10- Construct an empty replica, import an initial state and materialize a detached JSON view. Export is prepared outside timing; cleanup is outside timing.
initial-load+materialize/1000- Construct an empty replica, import an initial state and materialize a detached JSON view. Export is prepared outside timing; cleanup is outside timing.
offline-merge/100-per-peer- Two initialized peers make 100 committed offline edits each; time vector capture, two-way delta encode/import and materialization. Edits are outside timing.
upstream-map-conflict/16-peers- Adapted B3.1: 16 peers sharing a baseline concurrently update one map key; time importing all deltas and materializing one peer. Setup/encoding and a reference merge are outside timing.
proxy-overhead/read-count- Same cached nested field, native API versus the Pulsify pipeline bypassing proxies versus the public proxy.
proxy-overhead/mutate-history-0- Native json-joy versus identical Pulsify commit/native-journal pipeline with and without proxy traps; no encoding during local writes, encoded rows are verified identical after export.
proxy-overhead/mutate-history-1000- Native json-joy versus identical Pulsify commit/native-journal pipeline with and without proxy traps; no encoding during local writes, encoded rows are verified identical after export.
proxy-overhead/mutate-history-5000- Native json-joy versus identical Pulsify commit/native-journal pipeline with and without proxy traps; no encoding during local writes, encoded rows are verified identical after export.
02 / Protocol & connectors
Beyond the engine
35 supplementary cases cover frame codecs, operation codecs, local operations, acknowledged synchronization, initial loading and offline reconnects.
Apple M4 · bun-1.4.4 · 5 warmups + 25 measured samples · 2026-11-04
Revision 3bfc576 + working-tree changes Source data
| Scenario | Pulsify codec | JSON + base64 |
|---|---|---|
| Encode0 operation rows | 0.0745Best15 bytes · p95 0.12 | 0.24Worst52 bytes · p95 0.25 |
| Decode0 operation rows | 0.12Best15 bytes · p95 0.38 | 0.37Worst52 bytes · p95 0.50 |
| Encode1 operation rows | 0.18Best173 bytes · p95 0.22 | 0.85Worst348 bytes · p95 1.01 |
| Decode1 operation rows | 0.27Best173 bytes · p95 0.50 | 1.47Worst348 bytes · p95 1.97 |
| Encode100 operation rows | 0.21Best1,056 bytes · p95 0.35 | 1.49Worst1,528 bytes · p95 2.44 |
| Decode100 operation rows | 0.26Best1,056 bytes · p95 0.31 | 1.63Worst1,528 bytes · p95 2.42 |
Outer decoding leaves operation bytes opaque. JSON represents binary leaves as base64 and does not perform Pulsify’s protocol checks. These are task-level wrappers, with different validation work.
| Scenario | Pulsify codec | JSON + base64 |
|---|---|---|
| Encode1 operation rows | 1.06Worst135 bytes · p95 2.08 | 0.76Best206 bytes · p95 0.82 |
| Decode1 operation rows | 1.91Worst135 bytes · p95 2.56 | 1.54Best206 bytes · p95 1.96 |
| Encode100 operation rows | 14.87Worst1,018 bytes · p95 16.95 | 11.04Best3,257 bytes · p95 13.05 |
| Decode100 operation rows | 19.32Best1,018 bytes · p95 21.61 | 28.31Worst3,257 bytes · p95 29.98 |
| Scenario | PulsifyMedian · p95 below |
|---|---|
| update · one-way-0ms | 18.04p95 84.63 |
| update · one-way-5ms | 12,624.77p95 13,806.02 |
| initial-load · 10 | 53.73p95 110.82 |
| initial-load · 1000 | 1,184.21p95 1,970.13 |
| reconnect · 100-offline-edits | 310.92p95 353.96 |
Update cases include acknowledgement and convergence; the 5 ms variant delays each direction. Initial loading confirms public data availability. These are different workloads and are not ranked against each other.
Retained heap diagnostics
Pulsify only · not a cross-engine rankingGC runs before and after preparation. These observations are neither RSS measurements nor proof of leak absence. JS heap cannot fairly rank a JavaScript engine against WASM memory. This suite uses the legacy typed-definition API without the explicit application schema of the primary comparison.
All 35 measurements Iterations, median, p95, min, max, ops/s and payloads
Timing columns show µs/op. The p95 is a percentile of sample averages, not individual-event tail latency. Highlights compare participants in the same scenario and metric.
| Scenario / participant | Iterations | medianµs/op | p95 avg.µs/op | minµs/op | maxµs/op | Ops/s | Bytes/op |
|---|---|---|---|---|---|---|---|
protocol/compile-schemaprotocol | 200 | 17.99 | 24.48 | 16.72 | 26.86 | 55,580 | — |
frame/encode/0codec | 1,000 | 0.0745 | 0.12 | 0.0686 | 0.60 | 13,422,819 | 15.00 |
frame/decode/0codec | 1,000 | 0.12 | 0.38 | 0.0969 | 0.63 | 8,105,370 | 15.00 |
frame/encode/0json | 1,000 | 0.24 | 0.25 | 0.23 | 0.28 | 4,206,099 | 52.00 |
frame/decode/0json | 1,000 | 0.37 | 0.50 | 0.34 | 0.83 | 2,688,172 | 52.00 |
frame/encode/1codec | 1,000 | 0.18 | 0.22 | 0.16 | 0.58 | 5,406,633 | 173.00 |
frame/decode/1codec | 1,000 | 0.27 | 0.50 | 0.24 | 0.52 | 3,742,403 | 173.00 |
frame/encode/1json | 1,000 | 0.85 | 1.01 | 0.81 | 1.09 | 1,171,018 | 348.00 |
frame/decode/1json | 1,000 | 1.47 | 1.97 | 1.41 | 2.16 | 682,419 | 348.00 |
operations/encode/1operations | 500 | 1.06 | 2.08 | 0.93 | 2.18 | 944,287 | 135.00 |
operations/decode/1operations | 500 | 1.91 | 2.56 | 1.55 | 2.89 | 522,876 | 135.00 |
operations/encode/1operations-json | 500 | 0.76 | 0.82 | 0.73 | 1.28 | 1,312,191 | 206.00 |
operations/decode/1operations-json | 500 | 1.54 | 1.96 | 1.47 | 1.96 | 650,265 | 206.00 |
frame/encode/100codec | 200 | 0.21 | 0.35 | 0.18 | 1.71 | 4,790,419 | 1,056.00 |
frame/decode/100codec | 200 | 0.26 | 0.31 | 0.24 | 1.17 | 3,840,025 | 1,056.00 |
frame/encode/100json | 200 | 1.49 | 2.44 | 1.27 | 2.57 | 669,176 | 1,528.00 |
frame/decode/100json | 200 | 1.63 | 2.42 | 1.45 | 2.44 | 614,203 | 1,528.00 |
operations/encode/100operations | 100 | 14.87 | 16.95 | 13.91 | 17.73 | 67,265 | 1,018.00 |
operations/decode/100operations | 100 | 19.32 | 21.61 | 18.18 | 21.61 | 51,764 | 1,018.00 |
operations/encode/100operations-json | 100 | 11.04 | 13.05 | 10.44 | 13.32 | 90,559 | 3,257.00 |
operations/decode/100operations-json | 100 | 28.31 | 29.98 | 26.31 | 30.00 | 35,329 | 3,257.00 |
local/create/10Pulsify | 20 | 6.96 | 25.35 | 5.72 | 31.77 | 143,712 | — |
local/create/10json-joy | 20 | 10.96 | 39.18 | 7.61 | 43.40 | 91,220 | — |
local/create/1000Pulsify | 2 | 360.69 | 951.83 | 247.46 | 1,175.29 | 2,772 | — |
local/create/1000json-joy | 2 | 427.94 | 937.38 | 382.21 | 1,214.00 | 2,337 | — |
local/read-nestedPulsify | 10,000 | 0.0342 | 0.0426 | 0.0337 | 0.0458 | 29,268,264 | — |
local/mutate/history-0Pulsify | 200 | 0.16 | 0.42 | 0.15 | 0.43 | 6,137,982 | — |
local/mutate/history-1000Pulsify | 200 | 0.14 | 0.16 | 0.11 | 0.17 | 7,174,888 | — |
local/mutate/history-5000Pulsify | 200 | 0.0919 | 0.14 | 0.0813 | 0.17 | 10,884,354 | — |
local/mutatejson-joy | 200 | 0.29 | 0.76 | 0.26 | 5.33 | 3,478,261 | — |
sync/update/one-way-0mssync | 10 | 18.04 | 84.63 | 13.84 | 104.39 | 55,440 | — |
sync/update/one-way-5mssync | 2 | 12,624.77 | 13,806.02 | 12,288.06 | 15,715.06 | 79 | — |
sync/initial-load/10sync | 10 | 53.73 | 110.82 | 44.19 | 214.24 | 18,612 | — |
sync/initial-load/1000sync | 2 | 1,184.21 | 1,970.13 | 1,054.48 | 2,242.79 | 844 | — |
sync/reconnect/100-offline-editssync | 1 | 310.92 | 353.96 | 260.25 | 1,746.37 | 3,216 | — |
03 / Durable storage
Every backend. Under load.
Native Rust clients, real Pulsify histories, concurrent reads and durable writes. Explore every checkpointed cell, the recovery probes and the later mixed-workload tuning campaigns.
| Scenario | PostgreSQL16 | SQLite | AlloyDB OmniLocal Omni 17.10.0 | OrioleDBPG 17.11 / beta 19 | pgrust row0.4-beta | pgrust columnar0.4-beta |
|---|---|---|---|---|---|---|
| Write full document1,000 requests | 2,136.58Bestp99 157.31 ms · 0 errors | 191.87Worstp99 4,392.82 ms · 0 errors | 2,001.16p99 161.80 ms · 0 errors | 2,070.77p99 138.77 ms · 0 errors | 2,023.10p99 344.54 ms · 0 errors | 1,480.15p99 183.86 ms · 0 errors |
| Read full history1,000 requests | 1,250.01p99 241.58 ms · 0 errors | 778.62Worstp99 278.91 ms · 0 errors | 1,525.52Bestp99 189.82 ms · 0 errors | 1,488.32p99 182.14 ms · 0 errors | 1,487.93p99 210.42 ms · 0 errors | 979.46p99 274.63 ms · 0 errors |
| Append patch1,000 requests | 474.72p99 309.28 ms · 0 errors | 302.34p99 2,687.09 ms · 0 errors | 240.86Worstp99 611.26 ms · 0 errors | 519.94Bestp99 272.67 ms · 0 errors | 472.49p99 356.61 ms · 0 errors | —Not reached · failed series |
| Lossless compactionAttempts include no-ops | 7,301.02Best1 useful / 999 no-op | 2,680.03Worst1 useful / 999 no-op | 6,922.761 useful / 999 no-op | 5,844.111 useful / 999 no-op | 5,952.911 useful / 999 no-op | —Not reached · failed series |
| Read from version vector1,000 requests | 6,362.02Bestp99 118.81 ms · 0 errors | 1,296.78Worstp99 161.53 ms · 0 errors | 5,525.52p99 98.34 ms · 0 errors | 2,253.13p99 190.59 ms · 0 errors | 6,042.44p99 71.22 ms · 0 errors | —Not reached · failed series |
| Mixed workload45 / 25 / 10 / 10 / 10 mix | 638.80p99 700.51 ms · 0 errors | 459.74p99 1,572.82 ms · 0 errors | 521.27p99 895.44 ms · 0 errors | 407.89Worstp99 1,530.14 ms · 0 errors | 684.13Bestp99 709.08 ms · 0 errors | —Not reached · failed series |
Highlights compare the same workload within one deployment. Throughput counts successful requests. Compaction-attempt throughput includes no-ops; select useful compactions/s to compare actual work. Docker large runs cap each container at 4 GiB with no swap; medium Docker used the VM defaults.
Mixed-workload operation breakdown Same filters and metric as above
Each operation’s rate is its completed count divided by the whole mixed batch duration, not standalone operation capacity.
| Scenario | PostgreSQL16 | SQLite | AlloyDB OmniLocal Omni 17.10.0 | OrioleDBPG 17.11 / beta 19 | pgrust row0.4-beta | pgrust columnar0.4-beta |
|---|---|---|---|---|---|---|
| Write full documentWithin the mixed batch | 63.88p99 84.60 ms · 0 errors | 45.97p99 1,978.22 ms · 0 errors | 52.13p99 58.09 ms · 0 errors | 40.79Worstp99 111.80 ms · 0 errors | 68.41Bestp99 85.93 ms · 0 errors | —Not reached · failed series |
| Read full historyWithin the mixed batch | 63.88p99 75.43 ms · 0 errors | 45.97p99 121.53 ms · 0 errors | 52.13p99 81.21 ms · 0 errors | 40.79Worstp99 109.61 ms · 0 errors | 68.41Bestp99 97.84 ms · 0 errors | —Not reached · failed series |
| Append patchWithin the mixed batch | 287.46p99 412.54 ms · 0 errors | 206.88p99 1,865.58 ms · 0 errors | 234.57p99 559.47 ms · 0 errors | 183.55Worstp99 1,450.70 ms · 0 errors | 307.86Bestp99 393.76 ms · 0 errors | —Not reached · failed series |
| Lossless compactionWithin the mixed batch | 63.885 useful / 95 no-op | 45.9750 useful / 50 no-op | 52.135 useful / 95 no-op | 40.79Worst6 useful / 94 no-op | 68.41Best5 useful / 95 no-op | —Not reached · failed series |
| Read from version vectorWithin the mixed batch | 159.70p99 62.56 ms · 0 errors | 114.93p99 104.03 ms · 0 errors | 130.32p99 54.66 ms · 0 errors | 101.97Worstp99 101.13 ms · 0 errors | 171.03Bestp99 82.42 ms · 0 errors | —Not reached · failed series |
Matrix status & original records All checkpoints, including incomplete series
| Engine | Checkpointed cells | Request errors | Status | Evidence |
|---|---|---|---|---|
| AlloyDB OmniHover for recorded version | 24 / 24 | 0 | Complete | Raw JSON (gzip) |
| OrioleDBHover for recorded version | 24 / 24 | 0 | Complete | Raw JSON (gzip) |
| pgrust columnarHover for recorded version | 9 / 24 | 0 | Incomplete · backend failure | Raw JSON (gzip) |
| pgrust rowHover for recorded version | 24 / 24 | 0 | Complete | Raw JSON (gzip) |
| PostgreSQLHover for recorded version | 24 / 24 | 0 | Complete | Raw JSON (gzip) |
| SQLiteSQLite 3.51.1 | 24 / 24 | 0 | Complete | Raw JSON (gzip) |
Mixed-workload optimization results Before / after, with repeated-run medians
The primary large native comparison uses three baseline and three optimized campaigns with identical fixtures. Docker and medium each use one before/after pair, with less evidence about variance. These are separate from the original engine comparison.
| Scenario | BeforeMedian campaign throughput | OptimizedMedian campaign throughput |
|---|---|---|
| PostgreSQL · 128 clientsNative · spread · burst | 1,240.72Worst | 3,613.88Best+191.27% throughput |
| PostgreSQL · 128 clientsNative · spread · saturation | 1,526.79Worst | 3,740.39Best+144.98% throughput |
| PostgreSQL · 128 clientsNative · hot · burst | 1,258.74Worst | 2,513.95Best+99.72% throughput |
| PostgreSQL · 128 clientsNative · hot · saturation | 1,190.16Worst | 2,244.10Best+88.55% throughput |
| SQLite · 128 clientsNative · spread · burst | 1,125.47Worst | 2,921.90Best+159.61% throughput |
| SQLite · 128 clientsNative · spread · saturation | 911.73Worst | 2,760.49Best+202.78% throughput |
| SQLite · 128 clientsNative · hot · burst | 922.85Worst | 1,815.71Best+96.75% throughput |
| SQLite · 128 clientsNative · hot · saturation | 924.92Worst | 1,879.20Best+103.17% throughput |
Archived sequential storage suite All 81 measurements · PostgreSQL, SQLite & FoundationDB
Historical Docker / Bun suite: 3 warmups + 12 measured samples per case. FoundationDB used a Python JSON/base64 pipe per primitive; its transport costs are included. These scores are separate from the native Rust campaign above.
| Scenario | PostgreSQL16.15 · Bun SQL / TCP | SQLite3.53.4 · bun:sqlite | FoundationDB7.3.77 · Python pipe |
|---|---|---|---|
| Write full document | 4.68 | 2.59Best | 9.20Worst |
| Append patch | 2.16Best | 2.21 | 9.67Worst |
| Read full history | 3.05 | 2.34Best | 601.50Worst |
| load full | 9.18 | 6.43Best | 713.89Worst |
| Read from version vector | 0.78 | 0.70Best | 87.58Worst |
| sync delta | 0.98Best | 1.02 | 73.22Worst |
| Lossless compaction | 127.69 | 18.69Best | 2,086.53Worst |
| Read compacted history | 1.28 | 0.87Best | 30.28Worst |
| Read compacted delta | 1.06 | 0.84Best | 17.49Worst |
Production PostgreSQL adapter measurements Correctness-hardened revision · 3 batch sizes
Bun 1.4.4 / PostgreSQL 16.15 over localhost TCP in Docker. Durable logged writes, 128-byte opaque payloads, 2 warmups and 6 measured runs. The statistic is the fourth sorted sample of six (upper middle), rather than the arithmetic median. Batch sizes are different workloads and are not ranked against one another.
| Rows per append | Append, upper middle | Delta, upper middle | Delta rows returned |
|---|---|---|---|
| 1 | 2.02 | 0.22 | 1 |
| 100 | 6.87 | 0.32 | 10 |
| 1,000 | 24.25 | 0.64 | 10 |
04 / Read the numbers
Measured carefully. Read in context.
One machine, fresh state
Apple M4, 10 cores, ARM64, Bun 1.4.4. The CRDT suite uses 5 warmups and 25 measured samples, fresh replicas and a fixed-seed shuffled participant order. Fixture preparation, schema compilation, GC, verification and cleanup are outside timing.
Sample averages, not event tails
The CRDT median and p95 summarize per-sample average operation times. They are not individual-event latency percentiles. Nanosecond-scale differences can reflect noise; these runs do not establish statistical significance or a universal engine ranking.
The same task, different histories
Every engine gets editable nested containers. Pulsify uses an explicit compiled application schema. Local edits exclude binary encoding; the encoding and synchronization cases add that work explicitly. Loro is warmed Node WASM; its module load and compilation are excluded.
CPU, network and storage are distinct
The CRDT round trip is synchronous and in memory. Connector results include acknowledgement. Database results use finite 1,000-request batches and durable storage; they are not steady-state capacity estimates. Native macOS and Docker Linux are separate deployments.
Reproduce the results
Run on a quiet machine and keep each report. Compare matching runtimes, library versions, hardware, fixtures and iteration counts.
Database setup & commands# Full CRDT comparison: 5 warmups, 25 samples
bun run bench:crdt --output benchmarks/results/new.json \
--markdown benchmarks/results/new.md
# Supplementary protocol and connector suite
bun run bench --output benchmarks/results/protocol-new.json
# Native / Docker database campaign
bun run bench:database
05 / Research archive
The work behind the numbers
Use the recorded-run selectors above to explore 11 CRDT campaigns and 7 supplementary campaigns. The database selector also preserves native FoundationDB results, fresh probes and individual tuning runs. These reports document the separate optimization experiments and their limits.
| Report | What it covers | Original record |
|---|---|---|
| Native hybrid v3 | Current engine, state exchange and remaining limits | Read report |
| Native core | Initial engine rewrite and the preceding baseline | Read report |
| Grouped transport | Exact-history batching and encoding | Read report |
| Compiled preparation | Creation, import and append A/B diagnostics | Read report |
| Scalar commits | General vs specialized scalar commit diagnostics | Read report |
| Offline merge | Matched campaigns and instrumented phase timings | Read report |
| Scalar-run prototype | Historical warm-run experiments before integration | Read report |
| Independent audit | Fairness review and corrective recheck | Read report |
| Deferred encoding | Previous engine · local writes and memory trade-offs | Read report |
| Runtime simplification | Previous engine · copies and traversal costs | Read report |
| Validation removal | Previous engine · additional generic validator | Read report |
| Database incident & recovery | Incomplete matrices, timeouts, OOM and fresh probes | Read report |