Wirekite’s SQL Server change extractor captures committed changes from the transaction log and streams them into a Kafka/Redpanda queue. We benchmarked how fast that capture path runs on a single host. The headline: 374,000 changes per second sustained — backed by a separate one-hour endurance run that decoded 811 million changes with zero ordering violations and zero page splits.
The result
Two runs on the same rig, at different settings:
- An uncapped run reached the extraction ceiling — a sustained 374,000 changes/sec.
- A rate-capped one-hour run proved endurance — 811 million changes at a steady 226,000 changes/sec, holding from minute 10 to the end with no degradation and zero errors.
| metric | value |
|---|---|
| Peak sustained extraction | 374,000 changes/sec |
| One-hour endurance run | 811,000,000 changes @ 226,000 changes/sec |
| LSN ordering violations | 0 |
| Page splits | 0 |
| Queue publish errors | 0 |
| Records lost or corrupted | 0 |
The workload
A pure-UPDATE workload is the hardest case for SQL Server CDC: every change is a logged modification, and CDC adds a before/after image to the log for every tracked row — there’s no cheap append work to coast on.
Five identical wide tables, 5 million rows each, every operation an UPDATE:
CREATE TABLE dbo.firenibble_N (
id BIGINT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
int_col_1 INT,
int_col_2 INT,
int_col_3 INT,
int_col_4 INT,
varchar_col_1 VARCHAR(100),
varchar_col_2 VARCHAR(100),
varchar_col_3 VARCHAR(100),
varchar_col_4 VARCHAR(100),
date_col_1 DATE,
date_col_2 DATE
) WITH (FILLFACTOR = 80);
FILLFACTOR = 80 leaves free space on every page so in-place integer UPDATEs never trigger page splits.
| parameter | value |
|---|---|
| Operation mix | 100% UPDATE |
| Columns touched per update | 50% (integer columns) |
| Concurrent writers | 25 (endurance run) / 50 (ceiling run) |
| Rate cap | 250,000 changes/sec (endurance) / none (ceiling) |
| Duration | one hour (endurance run) |
The hardware
| Host | GCP c4-standard-96 |
| vCPUs | 96 |
| RAM | 354 GB |
| NUMA nodes | 8 |
| OS | Ubuntu 22.04 LTS |
| SQL Server | 2022 RTM-CU24-GDR (16.0.4250.1), Developer Edition |
Disk layout
The transaction log gets its own dedicated NVMe volume. SQL Server’s log writer serializes every commit on a synchronous flush, so log-volume latency directly sets the commit ceiling — sharing it with data-file I/O would cap throughput as the commit rate climbs.
| volume | device | role |
|---|---|---|
| data | NVMe, 256 GB | 8 × 16 GB data files (/mnt/mssql/data) |
| log | NVMe, 256 GB (dedicated) | single 50 GB pre-sized log file (/mnt/mssql/log) |
| tempdb | shares the data volume | 8 × 1 GB files |
SQL Server configuration
Instance (sp_configure)
| setting | value |
|---|---|
| max server memory | physical minus ~12 GB OS reserve |
| min server memory | matched to max |
| max degree of parallelism | 8 |
| cost threshold for parallelism | 50 |
| optimize for ad hoc workloads | 1 |
Database (ALTER DATABASE)
| setting | value |
|---|---|
| Recovery model | FULL (required by CDC) |
| Read Committed Snapshot | ON |
| Change Data Capture | ON |
| Query Store | OFF |
| Delayed Durability | DISABLED (CDC blocks it) |
| Target recovery time | 60 s |
Linux kernel tuning (Microsoft-recommended for SQL Server on Linux)
vm.swappiness = 1
vm.dirty_background_ratio = 3
vm.dirty_ratio = 80
vm.dirty_expire_centisecs = 500
vm.dirty_writeback_centisecs = 100
vm.max_map_count = 1600000
kernel.numa_balancing = 0
Transparent Huge Pages = always
The one-hour endurance run
811 million changes decoded over the hour at a steady 226,000 changes/sec. Throughput reached steady state by minute 10 and held it — no degradation across the full hour:
| segment | duration | changes | rate |
|---|---|---|---|
| 1 | 323 s | 65.4 M | 202 K/s |
| 2 | 513 s | 123.6 M | 241 K/s |
| 3 | 512 s | 118.2 M | 231 K/s |
| 4 | 521 s | 115.7 M | 222 K/s |
| 5 | 514 s | 117.1 M | 228 K/s |
| 6 | 505 s | 111.9 M | 222 K/s |
| 7 | 508 s | 116.7 M | 230 K/s |
| overall | 56 min | 768.7 M | 226 K/s |
Quality held perfectly the entire time:
| metric | value |
|---|---|
| Internal page splits | 0 |
| LSN ordering violations | 0 |
| Queue publish errors | 0 |
| Records lost / corrupted | 0 |
And Wirekite keeps the transaction log bounded as it captures — across the whole hour the log oscillated between roughly 1.5 GB and 4 GB instead of growing unbounded, so the log volume never ran away.
What gates the throughput
The binding constraint is SQL Server’s own commit path — the synchronous log writer (the WRITELOG wait), made heavier because CDC forbids delayed durability. The Wirekite extractor was never the bottleneck: at every rate tested it kept pace with the source with no growing backlog.
It’s an honest number, too. SQL Server CDC adds supplemental before/after-image logging for every tracked UPDATE — roughly doubling the log bytes per change. Wirekite requires CDC, so that overhead is already baked into the rates above.
What this means
374,000 changes per second is far more than most SQL Server sources will ever emit — a busy OLTP database committing 50,000 changes/sec long-run still has roughly 7× headroom before capture would begin to lag. And the one-hour, 811-million-change run shows the capture path holds its rate with zero ordering violations and a transaction log that stays bounded — the two failure modes that actually break CDC pipelines in production.