bkniffler/offline-sync-bench

2

stars

53

commits

TypeScript

primary language

Sep 10, 2026

updated

README

offline-sync-bench

Compare offline-first sync stacks using the same task app. The suite measures local queries, startup, edit delivery, offline recovery, conflicts, client scaling, access changes and attachments, and checks the returned data for correctness.

Includes Syncular JS/Rust, PowerSync, Turso, Zero, Electric, Electric + TanStack DB and experimental Jazz. Results describe each tested application and its guarantees.

Latest results

Latest available measurements · Apple M4 · local services · Syncular JS/Rust 0.17.0. Latency is shown in milliseconds; lower is faster. Browser client sizes use KiB. Latency values are medians; query/edit timings summarize each run’s p50. Starred entries are explained below each table. “Not supported” means the library lacks the native feature required by that test. Benchmark implementation gaps are work to fix, not product limitations.

Collection dates, configurations, sample sizes and ranges are in the linked details. Methods · Missing-case review · Failure explanations

Local task queries

Filter a task list, search titles and count tasks by group across 100,000 already-loaded tasks. Each run measures 25 operations after five warmups.

ClientTask listPrefix searchGrouped counts
Syncular JS0.020 ms0.010 ms3.88 ms
Syncular Rust0.020 ms0.020 ms3.54 ms
PowerSync0.110 ms0.120 ms4.22 ms
Turso0.070 ms0.100 ms13.82 ms
Zero0.110 ms0.140 ms63.87 ms
Electric1.27 ms0.885 ms3.46 ms
Electric + TanStack DB0.450 ms0.650 ms397.03 ms
Jazz v2 (experimental)1429.33 ms1435.88 ms2380.73 ms

A controlled test confirmed that matching SQL indexes remove an avoidable sort. Syncular uses in-memory SQL here; PowerSync and Turso use file-backed stores. Sub-millisecond gaps are small in practice. Index investigation. Electric filters/sorts arrays; TanStack uses indexed native queries; Jazz combines indexed search with JavaScript grouping.

Workload details · Syncular/Turso details · PowerSync details · Zero details · Other client details

Query 100,000 tasks across four projects. Project detail returns the first 100 tasks in one project, with each task’s title, project name and organization name. Organization dashboard summarizes all four projects with total, completed and open task counts, ordered by most open tasks.

ClientProject detailOrganization dashboard
Syncular JS0.020 ms5.20 ms
Syncular Rust0.040 ms4.73 ms
PowerSync0.170 ms5.55 ms
Turso0.200 ms23.58 ms
Zero0.210 ms47.96 ms
Electric<0.005 ms *0.120 ms *
Electric + TanStack DB130.62 ms567.16 ms
Jazz v2 (experimental)7476.44 ms **2684.66 ms **

* One run (n=1); run-to-run variability is unknown. Electric uses application lookup/order indexes over native shapes; dashboard counts are computed per query. “<0.005 ms” is below the saved p50’s 0.01 ms rounding precision; raw operation samples remain available.

** One run (n=1); run-to-run variability is unknown. Jazz uses native relationship includes for detail and materializes rows for JavaScript dashboard aggregation. Their separate costs are not isolated.

Storage matches the local-query case. Zero uses native relationships with JavaScript aggregation; their separate costs have not been measured.

Workload details · Syncular/Turso details · PowerSync details · Zero details · Other client details · Repaired case details

Starting with an empty client

Download data into a fresh client. These results use 100,000 tasks and warm services; details also cover 1,000/10,000 tasks and restarted services.

ClientFirst correct screenComplete local dataset
Syncular JS370.43 ms403.98 ms
Syncular Rust953.75 ms946.63 ms
PowerSync2652.49 ms2844.49 ms
Turso2625.69 ms2828.21 ms
Zero2078.49 ms2089.18 ms
Electric376.61 ms379.25 ms
Electric + TanStack DB7908.41 ms8076.20 ms
Jazz v2 (experimental)Not reached *Not reached *

* Cold 100k startup exceeded 90 seconds; warm startup was never reached.

Screen and complete-dataset milestones are observed independently. Server storage and OS caches are retained. PowerSync compacts fixture history before timing; the earlier setup failures came from missing maintenance in this harness. Explanation. Electric and Zero load memory caches, so their “Complete local dataset” does not establish a persistent offline copy.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Reopening an offline replica

Open an existing 2,000-task store in a new process with the network blocked. Measure when the first screen and all expected rows become available.

