Artie vs. AWS DMS: Benchmarking Real-Time CDC From Postgres to Snowflake | Artie
Artie is a real-time database replication platform that streams changes from databases like Postgres and MySQL into warehouses and lakes. We handle the full pipeline – change data capture (CDC), merges, schema evolution, backfills, and observability – so your data team doesn't have to.
Under sustained production-level writes, AWS DMS fell 33 minutes behind the source database - and the gap was still growing. Artie stayed under 30 seconds.
We ran head-to-head benchmarks comparing Artie and AWS Database Migration Service (DMS) for replicating data from Postgres to Snowflake. We tested initial snapshots, ongoing CDC replication, history mode, and impact on the source database. Every configuration and query is documented below, and all code is published at github.com/artie-labs/benchmarks.
At a Glance
ArtieAWS DMSDeltaSnapshot (100M rows)9m 36s~12m avgComparableCDC replication latency (sustained)29.7s avg2,029s avg68x fasterCDC history mode latency (sustained)84.8s avg1,978s avg23x fasterWAL retention at end of test385 MB6,271 MB16x lessWAL trendPlateauedStill growing–
Why This Benchmark Matters
If your stack is on AWS and you need to replicate from Postgres into Snowflake, DMS is probably the first thing you'll try. It's the default. It's already in your AWS console. And for one-time migrations, it works fine – because that's what it was built for.
But CDC isn't a one-time migration. It's a continuous stream of changes flowing from your production database into your warehouse, ideally in near real-time. As write volumes go up, the architectural differences between a migration tool and a purpose-built streaming platform start to compound. We wanted to put real numbers on exactly how much they compound.
The question: Under sustained, production-level writes (~22,400 events/sec for 10 minutes straight), how do Artie and DMS compare on latency, throughput, and operational impact to the source database?
Setup
To ensure a fair comparison, both tools replicated from the same AWS RDS Postgres instance to the same Snowflake warehouse in the same region.
ComponentSpecificationConfigurationSourceAWS RDS PostgreSQL 18.1db.m7i.2xlarge 8 vCPU, 32 GiB RAM, gp3 100 GB, us-west-2DestinationSnowflake, BENCHMARK_WH Size: Large Type: StandardAWS us-west-2AWS DMSdms.t3.large2 vCPU, 8 GiB RAM, gp2 50 GB, DMS v3.6.1ArtieArtie Reader (proprietary) Artie Transfer (open source)Deployed as containers in EKS, resource usage measured per runDatasetpgbench_accounts100 million rowsCDC load50 concurrent clients, 8 threads, 600 seconds, ~22,400 events/sec (13.4M total events)
All DMS task settings (batch sizes, buffer configs, error handling) are published in the benchmarks repo.
How the Pipelines Work
Before we get into results, it's worth understanding how these two pipelines are architected. This is where most of the performance gap comes from.
AWS DMS Pipeline (4 hops)
DMS reads from the Postgres WAL (Write-Ahead Log - the append-only log that Postgres uses internally for crash recovery and replication) and routes data through four stages before it lands in Snowflake:
DMS Instance - buffers changes and serializes them into Parquet files. At 22,400 events/sec, the 2 vCPU instance becomes CPU-bound.
S3 - file write latency plus batch accumulation delay.
Snowpipe - an async queue with unpredictable notification delay.
Merge Task - a separate consolidation step into the final table.
Artie Pipeline (2 hops)
Artie Reader reads directly from the Postgres logical replication stream, queues in Kafka, Artie Transfer buffers changes in memory, and flushes micro-batches to Snowflake using optimized MERGE statements. No intermediate storage, no S3, no Snowpipe.
Under low write volumes, the extra hops in the DMS pipeline may not matter much. Under sustained production load, the buffering at each stage compounds. That's the core of what these benchmarks measure.
Methodology
We used pgbench - PostgreSQL's built-in benchmarking tool - for all data generation and load testing. Nothing custom, nothing proprietary.
Populating the table (100M rows):
pgbench -i -I dtGvp -s 1000 postgres
Before each snapshot run:
-- Optimize page layout<br>VACUUM FULL pgbench_accounts;<br>-- Warm the RAM cache<br>SELECT COUNT(*) FROM pgbench_accounts;
Generating sustained CDC load:
We ran a custom TPC-B-like script that performs UPDATE, INSERT, and transaction operations across 100,000 distinct rows:
pgbench -f "custom_tpcb.sql"-c 50 -j 8 -T 600 -M prepared -P 5 -n postgres<br>\set aid random(1, 100000 * :scale)<br>\set bid random(1, 1 * :scale)<br>\set tid random(1, 10 * :scale)<br>\set delta random(-5000, 5000)<br>-- pipeline mode to reduce round trips<br>-- https://www.postgresql.org/docs/current/libpq-pipeline-mode.html<br>\startpipeline<br>BEGIN;<br>UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;<br>UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid =...