meta:
updated: "2026-07-19"
version: "0.20.1"
hardware:
devbox: "Apple M-series, macOS (colima docker), CDC profile ports 5434/3307/1434/27018"
note: >
Baselines below were measured on the devbox on 2026-07-19 unless dated
otherwise. A run stamps its own hardware/date; treat cross-hardware absolute
numbers as indicative, ratios (x baseline) as portable.
engines: [postgres, mysql, mssql, mongo]
scenarios:
- id: cdc_capture_throughput
what: "bulk capture rows/s — one bounded until_current drain over a seeded backlog"
harness: "dev/cdc/stand.sh perf"
contract: "engine-relative; regression = >2x drop vs baseline at the same rollover"
postgres: { measured: { rows_per_s: 256000, date: "2026-07-19" } }
mysql: { measured: { rows_per_s: 500000, date: "2026-07-19" } }
mssql: { measured: { rows_per_s: 189000, date: "2026-07-19" } }
mongo: { measured: { rows_per_s: 38000, date: "2026-07-19", note: "JSON-blob model; per-event resume token" } }
- id: cdc_interval_soak_rss_flat
what: "peak drain RSS stays FLAT as the drain INTERVAL grows 12x (10→120 min) — memory is O(rollover), NOT O(per-interval backlog); a regression tracks the interval"
harness: "dev/cdc_interval/soak_all.sh <engine> (PHASES='10 20 30 60 120 240 360 480')"
contract: "peak RSS at 480 min ≈ peak RSS at 10 min (within noise); every churned row captured"
note: >
The harness SELF-ASSERTS this contract each run (RSS-flat across the growing
intervals + source COUNT(*) == distinct drained ids), so the contract IS
guarded — the per-interval numbers below are just not persisted to a doc (the
soak.log lives in a run tmpdir). Do NOT re-run to 'fill' this: the harness
already proves it; harvest a run's soak.log if a captured baseline is wanted.
postgres: { measured: { note: "RSS-flat asserted by soak_all.sh pg (10→120 default); numbers unpersisted" } }
mysql: { measured: { note: "RSS-flat asserted by soak_all.sh mysql; numbers unpersisted" } }
mssql: { measured: { note: "RSS-flat asserted by soak_all.sh mssql; numbers unpersisted" } }
mongo: { measured: { peak_rss_mb: 72, note: "22k events, flat RSS across intervals (ad-hoc 2026-07)" } }
- id: cdc_cotenancy_writer_cost
what: "the writer's throughput/latency under a looping CDC drain vs a no-CDC baseline — the co-tenancy cost of running CDC on a live source"
harness: "dev/cdc/harm.py (PG paced) · harm_heavy.py (PG pgbench) · harm_mysql/mongo/mssql.py"
contract: "negligible except SQL Server; a regression pushes a passive-reader engine (PG/MySQL/Mongo) materially past ~1.0x"
postgres: { measured: { latency_x: 1.02, throughput_x: 0.98, date: "2026-07-19", note: "pgbench 15k tps; logical decoding is light" } }
mysql: { measured: { throughput_x: 1.07, date: "2026-07-19", note: "binlog dump is a passive reader — no cost" } }
mssql: { measured: { throughput_x: 0.78, date: "2026-07-19", note: "−22%: the capture Agent DUPLICATES every change into change tables (a 2nd write) — inherent to SQL Server CDC, not rivet" } }
mongo: { measured: { throughput_x: 1.01, date: "2026-07-19", note: "change stream reads the oplog — no cost" } }
- id: cdc_slot_retention_harm
what: "does the CDC READER pin source-side log retention (the disk-fill hazard)? PostgreSQL's logical slot does; the others' logs are trimmed on the server's own schedule"
harness: "dev/cdc/harm.py (scheduled-drain retention) · harm_*.py (reader-independence)"
contract: "PG: monitor slot lag / retained WAL (an abandoned slot fills the disk). Others: no reader-pinned retention — structurally cannot fill the source disk"
postgres: { measured: { class: "reader-pinned", note: "retention ~= interval × write-rate when keeping up (23.5MB/6s @ ~4MB/s); an ABANDONED slot is unbounded — observed a real 29GB pin that crashed postgres (disk-fill). MONITOR slot lag." } }
mysql: { measured: { class: "reader-independent", note: "binlog purged by binlog_expire_logs_seconds regardless of reader — no disk-fill harm" } }
mssql: { measured: { class: "reader-independent", note: "change tables trimmed by the CDC cleanup job regardless of reader — no disk-fill harm" } }
mongo: { measured: { class: "reader-independent", note: "oplog is a fixed-size capped collection (4.4GB cap observed) — no disk-fill harm" } }
- id: cdc_drain_keeps_up_under_heavy_wal
what: "at a high WAL/write rate, does a continuous (back-to-back) drain KEEP UP (retention bounded) or fall behind (retention grows unbounded)?"
harness: "dev/cdc/harm_heavy.py (pgbench + retention watchdog)"
contract: "PG: back-to-back drain keeps up to the tested rate; retention bounded. Beyond that rate, use a shorter scheduler interval or accept growing lag"
postgres: { measured: { keeps_up_to_tps: 15000, peak_retained_mb: 41, date: "2026-07-19", note: "watchdog did not trip; lag ~1MB" } }
mysql: { na: "no consume-retention coupling (reader-independent) — 'keep up' is not a retention concern" }
mssql: { na: "no consume-retention coupling (reader-independent)" }
mongo: { na: "no consume-retention coupling (capped oplog)" }
- id: batch_export_memory_flat
what: "peak RSS stays flat regardless of table/row size — no full-table materialization (the flat-memory field-validation)"
harness: "dev/bench/smoke.py (rivet row) · field runs"
contract: "peak RSS independent of row count; no OOM at prod scale"
postgres: { measured: { peak_rss_mb: 59, note: "454M-row table pulled in ~2h, no OOM (Airbyte OOM'd on the same) — field-validated" } }
mysql: { measured: { note: "same streaming architecture — flat RSS (shared build_array path)" } }
mssql: { measured: { note: "same streaming architecture — flat RSS" } }
mongo: { measured: { note: "same streaming architecture — flat RSS" } }
- id: batch_export_hot_source_harm
what: "OLTP p99 degradation / long-held query (xmin-hold → vacuum-block → bloat) on a LIVE source under a co-running rivet BATCH export — the 'can I point rivet at prod, or do I need a replica?' question"
harness: "dev/bench/smoke.py (OltpProbe p99 + GaugePoller longq/xmin/locks/blocked/dead-tuples) → docs/bench/report.html"
contract: >
SOURCE-SAFETY: rivet must hold NO long-running query (server-side cursor, not
a single long scan) so it never pins xmin / blocks vacuum / bloats the source.
The transient OLTP p99 bump during the export is the only cost; `mode: chunked`
lowers it. Verdict: SAFE to point at a live source.
postgres:
measured:
longq_s: 0.00
oltp_p99_x: 5.1
oltp_p99_x_chunked: 3.3
peak_rss_mb: 57
date: "2026-07-09"
note: >
longq=0.00s — rivet holds NO long-running query → does NOT pin xmin /
block vacuum / bloat the source, UNLIKE clickhouse (longq=50.3s) and
duckdb (longq=7.7s) which pin one query for the whole extract. Only cost
is a TRANSIENT p99 bump during the export (5.1x full-scan, 3.3x chunked) —
no lasting harm. (Captured in the cross-tool bench; NOT re-run since — the
source-safety property is architectural, not per-run.)
mysql: { measured: { longq_s: 0.00, note: "same server-side-cursor architecture — no long-held query; captured in the cross-engine bench" } }
mssql: { measured: { longq_s: 0.00, note: "same architecture — no long-held query" } }
mongo: { na: "no long-held snapshot-txn / xmin analog; the JSON-blob keyset scan does not hold a vacuum-blocking horizon the way an MVCC SQL snapshot does" }