ClientFirst correct screenAll rows available
Syncular JS53.14 ms55.48 ms
Syncular Rust46.89 ms52.74 ms
PowerSync197.66 ms202.65 ms
Turso40.95 ms *46.32 ms *
Zero90.87 ms **92.55 ms **
Electric25.29 ms ***26.78 ms ***
Electric + TanStack DB151.00 ms159.19 ms
Jazz v2 (experimental)134.94 ms166.01 ms

* One earlier attempt lost its connection during initial sync; these medians use the two successful runs. The cause remains unknown.

** One run (n=1); run-to-run variability is unknown. Zero reopens its native SQLite store. A two-second preparation wait lets scheduled persistence finish before close; it is outside the timing.

*** One run (n=1); run-to-run variability is unknown. Electric reopens an application-owned SQLite cache.

The OS file cache remains warm.

Workload details · Syncular/Turso details · PowerSync details · Other client details · Repaired case details

Sharing an edit

Make 50 title edits with 200 tasks loaded on independent writer and reader clients. Measure local commit, observed server acceptance and visibility on the reader.

ClientLocal commitServer acceptedReader visible
Syncular JS0.110 ms10.03 ms6.22 ms
Syncular Rust0.150 ms11.67 ms7.31 ms
PowerSync0.270 ms984.92 ms1003.20 ms
Turso0.120 ms24.23 ms25.93 ms
Zero0.360 ms15.14 ms15.65 ms
ElectricNot supported *Not supported *Not supported *
Electric + TanStack DBNot measured **3.00 ms5.24 ms
Jazz v2 (experimental)0.320 ms8.54 ms9.11 ms

* Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

** The adapter deliberately disables localCommit even though the collection exposes the optimistic local update; that is not a durable queue receipt.

These milestones overlap; local commit does not prove crash durability. PowerSync’s default 1,000 ms upload throttle may contribute to its delay, but that cause remains unproven. Investigation.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Syncing edits after an outage

Queue ten writes against 2,000 tasks during a 20-second writer outage. Time queue completion and correct reader data after connectivity returns.

ClientQueue completedReader visible
Syncular JS70.72 ms71.79 ms
Syncular Rust94.52 ms97.18 ms
PowerSync51.13 ms133.38 ms
Turso43.04 ms46.97 ms
Zero134.07 ms129.62 ms
ElectricNot supported *Not supported *
Electric + TanStack DB11111.22 ms11111.98 ms
Jazz v2 (experimental)586.18 ms340.84 ms

* Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

Queue completion and reader visibility have independent observers.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Syncing a larger offline queue

Repeat recovery with 100, 500 and 1,000 queued writes. Each column measures time from reconnection until the reader has the correct data.

Client100 writes500 writes1,000 writes
Syncular JS407.14 ms1562.45 ms2985.25 ms
Syncular Rust394.40 ms1603.45 ms3058.36 ms
PowerSync560.21 ms2263.44 ms4356.07 ms
Turso35.63 ms38.30 ms92.48 ms
Zero577.68 ms3093.88 ms5194.49 ms
ElectricNot supported *Not supported *Not supported *
Electric + TanStack DB23423.11 ms23247.82 ms18323.55 ms
Jazz v2 (experimental)1445.02 ms2561.66 ms5413.21 ms

* Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

The cause of the large Turso/Syncular gap remains unmeasured.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Recovering queued edits after a crash

Queue 1,000 writes, kill the writer process, reopen the same store offline, then reconnect. Verify every pending edit survives and reaches the reader.

ClientReopen offlineQueue completedReader visible
Syncular JS56.35 ms3421.95 ms3368.31 ms
Syncular Rust62.79 ms3364.58 ms3297.35 ms
PowerSync194.33 ms5112.17 ms5167.18 ms
Turso38.06 ms77.64 ms86.86 ms
ZeroNeeds persistent test *Needs persistent test *Needs persistent test *
ElectricNot supported **Not supported **Not supported **
Electric + TanStack DB330.41 ms ***18091.82 ms ***18096.21 ms ***
Jazz v2 (experimental)195.26 ms11780.65 ms10728.40 ms

* Memory storage explains the current skip, but IndexedDB alone does not prove immediate crash durability. Zero limits offline writes by connection state.

** Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

*** One run (n=1); run-to-run variability is unknown. The native executor restores all 1,000 transactions from an application-supplied SQLite storage adapter.

Reopen time starts at process launch; recovery times start at network restoration.

Workload details · Syncular/Turso details · PowerSync details · Other client details · Repaired case details

Two clients editing the same task

A queues an offline edit; B edits the same task online. Reconnect A and verify all three clients agree with the configured conflict policy.

