Postgres CDC: Every Change Data Capture Method Compared

Intergalactic Data Labs
—
—
11 min read
Postgres offers five ways to capture changes, and only one of them sees every insert, update, and delete without changing your application. As of October 2026 those are triggers, LISTEN and NOTIFY, timestamp polling, logical decoding of the write-ahead log, and tools built on that log such as Debezium, Fivetran, PeerDB, and Filament. By the end of this guide you will have logical replication CDC running and verified.
You need | Value |
|---|---|
Postgres version | 10 or later for pgoutput, 18.6 current |
Server parameter |
|
Role | REPLICATION attribute plus SELECT on tables |
Tables | Primary key on every captured table |
Worked example | Filament Postgres source, beta |
Time to complete | About 45 minutes |
Versions come from the Postgres 18 docs and Debezium's connector page, parameters and roles from the logical replication configuration page.
How does Postgres change data capture work?
Every Postgres CDC method is a way of reading, or imitating, the write-ahead log. Change data capture means recording each row change as an event you can replay elsewhere. Postgres already records every committed change in its write-ahead log, so the real question is how you get those records out.
The Postgres docs define the native path. "Logical decoding is the process of extracting all persistent changes to a database's tables into a coherent, easy to understand format." A replication slot marks how far a consumer has read. The same page notes that slots are crash-safe and that only one receiver may consume a slot at a time.
Three pieces work together. A publication names the tables and operations to publish, a slot holds the stream position and the WAL the consumer still needs, and an output plugin such as pgoutput or wal2json formats the changes.
The other methods avoid the log and pay for it. Triggers run inside every transaction. NOTIFY payloads are capped under 8000 bytes and reach only sessions listening at that moment. Polling an updated_at column cannot see deletes, as the Filament source docs put it, "Hard deletes are not observable through an update watermark."
Method | Sees deletes | Needs wal_level logical | Survives consumer downtime | Touches the app |
|---|---|---|---|---|
Row triggers to audit table | Yes | No | Yes | Adds write cost |
LISTEN and NOTIFY | Only if sent | No | No | Yes, app sends |
Timestamp or xmin polling | No | No | Yes, re-query | Needs cursor column |
Logical decoding, pgoutput | Yes | Yes | Yes, slot holds WAL | No |
Logical decoding, wal2json | Yes | Yes, plus plugin listing | Yes, slot holds WAL | No |
Tool on top of a slot | Yes | Yes | Yes, slot plus checkpoint | No |
Row sources are the CREATE TRIGGER, LISTEN, system columns, and logical decoding pages.
Which Postgres CDC method should you pick?
Pick logical decoding unless you cannot change wal_level, and put a tool on top of it. Triggers still win for an audit trail inside the same database. Polling wins when deletes do not matter, and NOTIFY wins for waking a worker, never for moving data.
One disclosure before the worked example. Galaxy builds Filament, the tool used in the steps below, and its Postgres source is beta, which the docs define as runs end to end but not verified for every environment. The Postgres steps transfer to Debezium, PeerDB, or dlt unchanged.
How to set up Postgres CDC with Filament, step by step
Eight steps take you from a stock Postgres to a verified change stream feeding a typed replica. Steps one to four are plain Postgres and apply to every tool. Steps five to eight are the Filament side.
1. Set wal_level to logical and restart
Run this on the source, then restart, because the docs say this parameter can only be set at server start.
The default of 10 for max_replication_slots and max_wal_senders is enough for one pipeline, per the replication settings page. Managed services rename the switch, see the breakage section.
2. Create a replication role
The security page requires the REPLICATION attribute and SELECT on every published table.
The CREATE grant lets the tool manage its own publication. Add a pg_hba.conf entry allowing replication connections for this role.
3. Confirm primary keys and replica identity
Every captured table needs a primary key. Filament rejects keyless tables in CDC mode, and Postgres refuses updates and deletes on published tables without a usable replica identity.
Any table that comes back needs a key before step six. Leave replica identity at DEFAULT unless a table carries large TOAST columns, in which case set it to FULL.
4. Decide who owns the publication
Filament creates a publication named filament by default and keeps its table list current. If a DBA prefers to provision it, create it now and pass manage_publication=false later.
Scope it to the tables you need, which PeerDB's docs also recommend.
5. Deploy Filament with the PostgreSQL datastore
The source docs state that CDC runs need a datastore with durable replication-stream admission and that the PostgreSQL datastore provides it. That means a deployed Filament rather than the local SQLite context. The Helm chart is the recommended production path, and the Render Blueprint is the one-click option.
Then point the CLI at it, per the CLI guide.
6. Create the CDC source and the sink
Connector fields become flags with a source- or sink- prefix, so the replication field becomes --source-replication. The DSN is stored by the deployment's secret provider.
7. Create the pipeline in merge mode
A CDC route carries no per-resource read mode, and its write mode is append or merge. Merge keeps a current replica, so deletes remove rows, per the replication modes page. The flags below follow the documented pipeline command with the CDC values.
Choose append instead for an immutable history where deletes stay as events.
8. Run the first cycle, then schedule it
The first run creates the slot with an exported snapshot and reads each table through it, so, in the source docs' words, "the baseline and the change stream share one consistent point."
Each later run is a bounded catch-up from the last committed position to the current WAL position. A cron schedule in the web app's pipeline settings keeps the replica current without a resident process.
How to verify Postgres CDC worked
One query on the source and one on the destination prove the pipeline is healthy. Start with the slot, using the pg_replication_slots view.
A wal_status of reserved and a small lag_bytes after a run means the consumer is keeping up. Then make a change and run another cycle.
Run filament run orders-cdc again and query the replica. Row 42 should carry the new status and customer 7 should be gone. Columns prefixed _filament_ record the operation and source position for each change.
What to do when Postgres CDC breaks
Five failures cover almost every CDC incident.
The slot is filling the disk
Postgres warns that "Replication slots persist across crashes and know nothing about the state of their consumer(s)." Cap retention with max_slot_wal_keep_size, drop slots nobody consumes, and on Postgres 18 set idle_replication_slot_timeout per the replication settings.
wal_level is not logical on a managed service
Each provider hides the parameter behind its own switch.
Service | Setting | Restart |
|---|---|---|
Amazon RDS and Aurora |
| Yes |
Google Cloud SQL |
| Yes |
Azure Flexible Server |
| Yes |
Supabase | Already logical, direct connection only | No |
Neon | Enable in project settings, cannot revert | Yes |
Per the RDS, Cloud SQL, Azure, Supabase, and Neon docs.
Updates or deletes fail on the publisher
The error means a published table lacks a usable replica identity. Add a primary key, or as a last resort set the identity to FULL, which the ALTER TABLE docs describe as recording the old values of all columns.
The slot shows wal_status lost
A lost slot cannot be replayed. Drop it and let the next run rebuild from a fresh snapshot.
wal2json is refused after a minor upgrade
Postgres 18.6 and the matching minor releases added output_plugin_libraries, which defaults to pgoutput and test_decoding only, per the 18.6 release notes. Add wal2json to that list and reload. Pipelines on pgoutput are unaffected.
Other ways to do Postgres CDC and what they cost you
Every tool below reads the same slot, so the differences are deployment, pricing unit, and what happens around the stream. Licenses come from each project's LICENSE file, not its README badge.
Tool | Plugin | Deployment | License or pricing | Documented limit |
|---|---|---|---|---|
pgoutput or decoderbufs | Kafka Connect or Debezium Server | No DDL events, no generated columns | ||
pgoutput | Self-host or ClickHouse Cloud | Maintained paths are Postgres to ClickHouse and Postgres | ||
pgoutput | Self-host or Cloud | One destination per CDC source | ||
pgoutput | SaaS | Needs its own slot, full dump first | ||
Publication based | SaaS | Needs a watermarks table | ||
pgoutput | Python library | No scd2 merge strategy | ||
pgoutput | Go CLI | Catch-up by default, stream with a flag | ||
test_decoding or pglogical | Managed instance | FULL identity unsupported with pglogical | ||
pgoutput | Managed | One slot per stream | ||
pgoutput | CLI, Helm, Go library | TRUNCATE fails the run, needs its datastore |
The Filament route fits teams that want a binary they run themselves with CRC32-C verification on each batch, per the integrity docs.
No speed claim appears here on purpose. The public August 2026 benchmark cohort measured full loads only, and the Filament benchmark page says results do not predict incremental or CDC performance.
Next steps
Start with what change data capture is and Postgres logical replication in full. Full load vs incremental vs CDC explains when polling is enough and the CDC tools listicle goes deeper on the alternatives. The Postgres replication benchmark holds the full-load evidence and the zero-downtime migration guide shows CDC doing a cutover. The Filament announcement covers deployment options.
Frequently asked questions
What are the main CDC methods in PostgreSQL?
There are five. Row triggers that write to an audit table, LISTEN and NOTIFY messages sent by the application, and polling a timestamp or xmin column are the three that avoid the log. Logical decoding through pgoutput or wal2json reads the log directly, and CDC tools consume that same log for you. Logical decoding is the only one that sees every insert, update, and delete without touching the application.
Is logical replication the same as CDC?
Not quite. Logical decoding is the Postgres mechanism that turns the write-ahead log into a stream of row changes. Logical replication is the built-in publish and subscribe feature that consumes that stream to copy tables to another Postgres. CDC tools consume the same stream through a replication slot and send the changes to warehouses, lakes, or queues instead.
How do I check if CDC is enabled in Postgres?
Run SHOW wal_level and confirm it returns logical. Then query pg_replication_slots to see whether any logical slot exists and whether its active column is true. A wal_level of replica or minimal means no logical decoding is possible until you change the parameter and restart the server.
What is the difference between pgoutput and wal2json?
pgoutput is the output plugin that ships with Postgres and powers built-in logical replication, so it is always present and speaks a binary protocol. wal2json is a third-party plugin that emits JSON and must be installed separately. Since the August 2026 minor releases it must also be listed in output_plugin_libraries or the server refuses to load it.
What is REPLICA IDENTITY and when do I need FULL?
Replica identity controls which old column values Postgres writes to the log for updates and deletes. The default records the primary key, which is enough for most tools. FULL records the whole old row and is needed when a table has no primary key or a consumer must see unchanged TOAST columns.
How do I stop a replication slot from filling my disk?
Set max_slot_wal_keep_size so an abandoned slot cannot retain unlimited WAL, drop slots you no longer consume with pg_drop_replication_slot, and watch wal_status in pg_replication_slots. Postgres 18 adds idle_replication_slot_timeout, which invalidates slots that sit unused. Tools that emit heartbeats keep the slot moving on quiet databases.
Does Postgres CDC need Kafka?
No. Kafka is a transport some tools chose, not a Postgres requirement. Debezium Server, PeerDB, Estuary, dlt, ingestr, and Filament all read a replication slot and write straight to a destination without a broker. Pick Kafka when many consumers need the same stream.
Can Filament run Postgres CDC on a schedule instead of continuously?
Yes. The Postgres source runs CDC as bounded catch-up cycles, each replaying from the last committed position to the current WAL position and then returning. The docs describe continuous CDC as a re-request loop, so a cron schedule on the pipeline keeps the replica current without a resident process.
More articles
Questions
Answered
FAQ
What does Galaxy do?
What is Filament?
What is enterprise context management?
What does working with Galaxy look like?
How do you handle security and compliance?
Why does Galaxy build in the open?
Company
Copyright © 2026 Galaxy. All rights reserved.