Syncle

Benchmarks

Every number here comes from a recorded run of the benchmark suite against real databases. The results are committed as benchmarks/results.json and this page only renders that file, so every figure can be traced back to a run and reproduced.

Measured on a single local machine: Syncle, both databases and Redis all run on the same host, in containers, with no network between them. Real deployments put a network in that path, and against a managed or remote database the round-trip latency is usually what sets the pace rather than the database itself, so expect lower absolute numbers there. Use these runs to compare code paths against each other; they will not predict your production ceiling. Every figure here was measured, not estimated.

The configuration

The shipped defaults, not tuning done for the benchmark. These are read from the running configuration, so they are the ones the run actually used.

Rows per delivery
100,000 (SYNCLE_CDC_BATCH_SIZE)
Batch byte ceiling
64 MB (SYNCLE_CDC_BATCH_BYTES)
Partial-batch linger
50 ms (SYNCLE_CDC_LINGER_MS)
Durable spool
off — the default; one scenario below enables it
Write mode
idempotent upsert, keyed on the primary key

The machine

Syncle, the databases and Redis all ran on this one machine.

Platform
darwin 25.5.0 (arm64)
CPU
Apple M4
Cores
10
Memory
16 GB
Node
v22.23.1
PostgreSQL
16.15
MySQL
8.0.46
Redis
7.4.11
Databases
containerised, same machine (docker-compose.test.yml)
Sharing the machine
none — the test stack had the machine to itself

With the durable spool

The same sync, with SYNCLE_CDC_SPOOL=on. Changes go to a Redis stream first and the source is acknowledged as soon as they are durably spooled, so a slow destination cannot hold the source’s log open. It costs throughput; that is the trade it exists to make.

ScenarioRowsTimeRows / sec
PostgreSQL → PostgreSQL
  • verified: 1000000 rows, exactly once
  • syncle cpu (peak / avg): 238% / 49%
  • syncle memory (peak): 451 MB
  • pg cpu / memory (peak): 148% / 1421 MB
  • peak spool depth: 75246
1,000,00023.4 s42,731

Change data capture, end to end

A row is committed on the source, picked up from that database’s own change log (Postgres logical replication, MySQL binlog) and written to the destination. Timing starts when the writes begin and stops when the last row has landed, so it includes reading the log, delivery and the destination write. Every run is verified complete and duplicate-free before its time is recorded.

Rows per second, source down the side, destination across the top. Every run was verified complete and duplicate-free before its time was recorded.
Source \ DestinationPostgreSQLMySQLMongoDBSQLiteRedis
PostgreSQL74,968
1,000,000 rows
102,828
1,000,000 rows
27,969
1,000,000 rows
133,743
1,000,000 rows
112,638
1,000,000 rows
MySQL84,674
1,000,000 rows
98,087
1,000,000 rows
28,000
1,000,000 rows
125,881
1,000,000 rows
88,238
1,000,000 rows
MongoDB30,683
1,000,000 rows
34,488
1,000,000 rows
19,505
1,000,000 rows
40,960
1,000,000 rows
36,249
1,000,000 rows
Redis41,698
1,000,000 rows
47,925
1,000,000 rows
21,110
1,000,000 rows
49,958
1,000,000 rows
40,858
1,000,000 rows

Destination write ceiling

Rows written straight into a destination table, with no change stream involved. This is the fastest the destination can accept them, and the bar the sync figures above are measured against.

ScenarioRowsTimeRows / sec
PostgreSQL · upsert
  • syncle cpu (peak / avg): 88% / 8%
  • syncle memory (peak): 134 MB
  • pg cpu / memory (peak): 98% / 1227 MB
1,000,0007.73 s129,316
MySQL · upsert
  • syncle cpu (peak / avg): 108% / 24%
  • syncle memory (peak): 174 MB
1,000,0003.24 s308,737
SQLite · upsert1,000,000783 ms1,277,139
Redis · upsert
  • syncle cpu (peak / avg): 273% / 90%
  • syncle memory (peak): 542 MB
1,000,0003.66 s273,149

Reproducing this

docker compose -f docker-compose.test.yml up -d
pnpm benchmark

The run resets replication slots, fixtures and the metadata store first, because leftovers from a previous run distort everything after them. Each run is checked for completeness and duplicates before its time is recorded, so a fast number that moved the wrong data is not published as a result.

Recorded 2026-09-08.