ClientVerified outcomeAll clients agree
Syncular JSB’s edit retained35.67 ms
Syncular RustB’s edit retained46.55 ms
PowerSyncA’s replayed edit retained50.44 ms
TursoA’s replayed edit retained90.93 ms
ZeroA’s replayed edit retained3821.52 ms
ElectricNot supported *Not supported *
Electric + TanStack DBA’s replayed edit retained855.28 ms
Jazz v2 (experimental)B’s edit retained1012.84 ms

* Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

The timings describe different conflict policies. Syncular rejects stale versions; PowerSync and Turso replay A’s title update. TanStack and Zero apply A’s arriving title update; Jazz retains B’s later-written field.

Workload details · Syncular/Turso details · PowerSync details · Other client details

An offline edit racing with deletion

A queues an offline edit; B deletes that task online. Reconnect A and check that the deleted task stays deleted on all three clients.

ClientVerified outcomeAll clients agree
Syncular JSDeletion retained34.35 ms
Syncular RustDeletion retained43.35 ms
PowerSyncDeletion retained164.32 ms
TursoDeletion retained80.04 ms
ZeroDeletion retained3849.39 ms
ElectricNot supported *Not supported *
Electric + TanStack DBDeletion retained823.76 ms
Jazz v2 (experimental)Not established **Did not converge **

* Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

** Writer and other clients disagree after acknowledged writes and a 90-second deadline.

Syncular rejects the stale write; PowerSync and Turso’s SQL UPDATE leaves the missing row deleted. This tests one ordered race, not every conflict interleaving.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Sending one edit to many clients

With 2,000 tasks on each reader, measure one live edit reaching every connected client. The columns show time until the slowest reader is correct.

Client5 readers25 readers
Syncular JS16.43 ms17.26 ms
Syncular Rust16.50 ms28.13 ms
PowerSync723.54 ms595.21 ms
Turso39.08 ms *534.47 ms *
Zero31.46 ms79.28 ms
ElectricNot supported **Not supported **
Electric + TanStack DB16.33 ms32.24 ms
Jazz v2 (experimental)153.85 ms337.17 ms

* One earlier attempt ran out of disk space during 25-reader setup; these medians use the two successful runs.

** Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

PowerSync’s observed ranges overlap at five and 25 readers; the lower 25-reader median does not establish a speedup.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Many clients reconnecting together

Disconnect five or 25 readers, accumulate 100 updates, then restore their connections together. Measure time until every reader has the complete correct dataset.

Client5 readers25 readers
Syncular JS87.17 ms291.64 ms
Syncular Rust78.96 ms281.34 ms
PowerSync36.74 ms153.73 ms
Turso51.55 ms *119.52 ms *
Zero4279.02 ms4331.42 ms
ElectricNot supported **Not supported **
Electric + TanStack DB1926.60 ms3628.66 ms
Jazz v2 (experimental)2145.69 ms6908.60 ms

* One earlier attempt ran out of disk space during 25-reader setup; these medians use the two successful runs.

** Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

Retained storage and the maintenance pause affect the environment.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Removing access to a project

Revoke access to one of two 500-task projects. Measure removal of unauthorized rows while preserving the allowed project, both online and after reconnecting.

ClientOnline removalRemoval after reconnect
Syncular JS14.25 ms10.04 ms
Syncular Rust18.35 ms13.30 ms
PowerSync52.14 ms4693.80 ms
TursoNo equivalent *No equivalent *
Zero67.46 ms5028.48 ms
Electric42.41 ms46.46 ms
Electric + TanStack DB53.77 ms63.64 ms
Jazz v2 (experimental)Purge timed out **Purge timed out **

* The pinned local sync server exposes database replication, not per-actor row revocation while retaining other actors and server data. Lazy page fetching is not authorization filtering.

** The 60,000 ms purge deadline expires: online queries hide revoked rows but local storage retains them; reconnect also leaves queries stale. Fresh-client authorization passes.

Syncular uses explicit synchronization; PowerSync uses continuous synchronization. Local removal cannot erase previously copied data. Electric and TanStack rebuild the application cache; Zero invalidates its native memory cache. These provide different guarantees from persistent native purge.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Uploading and downloading attachments

Transfer two 2 MiB objects linked to tasks. Measure upload, an uncached download and recovery after an interrupted download; verify the complete object hashes.

