/

Enterprise Context Strategy

Postgres Logical Replication: The Complete Guide

Intergalactic Data Labs

—

—

12 min read

Table of contents

Summarize

Postgres logical replication decodes committed row changes from the write-ahead log and streams them, per table, to any client that speaks the pgoutput protocol. As of September 2026 there are three ways to consume it. Native CREATE SUBSCRIPTION copies into another Postgres, Debezium and PeerDB push it into Kafka or ClickHouse, and Filament, dlt, Airbyte, and Fivetran land it in typed tables. Snapshot consistency, slot hygiene, and the target decide which.

Way to consume

Targets

Initial snapshot

Deployment

License

Native subscription

Postgres only

Per-table sync workers

None, built in

PostgreSQL

Filament

Postgres, MySQL, warehouses, lakes

One exported snapshot

Binary, Go library, Helm

Apache 2.0

Debezium

Kafka topics, Debezium Server sinks

Consistent snapshot

Kafka Connect or Debezium Server

Apache 2.0

PeerDB

ClickHouse, Postgres

Full initial load

Multi-container Docker

AGPLv3

dlt

Any dlt destination

Optional snapshot

Python library

Apache 2.0

Airbyte

Airbyte catalog

Debezium snapshot

Hosted or self-hosted

ELv2

Fivetran

Fivetran catalog

Managed

Hosted only

Commercial, MAR

What is Postgres logical replication?

Logical replication is the WAL, decoded into row changes and filtered by a publication. The Postgres manual defines it as "a method of replicating data objects and their changes, based upon their replication identity." Logical contrasts with physical replication, "which uses exact block addresses and byte-by-byte replication."

Four parts make it work. The write-ahead log records every change first. Logical decoding turns those records into "a coherent, easy to understand format," per the decoding docs, through the pgoutput plugin, "the standard one used for the built-in logical replication," per the protocol reference. A publication chooses which tables and operations to publish.

The fourth part is the replication slot, "a stream of changes that can be replayed to a client in the order they were made," per the decoding docs. Creating a slot over the replication protocol exports a snapshot of "exactly the state of the database after which all changes will be included in the change stream." That gives an external consumer a baseline and a stream that agree.

Physical replication ships the whole cluster instead, and WAL shipping "can only be done for the entire database server," per the comparison page.

Property

Physical (streaming)

Logical

Unit

Whole cluster

Selected tables

Format

Raw WAL bytes

Decoded row changes

Target writable

No, read-only standby

Yes

Cross major version

No

Yes

Non-Postgres targets

No

Yes, through pgoutput

DDL replicated

Yes

No

What decides a logical replication setup?

Six criteria separate a setup that survives production from one that fills a disk on a Sunday.

Criterion

What to check

Source

Snapshot consistency

Baseline and stream share one point

Decoding docs

Slot and WAL retention

Slot advances, max_slot_wal_keep_size set

Replication settings

Delete handling

Replica identity on every table

Publication docs

Schema changes

DDL is never replicated

Restrictions

Operational footprint

Processes and services you now run

Each tool's docs

Destination

Postgres only, or anything

Subscription docs

Slot retention is the one that hurts. Slots "will prevent removal of required resources even when there is no connection using them," per the decoding docs, and "in extreme cases this could cause the database to shut down."

Deletes need a replica identity, the primary key by default, per the publication docs. Schema changes are the silent one, since "the database schema and DDL commands are not replicated," per the restrictions page.

How to set up Postgres logical replication step by step

Five steps take a stock server to a second Postgres receiving every change in commit order. This is the native path from the quick setup page, and every external consumer reuses the first two steps.

Step 1. Set wal_level to logical and restart
ALTER SYSTEM SET wal_level = logical;
-- restart the server, then confirm

ALTER SYSTEM SET wal_level = logical;
-- restart the server, then confirm

ALTER SYSTEM SET wal_level = logical;
-- restart the server, then confirm

The WAL settings say this parameter "can only be set at server start." On RDS set rds.logical_replication to 1, per the RDS docs, and on Cloud SQL turn on cloudsql.logical_decoding, per the Cloud SQL docs.

Step 2. Create a replication role and allow it in pg_hba.conf
CREATE ROLE repuser WITH LOGIN REPLICATION PASSWORD 'change-me';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO

