Logical replication benchmark results
Raw data behind The Hidden CPU Cost of PostgreSQL Logical
Replication. The harness which produced
these numbers is
run-benchmark.sh; the
workload it runs is
logical-replication-insert.sql.
Reproducing
The harness creates two disposable PostgreSQL clusters under /private/tmp, uses
localhost ports 56432 and 56433, and removes the clusters when it exits. It
defaults to the PostgreSQL 18 binaries from Postgres.app. Override PG_BIN,
PUB_PORT or SUB_PORT if necessary.
bash run-benchmark.sh
Run date: 2026-08-03
Environment:
- PostgreSQL 18.1 (Postgres.app)
- Apple M1 Pro, 10 cores, 32 GB RAM
- macOS 15.7.1
- Two PostgreSQL clusters on the same host
- 256 MB
shared_buffersper cluster fsync=off,synchronous_commit=off,full_page_writes=off,autovacuum=off- Publisher used
wal_level=logicalin every scenario
The main workload used eight clients, 20,000 transactions and 25 inserts per
transaction (500,000 rows). CPU figures include PostgreSQL server processes but
exclude pgbench and the pg_recvlogical client. Each subscription run waited
for an explicit marker row to become queryable on the subscriber.
Raw main-workload runs
| Run | Scenario | TPS | Workload s | Drain s | Total s | Publisher CPU s | Subscriber CPU s |
|---|---|---|---|---|---|---|---|
| A | Baseline | 11843.675 | 1.712 | 0.024 | 1.736 | 4.750 | 0.000 |
| A | Decode only | 7195.940 | 2.803 | 3.972 | 6.775 | 7.970 | 0.000 |
| A | Logical text | 11506.997 | 1.760 | 2.491 | 4.251 | 7.470 | 3.010 |
| A | Logical binary | 9314.725 | 2.179 | 2.025 | 4.204 | 7.890 | 2.640 |
| A | Text + four indexes | 9148.729 | 2.220 | 3.053 | 5.274 | 6.650 | 4.660 |
| B | Baseline | 8016.260 | 2.530 | 0.048 | 2.578 | 4.710 | 0.000 |
| B | Decode only | 9083.127 | 2.225 | 4.484 | 6.710 | 7.920 | 0.000 |
| B | Logical text | 5461.282 | 3.702 | 3.454 | 7.156 | 7.690 | 2.930 |
| B | Logical binary | 6976.843 | 2.900 | 2.123 | 5.023 | 7.230 | 2.540 |
| B | Text + four indexes | 11307.203 | 1.790 | 3.604 | 5.394 | 6.090 | 4.620 |
| C | Baseline | 12645.438 | 1.609 | 0.028 | 1.637 | 4.350 | 0.000 |
| C | Decode only | 5635.959 | 3.572 | 4.354 | 7.927 | 7.720 | 0.000 |
| C | Logical text | 9405.749 | 2.155 | 2.477 | 4.632 | 7.710 | 2.940 |
| C | Logical binary | 7892.504 | 2.563 | 3.004 | 5.566 | 7.740 | 2.760 |
| C | Text + four indexes | 8917.373 | 2.265 | 3.624 | 5.889 | 5.470 | 4.840 |
Main-workload medians
| Scenario | Publisher CPU s | Subscriber CPU s | Total CPU s | Relative CPU | Drain s |
|---|---|---|---|---|---|
| Baseline | 4.71 | 0.00 | 4.71 | 1.00x | 0.03 |
| Decode only | 7.92 | 0.00 | 7.92 | 1.68x | 4.35 |
| Logical text | 7.69 | 2.94 | 10.62 | 2.25x | 2.49 |
| Logical binary | 7.74 | 2.64 | 10.50 | 2.23x | 2.12 |
| Text + four indexes | 6.09 | 4.66 | 10.71 | 2.27x | 3.60 |
Publisher CPU, subscriber CPU and drain are per-column medians. Total CPU is the median of the per-run publisher-plus-subscriber sums, so it is not always the exact sum of the independently calculated CPU medians.
TPS varied substantially because publisher and subscriber shared the same CPU. The post therefore uses CPU and applied-marker drain time as the primary metrics.
Subscriber WAL
One representative run, with pg_stat_wal reset after scenario setup and before
the workload:
| Scenario | Subscriber WAL bytes | MiB |
|---|---|---|
| Logical text | 143,476,467 | 136.8 |
| Logical binary | 143,448,119 | 136.8 |
| Text + four indexes | 284,912,301 | 271.7 |
REPLICA IDENTITY FULL
The table contained 50,000 rows. The subscription was paused whilst the final 2,000 rows were updated, then resumed. Completion was measured with a separate replicated marker transaction.
| Run | Subscriber lookup path | Apply s | Publisher CPU s | Subscriber CPU s |
|---|---|---|---|---|
| A | No usable index | 4.565 | 0.020 | 4.220 |
| A | B-tree on id |
0.149 | 0.020 | 0.010 |
| B | No usable index | 4.777 | 0.020 | 4.270 |
| B | B-tree on id |
0.151 | 0.020 | 0.010 |
The midpoint ratio is approximately 31.1x. This is an intentionally adversarial row distribution designed to expose the sequential lookup path, not a universal replication multiplier.