ClientUploadFresh downloadDownload retry
Syncular JS30.53 ms28.57 ms18.52 ms
Syncular Rust20.08 ms *47.96 ms *38.90 ms *
PowerSync21.06 ms **53.15 ms **10.91 ms **
TursoNot supported ***Not supported ***Not supported ***
ZeroNot supported ***Not supported ***Not supported ***
ElectricNot supported ****Not supported ****Not supported ****
Electric + TanStack DBNot supported ***Not supported ***Not supported ***
Jazz v2 (experimental)201.37 ms *****205.93 ms *****685.48 ms *****

* One run (n=1); run-to-run variability is unknown. Early WebSocket frames are now buffered while the server session opens; upload and interrupted-download checks pass.

** One run (n=1). Uses PowerSync’s experimental native attachment queue and streaming transport with MinIO. Upload excludes file staging; download retry follows an HTTP cut after 64 KiB.

*** This library has no native attachment storage and transfer API. An application-provided uploader is outside this benchmark.

**** Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

***** One run (n=1). Uses Jazz’s native 256 KiB file chunks. Upload includes chunk creation and edge persistence; retry follows a disconnect after the first chunk and reuses the native cache. These boundaries differ from the object-store clients.

Syncular and PowerSync retry object-store downloads; Jazz reads native synced chunks. The footnotes explain the different upload and retry boundaries.

Workload details · Syncular JS details · Syncular Rust details · PowerSync and Jazz details

Uploading and downloading a 500 MB file

Upload one 500,000,000-byte file linked to a task, then download it in a new process with an empty client cache. Upload includes native staging; download ends when complete bytes are materialized. Verify the full SHA-256 hash. One run per client (n=1), using local services; file preparation and final hash validation are outside the clock.

ClientUploadFresh download
Syncular JS3522.82 ms2306.69 ms
Syncular Rust3253.71 ms *3041.53 ms *
PowerSync1175.71 ms **604.40 ms **
TursoNot supported ***Not supported ***
ZeroNot supported ***Not supported ***
ElectricNot supported ***Not supported ***
Electric + TanStack DBNot supported ***Not supported ***
Jazz v2 (experimental)49314.22 ms ****60176.33 ms ****

* Rust uses its published native blob API, which materializes bytes as hex internally. That conversion is included; the harness returns only a hash receipt over stdio.

** PowerSync uses its experimental native attachment queue and filesystem transport.

*** These libraries have no native attachment upload/download feature; Electric is read-only.

**** Jazz uses its default 256 KiB chunks (1,908 parts). Its native helper awaits each part insertion; the other measured clients transfer whole objects through MinIO.

Workload, cached fixture and raw results

Browser client size

Initialize a real browser client and verify local storage across a reload. Count the requested JavaScript, workers and WASM—including embedded WASM—once per file. JavaScript is minified; gzip totals use level 9. These are storage-ready startup sizes. Each cell shows raw/gzip KiB. Core includes SDK code and shared adapters; Storage includes separate engine loaders, workers and WASM. Integrated storage code stays in Core. 1 KiB = 1,024 bytes.

ClientCoreStorageTotal
Syncular JS114.29/34.33 KiB1068.29/460.71 KiB1182.58/495.03 KiB
Syncular RustNot applicable *Not applicable *Not applicable *
PowerSync118.47/36.69 KiB1169.69/535.67 KiB1288.16/572.36 KiB
TursoNot applicable *Not applicable *Not applicable *
Zero293.13/92.75 KiB0.00/0.00 KiB293.13/92.75 KiB
Electric **56.05/17.84 KiB0.00/0.00 KiB56.05/17.84 KiB
Electric + TanStack DB310.42/87.09 KiB1540.92/680.49 KiB1851.34/767.58 KiB
Jazz v2 (experimental) ***165.15/45.17 KiB9619.93/3020.83 KiB9785.07/3066.00 KiB

* Syncular Rust and Turso use native-host clients in this harness. PowerSync uses its Web SDK for this comparison.

** Plain Electric is an in-memory, read-only client and refetches after reload; it is not an equivalent persistent offline client.

*** Jazz’s WASM contains both storage and sync logic; its Storage figure is not a pure database size. TanStack includes native SQLite persistence and an IndexedDB outbox; its worker’s embedded WASM is counted once.

Browser checks and complete asset inventory · Scope and configurations

Run a benchmark

Install Bun and start Docker, then:

bun install --frozen-lockfile
bun run bench:run -- --stack syncular --scenario local-query

The harness resets the selected stack’s benchmark fixtures. Running campaigns and publishing results · Benchmark definitions

Contributors

bkniffler

53 commits

bkniffler/offline-sync-bench

2

stars

53

commits

TypeScript

primary language

Sep 10, 2026

updated

README

