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.
| Scenario | Rows | Time | Rows / sec |
|---|---|---|---|
PostgreSQL → PostgreSQL
| 1,000,000 | 23.4 s | 42,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.
| Source \ Destination | PostgreSQL | MySQL | MongoDB | SQLite | Redis |
|---|---|---|---|---|---|
| PostgreSQL | 74,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 |
| MySQL | 84,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 |
| MongoDB | 30,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 |
| Redis | 41,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.
| Scenario | Rows | Time | Rows / sec |
|---|---|---|---|
PostgreSQL · upsert
| 1,000,000 | 7.73 s | 129,316 |
MySQL · upsert
| 1,000,000 | 3.24 s | 308,737 |
| SQLite · upsert | 1,000,000 | 783 ms | 1,277,139 |
Redis · upsert
| 1,000,000 | 3.66 s | 273,149 |
Reproducing this
docker compose -f docker-compose.test.yml up -d
pnpm benchmarkThe 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.