CREATE ROLE repuser WITH LOGIN REPLICATION PASSWORD 'change-me';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO

CREATE ROLE repuser WITH LOGIN REPLICATION PASSWORD 'change-me';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO




The pg_hba.conf line is the manual's own example, and the default of 10 for max_replication_slots covers a basic setup, per the replication settings.

Step 3. Copy the schema and create the publication
pg_dump --schema-only -t users -t departments app | psql -h
pg_dump --schema-only -t users -t departments app | psql -h
pg_dump --schema-only -t users -t departments app | psql -h
CREATE PUBLICATION mypub FOR TABLE users,
CREATE PUBLICATION mypub FOR TABLE users,
CREATE PUBLICATION mypub FOR TABLE users,

The schema "can be copied by hand using pg_dump --schema-only," per the restrictions page. A publication publishes insert, update, delete, and truncate by default, per CREATE PUBLICATION, and creating it "does not start replication."

Step 4. Create the subscription
CREATE SUBSCRIPTION mysub
  CONNECTION 'dbname=app host=publisher user=repuser'

CREATE SUBSCRIPTION mysub
  CONNECTION 'dbname=app host=publisher user=repuser'

CREATE SUBSCRIPTION mysub
  CONNECTION 'dbname=app host=publisher user=repuser'

This command "normally creates a replication slot on the publisher," per CREATE SUBSCRIPTION. Sync workers snapshot each table, then stream the changes made during the copy, per the architecture page.

Step 5. Verify the subscription and the slot
SELECT subname, received_lsn, latest_end_lsn FROM pg_stat_subscription;
SELECT slot_name, active, wal_status, confirmed_flush_lsn FROM

SELECT subname, received_lsn, latest_end_lsn FROM pg_stat_subscription;
SELECT slot_name, active, wal_status, confirmed_flush_lsn FROM

SELECT subname, received_lsn, latest_end_lsn FROM pg_stat_subscription;
SELECT slot_name, active, wal_status, confirmed_flush_lsn FROM

In pg_replication_slots, wal_status should read reserved, and lost "means that this slot is no longer usable." Insert, update, and delete a row on the publisher, then check the subscriber.

What are the ways to consume Postgres logical replication?

The native subscription is the reference implementation, and six external consumers speak the same pgoutput protocol to reach other targets.

Consumer

Real strength

Documented limit

Native subscription

No software, transactional apply order

Same-named Postgres tables only, DDL and sequences by hand

Filament

Snapshot and stream from one exported snapshot, CRC32-C per batch

Postgres source and sink are beta

Debezium

Mature per-table Kafka topics with before and after images

Needs Kafka Connect or Debezium Server

PeerDB

Fast Postgres to ClickHouse path, full Postgres types

Other destinations deprecated, seven containers, AGPLv3

dlt

Python-native, any dlt destination

Library, you own scheduling and state

Airbyte

Large catalog, certified source

One destination per CDC source, raw plus final tables, ELv2

Fivetran

Managed, published DDL support table

Hosted only, MAR meter scales with rows

The native subscription applies changes "in the same order as the publisher," per the manual, but only into same-named Postgres tables. Debezium's Postgres connector streams every table to its own Kafka topic and expects "Kafka and Kafka Connect are installed," or the Kafka-less Debezium Server.

PeerDB's README now names Postgres to ClickHouse and Postgres to Postgres as its only maintained paths. Its LICENSE file is AGPLv3, whatever the README badge says. dlt's pg_replication source is a Python library over the same plugin. Airbyte's troubleshooting page says its xmin fallback "cannot support row deletions," and Fivetran's pricing meters per million monthly active rows.

Where does Filament fit?

Filament is a Go replication engine that consumes pgoutput and answers the criteria that break setups most often. Galaxy builds Filament. Its Postgres source is beta today, per the connector page, the stage where a connector runs end to end.

On snapshot consistency, the source docs say a table with no checkpoint is bootstrapped so that "the baseline and the change stream share one consistent point." That is the exported-snapshot mechanism from the manual, used as designed.

On slot hygiene, Filament owns the slot name per pipeline route, acknowledges "only the previously durable cursor," and drops retired slots "when a successor commits." Each steady-state run is a bounded catch-up cycle, so "continuous CDC is a re-request loop, so it can run on a schedule."