offline-sync-bench

Compare offline-first sync stacks using the same task app. The suite measures local queries, startup, edit delivery, offline recovery, conflicts, client scaling, access changes and attachments, and checks the returned data for correctness.

Includes Syncular JS/Rust, PowerSync, Turso, Zero, Electric, Electric + TanStack DB and experimental Jazz. Results describe each tested application and its guarantees.

Latest results

Latest available measurements · Apple M4 · local services · Syncular JS/Rust 0.17.0. Latency is shown in milliseconds; lower is faster. Browser client sizes use KiB. Latency values are medians; query/edit timings summarize each run’s p50. Starred entries are explained below each table. “Not supported” means the library lacks the native feature required by that test. Benchmark implementation gaps are work to fix, not product limitations.

Collection dates, configurations, sample sizes and ranges are in the linked details. Methods · Missing-case review · Failure explanations

Local task queries

Filter a task list, search titles and count tasks by group across 100,000 already-loaded tasks. Each run measures 25 operations after five warmups.

ClientTask listPrefix searchGrouped counts
Syncular JS0.020 ms0.010 ms3.88 ms
Syncular Rust0.020 ms0.020 ms3.54 ms
PowerSync0.110 ms0.120 ms4.22 ms
Turso0.070 ms0.100 ms13.82 ms
Zero0.110 ms0.140 ms63.87 ms
Electric1.27 ms0.885 ms3.46 ms
Electric + TanStack DB0.450 ms0.650 ms397.03 ms
Jazz v2 (experimental)1429.33 ms1435.88 ms2380.73 ms

A controlled test confirmed that matching SQL indexes remove an avoidable sort. Syncular uses in-memory SQL here; PowerSync and Turso use file-backed stores. Sub-millisecond gaps are small in practice. Index investigation. Electric filters/sorts arrays; TanStack uses indexed native queries; Jazz combines indexed search with JavaScript grouping.

Workload details · Syncular/Turso details · PowerSync details · Zero details · Other client details

Query 100,000 tasks across four projects. Project detail returns the first 100 tasks in one project, with each task’s title, project name and organization name. Organization dashboard summarizes all four projects with total, completed and open task counts, ordered by most open tasks.

ClientProject detailOrganization dashboard
Syncular JS0.020 ms5.20 ms
Syncular Rust0.040 ms4.73 ms
PowerSync0.170 ms5.55 ms
Turso0.200 ms23.58 ms
Zero0.210 ms47.96 ms
Electric<0.005 ms *0.120 ms *
Electric + TanStack DB130.62 ms567.16 ms
Jazz v2 (experimental)7476.44 ms **2684.66 ms **

* One run (n=1); run-to-run variability is unknown. Electric uses application lookup/order indexes over native shapes; dashboard counts are computed per query. “<0.005 ms” is below the saved p50’s 0.01 ms rounding precision; raw operation samples remain available.

** One run (n=1); run-to-run variability is unknown. Jazz uses native relationship includes for detail and materializes rows for JavaScript dashboard aggregation. Their separate costs are not isolated.

Storage matches the local-query case. Zero uses native relationships with JavaScript aggregation; their separate costs have not been measured.

Workload details · Syncular/Turso details · PowerSync details · Zero details · Other client details · Repaired case details

Starting with an empty client

Download data into a fresh client. These results use 100,000 tasks and warm services; details also cover 1,000/10,000 tasks and restarted services.

ClientFirst correct screenComplete local dataset
Syncular JS370.43 ms403.98 ms
Syncular Rust953.75 ms946.63 ms
PowerSync2652.49 ms2844.49 ms
Turso2625.69 ms2828.21 ms
Zero2078.49 ms2089.18 ms
Electric376.61 ms379.25 ms
Electric + TanStack DB7908.41 ms8076.20 ms
Jazz v2 (experimental)Not reached *Not reached *

* Cold 100k startup exceeded 90 seconds; warm startup was never reached.

Screen and complete-dataset milestones are observed independently. Server storage and OS caches are retained. PowerSync compacts fixture history before timing; the earlier setup failures came from missing maintenance in this harness. Explanation. Electric and Zero load memory caches, so their “Complete local dataset” does not establish a persistent offline copy.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Reopening an offline replica

Open an existing 2,000-task store in a new process with the network blocked. Measure when the first screen and all expected rows become available.

ClientFirst correct screenAll rows available
Syncular JS53.14 ms55.48 ms
Syncular Rust46.89 ms52.74 ms
PowerSync197.66 ms202.65 ms
Turso40.95 ms *46.32 ms *
Zero90.87 ms **92.55 ms **
Electric25.29 ms ***26.78 ms ***
Electric + TanStack DB151.00 ms159.19 ms
Jazz v2 (experimental)134.94 ms166.01 ms

* One earlier attempt lost its connection during initial sync; these medians use the two successful runs. The cause remains unknown.

** One run (n=1); run-to-run variability is unknown. Zero reopens its native SQLite store. A two-second preparation wait lets scheduled persistence finish before close; it is outside the timing.

*** One run (n=1); run-to-run variability is unknown. Electric reopens an application-owned SQLite cache.

The OS file cache remains warm.

Workload details · Syncular/Turso details · PowerSync details · Other client details · Repaired case details

Sharing an edit

Make 50 title edits with 200 tasks loaded on independent writer and reader clients. Measure local commit, observed server acceptance and visibility on the reader.

ClientLocal commitServer acceptedReader visible
Syncular JS0.110 ms10.03 ms6.22 ms
Syncular Rust0.150 ms11.67 ms7.31 ms
PowerSync0.270 ms984.92 ms1003.20 ms
Turso0.120 ms24.23 ms25.93 ms
Zero0.360 ms15.14 ms15.65 ms
ElectricNot supported *Not supported *Not supported *
Electric + TanStack DBNot measured **3.00 ms5.24 ms
Jazz v2 (experimental)0.320 ms8.54 ms9.11 ms

* Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

** The adapter deliberately disables localCommit even though the collection exposes the optimistic local update; that is not a durable queue receipt.

These milestones overlap; local commit does not prove crash durability. PowerSync’s default 1,000 ms upload throttle may contribute to its delay, but that cause remains unproven. Investigation.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Syncing edits after an outage

Queue ten writes against 2,000 tasks during a 20-second writer outage. Time queue completion and correct reader data after connectivity returns.

ClientQueue completedReader visible
Syncular JS70.72 ms71.79 ms
Syncular Rust94.52 ms97.18 ms
PowerSync51.13 ms133.38 ms
Turso43.04 ms46.97 ms
Zero134.07 ms129.62 ms
ElectricNot supported *Not supported *
Electric + TanStack DB11111.22 ms11111.98 ms
Jazz v2 (experimental)586.18 ms340.84 ms

* Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

Queue completion and reader visibility have independent observers.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Syncing a larger offline queue

Repeat recovery with 100, 500 and 1,000 queued writes. Each column measures time from reconnection until the reader has the correct data.

Client100 writes500 writes1,000 writes
Syncular JS407.14 ms1562.45 ms2985.25 ms
Syncular Rust394.40 ms1603.45 ms3058.36 ms
PowerSync560.21 ms2263.44 ms4356.07 ms
Turso35.63 ms38.30 ms92.48 ms
Zero577.68 ms3093.88 ms5194.49 ms
ElectricNot supported *Not supported *Not supported *
Electric + TanStack DB23423.11 ms23247.82 ms18323.55 ms
Jazz v2 (experimental)1445.02 ms2561.66 ms5413.21 ms

* Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

The cause of the large Turso/Syncular gap remains unmeasured.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Recovering queued edits after a crash

Queue 1,000 writes, kill the writer process, reopen the same store offline, then reconnect. Verify every pending edit survives and reaches the reader.

ClientReopen offlineQueue completedReader visible
Syncular JS56.35 ms3421.95 ms3368.31 ms
Syncular Rust62.79 ms3364.58 ms3297.35 ms
PowerSync194.33 ms5112.17 ms5167.18 ms
Turso38.06 ms77.64 ms86.86 ms
ZeroNeeds persistent test *Needs persistent test *Needs persistent test *
ElectricNot supported **Not supported **Not supported **
Electric + TanStack DB330.41 ms ***18091.82 ms ***18096.21 ms ***
Jazz v2 (experimental)195.26 ms11780.65 ms10728.40 ms

* Memory storage explains the current skip, but IndexedDB alone does not prove immediate crash durability. Zero limits offline writes by connection state.

** Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

*** One run (n=1); run-to-run variability is unknown. The native executor restores all 1,000 transactions from an application-supplied SQLite storage adapter.

Reopen time starts at process launch; recovery times start at network restoration.

Workload details · Syncular/Turso details · PowerSync details · Other client details · Repaired case details

Two clients editing the same task

A queues an offline edit; B edits the same task online. Reconnect A and verify all three clients agree with the configured conflict policy.