On deletes, the merge write mode applies them in order to maintain "a current replica," per the replication modes page. On integrity, the engine computes a CRC32-C checksum before every write and "the sink recalculates that checksum," per the integrity page. On footprint, it runs as one binary, a Go module, or a Helm chart, per the installation page, under an Apache 2.0 LICENSE.

Speed claims stay scoped. The public cohort measured full loads only, where Filament's 115 s on 298 million rows led every tool and Debezium's figure is its initial snapshot, not streaming. No public run measures CDC throughput, so this guide makes no speed claim for it.

How to run Postgres logical replication with Filament

Four more steps reuse the first two steps above and replace the subscription with a Filament pipeline. Filament creates the publication itself, so skip the manual CREATE PUBLICATION unless an administrator provisions one.

Step 6. Point the CLI at a Postgres-backed server
curl -fsSL https://getgalaxy.io/filament/install | sh
filament context add production --server

curl -fsSL https://getgalaxy.io/filament/install | sh
filament context add production --server

curl -fsSL https://getgalaxy.io/filament/install | sh
filament context add production --server

CDC runs "require a datastore with durable replication-stream admission," and the source docs name the PostgreSQL datastore for it. The Helm chart sets it with persistence.postgresql.dsn, and the quickstart runs the same stack locally with just infra and just dev.

Step 7. Create the CDC source and the sink
export POSTGRES_DSN="postgresql://repuser:change-me@publisher:5432/app"
export POSTGRES_SINK_DSN="postgresql://repuser:change-me@replica:5432/app"

filament source create production \
  --source-connector postgres \
  --source-connection-method url \
  --source-dsn-env POSTGRES_DSN \
  --source-replication cdc

filament sink create replica \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env

export POSTGRES_DSN="postgresql://repuser:change-me@publisher:5432/app"
export POSTGRES_SINK_DSN="postgresql://repuser:change-me@replica:5432/app"

filament source create production \
  --source-connector postgres \
  --source-connection-method url \
  --source-dsn-env POSTGRES_DSN \
  --source-replication cdc

filament sink create replica \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env

export POSTGRES_DSN="postgresql://repuser:change-me@publisher:5432/app"
export POSTGRES_SINK_DSN="postgresql://repuser:change-me@replica:5432/app"

filament source create production \
  --source-connector postgres \
  --source-connection-method url \
  --source-dsn-env POSTGRES_DSN \
  --source-replication cdc

filament sink create replica \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env

Connector fields become flags with a source- prefix and underscores become hyphens, per the CLI guide, so replication is --source-replication. Run filament source discover production to list tables with their primary keys, since CDC "requires wal_level=logical and a primary key on every selected table."

Step 8. Create a merge pipeline and run the first cycle
filament pipeline create users-cdc \
  --source production \
  --sink replica \
  --resources users,departments \
  --sync-mode cdc \
  --write-mode

filament pipeline create users-cdc \
  --source production \
  --sink replica \
  --resources users,departments \
  --sync-mode cdc \
  --write-mode

filament pipeline create users-cdc \
  --source production \
  --sink replica \
  --resources users,departments \
  --sync-mode cdc \
  --write-mode

The first run creates the slot with an exported snapshot and reads each table in full through it, under a publication named filament by default. Merge means "inserts and updates upsert, and deletes delete," per the Postgres sink docs.

Step 9. Schedule it and confirm the slot advances
filament run ls
filament run ls
filament run ls
SELECT slot_name, active, wal_status, confirmed_flush_lsn, safe_wal_size
FROM

SELECT slot_name, active, wal_status, confirmed_flush_lsn, safe_wal_size
FROM

SELECT slot_name, active, wal_status, confirmed_flush_lsn, safe_wal_size
FROM

A pipeline "has at most one schedule" with a cron and an overlap policy, per the pipelines API. A healthy run ends completed with a cursor.checkpoint_saved event, per the runs and recovery page, and confirmed_flush_lsn moves forward after each cycle.

What are common logical replication pitfalls?