ClientVerified outcomeAll clients agree
Syncular JSB’s edit retained35.67 ms
Syncular RustB’s edit retained46.55 ms
PowerSyncA’s replayed edit retained50.44 ms
TursoA’s replayed edit retained90.93 ms
ZeroA’s replayed edit retained3821.52 ms
ElectricNot supported *Not supported *
Electric + TanStack DBA’s replayed edit retained855.28 ms
Jazz v2 (experimental)B’s edit retained1012.84 ms

* Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

The timings describe different conflict policies. Syncular rejects stale versions; PowerSync and Turso replay A’s title update. TanStack and Zero apply A’s arriving title update; Jazz retains B’s later-written field.

Workload details · Syncular/Turso details · PowerSync details · Other client details

An offline edit racing with deletion

A queues an offline edit; B deletes that task online. Reconnect A and check that the deleted task stays deleted on all three clients.

ClientVerified outcomeAll clients agree
Syncular JSDeletion retained34.35 ms
Syncular RustDeletion retained43.35 ms
PowerSyncDeletion retained164.32 ms
TursoDeletion retained80.04 ms
ZeroDeletion retained3849.39 ms
ElectricNot supported *Not supported *
Electric + TanStack DBDeletion retained823.76 ms
Jazz v2 (experimental)Not established **Did not converge **

* Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

** Writer and other clients disagree after acknowledged writes and a 90-second deadline.

Syncular rejects the stale write; PowerSync and Turso’s SQL UPDATE leaves the missing row deleted. This tests one ordered race, not every conflict interleaving.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Sending one edit to many clients

With 2,000 tasks on each reader, measure one live edit reaching every connected client. The columns show time until the slowest reader is correct.

Client5 readers25 readers
Syncular JS16.43 ms17.26 ms
Syncular Rust16.50 ms28.13 ms
PowerSync723.54 ms595.21 ms
Turso39.08 ms *534.47 ms *
Zero31.46 ms79.28 ms
ElectricNot supported **Not supported **
Electric + TanStack DB16.33 ms32.24 ms
Jazz v2 (experimental)153.85 ms337.17 ms

* One earlier attempt ran out of disk space during 25-reader setup; these medians use the two successful runs.

** Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

PowerSync’s observed ranges overlap at five and 25 readers; the lower 25-reader median does not establish a speedup.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Many clients reconnecting together

Disconnect five or 25 readers, accumulate 100 updates, then restore their connections together. Measure time until every reader has the complete correct dataset.

Client5 readers25 readers
Syncular JS87.17 ms291.64 ms
Syncular Rust78.96 ms281.34 ms
PowerSync36.74 ms153.73 ms
Turso51.55 ms *119.52 ms *
Zero4279.02 ms4331.42 ms
ElectricNot supported **Not supported **
Electric + TanStack DB1926.60 ms3628.66 ms
Jazz v2 (experimental)2145.69 ms6908.60 ms

* One earlier attempt ran out of disk space during 25-reader setup; these medians use the two successful runs.

** Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

Retained storage and the maintenance pause affect the environment.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Removing access to a project

Revoke access to one of two 500-task projects. Measure removal of unauthorized rows while preserving the allowed project, both online and after reconnecting.

ClientOnline removalRemoval after reconnect
Syncular JS14.25 ms10.04 ms
Syncular Rust18.35 ms13.30 ms
PowerSync52.14 ms4693.80 ms
TursoNo equivalent *No equivalent *
Zero67.46 ms5028.48 ms
Electric42.41 ms46.46 ms
Electric + TanStack DB53.77 ms63.64 ms
Jazz v2 (experimental)Purge timed out **Purge timed out **

* The pinned local sync server exposes database replication, not per-actor row revocation while retaining other actors and server data. Lazy page fetching is not authorization filtering.

** The 60,000 ms purge deadline expires: online queries hide revoked rows but local storage retains them; reconnect also leaves queries stale. Fresh-client authorization passes.

Syncular uses explicit synchronization; PowerSync uses continuous synchronization. Local removal cannot erase previously copied data. Electric and TanStack rebuild the application cache; Zero invalidates its native memory cache. These provide different guarantees from persistent native purge.

Workload details · Syncular/Turso details · PowerSync details · Other client details

Uploading and downloading attachments

Transfer two 2 MiB objects linked to tasks. Measure upload, an uncached download and recovery after an interrupted download; verify the complete object hashes.

ClientUploadFresh downloadDownload retry
Syncular JS30.53 ms28.57 ms18.52 ms
Syncular Rust20.08 ms *47.96 ms *38.90 ms *
PowerSync21.06 ms **53.15 ms **10.91 ms **
TursoNot supported ***Not supported ***Not supported ***
ZeroNot supported ***Not supported ***Not supported ***
ElectricNot supported ****Not supported ****Not supported ****
Electric + TanStack DBNot supported ***Not supported ***Not supported ***
Jazz v2 (experimental)201.37 ms *****205.93 ms *****685.48 ms *****

* One run (n=1); run-to-run variability is unknown. Early WebSocket frames are now buffered while the server session opens; upload and interrupted-download checks pass.

** One run (n=1). Uses PowerSync’s experimental native attachment queue and streaming transport with MinIO. Upload excludes file staging; download retry follows an HTTP cut after 64 KiB.

*** This library has no native attachment storage and transfer API. An application-provided uploader is outside this benchmark.

**** Electric provides read-path sync only. This benchmark requires client writes; no custom write queue or uploader is added.

***** One run (n=1). Uses Jazz’s native 256 KiB file chunks. Upload includes chunk creation and edge persistence; retry follows a disconnect after the first chunk and reuses the native cache. These boundaries differ from the object-store clients.

Syncular and PowerSync retry object-store downloads; Jazz reads native synced chunks. The footnotes explain the different upload and retry boundaries.

Workload details · Syncular JS details · Syncular Rust details · PowerSync and Jazz details

Uploading and downloading a 500 MB file

Upload one 500,000,000-byte file linked to a task, then download it in a new process with an empty client cache. Upload includes native staging; download ends when complete bytes are materialized. Verify the full SHA-256 hash. One run per client (n=1), using local services; file preparation and final hash validation are outside the clock.

ClientUploadFresh download
Syncular JS3522.82 ms2306.69 ms
Syncular Rust3253.71 ms *3041.53 ms *
PowerSync1175.71 ms **604.40 ms **
TursoNot supported ***Not supported ***
ZeroNot supported ***Not supported ***
ElectricNot supported ***Not supported ***
Electric + TanStack DBNot supported ***Not supported ***
Jazz v2 (experimental)49314.22 ms ****60176.33 ms ****

* Rust uses its published native blob API, which materializes bytes as hex internally. That conversion is included; the harness returns only a hash receipt over stdio.

** PowerSync uses its experimental native attachment queue and filesystem transport.

*** These libraries have no native attachment upload/download feature; Electric is read-only.

**** Jazz uses its default 256 KiB chunks (1,908 parts). Its native helper awaits each part insertion; the other measured clients transfer whole objects through MinIO.

Workload, cached fixture and raw results

Browser client size

Initialize a real browser client and verify local storage across a reload. Count the requested JavaScript, workers and WASM—including embedded WASM—once per file. JavaScript is minified; gzip totals use level 9. These are storage-ready startup sizes. Each cell shows raw/gzip KiB. Core includes SDK code and shared adapters; Storage includes separate engine loaders, workers and WASM. Integrated storage code stays in Core. 1 KiB = 1,024 bytes.

ClientCoreStorageTotal
Syncular JS114.29/34.33 KiB1068.29/460.71 KiB1182.58/495.03 KiB
Syncular RustNot applicable *Not applicable *Not applicable *
PowerSync118.47/36.69 KiB1169.69/535.67 KiB1288.16/572.36 KiB
TursoNot applicable *Not applicable *Not applicable *
Zero293.13/92.75 KiB0.00/0.00 KiB293.13/92.75 KiB
Electric **56.05/17.84 KiB0.00/0.00 KiB56.05/17.84 KiB
Electric + TanStack DB310.42/87.09 KiB1540.92/680.49 KiB1851.34/767.58 KiB
Jazz v2 (experimental) ***165.15/45.17 KiB9619.93/3020.83 KiB9785.07/3066.00 KiB

* Syncular Rust and Turso use native-host clients in this harness. PowerSync uses its Web SDK for this comparison.

** Plain Electric is an in-memory, read-only client and refetches after reload; it is not an equivalent persistent offline client.

*** Jazz’s WASM contains both storage and sync logic; its Storage figure is not a pure database size. TanStack includes native SQLite persistence and an IndexedDB outbox; its worker’s embedded WASM is counted once.

Browser checks and complete asset inventory · Scope and configurations

Run a benchmark

Install Bun and start Docker, then:

bun install --frozen-lockfile
bun run bench:run -- --stack syncular --scenario local-query

The harness resets the selected stack’s benchmark fixtures. Running campaigns and publishing results · Benchmark definitions

Contributors

bkniffler

53 commits

Languages

TypeScript

89.8%

Python

7.6%

JavaScript

1.3%

Rust

1.1%