Five failures cover most outages, and each has a documented fix that applies to every consumer.

  • An idle slot fills the disk because a stopped consumer never confirms, so drop unneeded slots and set max_slot_wal_keep_size, per the replication settings.

  • An update or delete fails on the publisher because the table has no replica identity, so add a primary key or run ALTER TABLE t REPLICA IDENTITY FULL, per the ALTER TABLE docs.

  • An update arrives without an unchanged TOAST column because DEFAULT identity omits it, which Filament treats as an error until the table is set to FULL, per its source docs.

  • Replication stops after a migration because DDL is not replicated, so apply additive changes to the subscriber first, per the restrictions page.

  • Newly published tables never arrive because adding one "will require an ALTER SUBSCRIPTION ... REFRESH PUBLICATION," per ALTER PUBLICATION, while Filament keeps its own publication current.

Which option fits which job?

Pick by the constraint that breaks first. The target system decides more than anything else.

Job

Pick

Why

Postgres replica, warehouse, or lake with verified batches

Filament

Exported-snapshot bootstrap, typed sinks, merge deletes

In-place major version upgrade or cutover

Native subscription

Built in, transactional apply

Event stream for many downstream services

Debezium

Per-table Kafka topics

Postgres to ClickHouse Cloud

PeerDB

Native ClickHouse integration

Python pipeline with custom transforms

dlt

Library, any dlt destination

No software to run, any source

Fivetran

Managed, MAR billed

For most teams keeping a replica, a warehouse, or a lake current from Postgres, Filament is the pick. It uses the exported snapshot as the manual intends, owns its slots, applies deletes in order, and verifies each batch. The native subscription is the narrow case, a one-time upgrade or cutover between two Postgres servers where you will manage schema and slots by hand.

Related guides

Frequently asked questions

What is logical replication in Postgres?

Logical replication is a Postgres feature that decodes committed inserts, updates, and deletes from the write-ahead log and streams them per table. A publication on the source names the tables, and a subscription or any client speaking the pgoutput protocol receives the changes in commit order.

What is the difference between logical and physical replication in Postgres?

Physical replication ships the WAL byte for byte, so the standby is an exact read-only copy of the whole cluster. Logical replication decodes the WAL into row changes, so you can pick tables, replicate across major versions, write to the subscriber, and feed non-Postgres systems.

How do I enable logical replication in PostgreSQL?

Set wal_level to logical and restart, because that parameter only applies at server start. Give a role the REPLICATION attribute and allow it in pg_hba.conf. Create a publication on the source, then create a subscription on the target or connect a CDC tool to the publication.

What is a replication slot and why does it fill the disk?

A replication slot is a server-side bookmark that records how far a consumer has read. Postgres keeps every WAL segment past that point until the consumer confirms it. If the consumer stops and the slot stays, WAL accumulates without limit by default, which is why the docs say to drop unused slots and why max_slot_wal_keep_size exists.

What does logical replication not replicate?

Schema changes and DDL, sequence values, large objects, views, materialized views, and foreign tables are not replicated. The published tables must already exist on the subscriber with the same fully qualified names. Truncates replicate but can fail when foreign keys point to tables outside the subscription.

Can Postgres logical replication capture deletes?

Yes. Deletes are written to the WAL like any other change, so every consumer of a publication that publishes delete sees them, as long as the table has a replica identity. Polling methods that read updated_at or the xmin column cannot see a hard delete, which is the main reason to prefer logical replication.

Which tool is best for consuming Postgres logical replication?

Filament fits most teams. It bootstraps the stream from one exported snapshot, runs each catch-up as a bounded cycle you can cron, verifies every batch with a CRC32-C checksum, and runs as one binary inside your network. Native CREATE SUBSCRIPTION covers a one-time cutover between two Postgres servers.

How do I set up Postgres CDC with Filament?

Set wal_level to logical, give a role the REPLICATION attribute, and point the Filament CLI at a server backed by its PostgreSQL datastore. Create a Postgres source with replication set to cdc, create a sink, and create a pipeline with merge as its write mode. The first run snapshots through the slot's exported snapshot and later runs replay the WAL.

More articles

Stay up to date with what we’re building

Stay up to date with what we’re building

Stay up to date with what we’re building

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?

Own your knowledge stack

Own your knowledge stack

Copyright © 2026 Galaxy. All rights reserved.