Two Replication Systems, Not One With Options

Two Replication Systems, Not One With Options

There are two ways to replicate data in Postgres, and conflating them is how you end up with a failed cutover at 2 AM or a standby that’s been silently diverging for six months. Postgres includes two discrete replication engines under the hood. They are not merely different configurations of the same system. They are entirely separate mechanisms designed for opposing challenges. Physical replication ships WAL bytes to a standby that replays them block-for-block. Logical replication decodes WAL into row-level changes and applies them as SQL on a subscriber. They solve fundamentally different problems, and the decision isn’t about which is “better” — it’s about which tradeoffs you can actually live with.

<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:0.75rem;margin:2rem 0;"> <iframe src="https://www.youtube.com/embed/kTrfnb_TKG8" title="Logical vs Physical Replication: When Each Wins" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> </div>

▶ Watch on YouTube: Logical vs Physical Replication: When Each Wins

Two Replication Systems, Not One With Options

I walked through the high-level differences in the companion video, but this article goes deeper: exact SQL you can paste, the edge cases that don’t show up until production, and a decision framework that accounts for what happens six months after you set it up.

The thesis is simple: physical replication gives you a byte-identical standby for HA and DR with zero schema drift, but it’s same-version, whole-cluster, and read-only on the replica. Logical replication gives you row-level flexibility — cross-version upgrades, selective tables, writable subscribers — but you own DDL sync, sequences, and conflict resolution yourself. Pick wrong and you’re not just paying a performance tax. You’re paying with a cutover that fails because sequences wrapped, or a schema that drifted because someone added a column on the publisher and nobody ran it on the subscriber.

---

How Physical Replication Actually Works

How Physical Replication Actually Works

Physical replication is block-level streaming. The primary database instance writes write-ahead log (WAL) records to disk to ensure durability. Under physical replication, those exact WAL records are shipped over a network connection to a standby node. The standby reads the WAL records and applies them byte-for-byte to its own database cluster, resulting in an identical block-level copy. Same pages, same tuple layout, same everything.

[ Primary Database ] 
        │
   Writes WAL
        │
        ▼
[ WAL Sender ] ────(Network: Raw WAL Bytes)────► [ WAL Receiver ]
                                                       │
                                                 Applies blocks
                                                       │
                                                       ▼
                                              [ Standby Database ]

Because the standby replays block-level changes directly to its data files, it is a clone of the primary cluster. Every table, index, system catalog entry, and internal pointer matches the primary exactly. This is why the standby must run the exact same major version of Postgres. The WAL format is version-specific; a PG13 primary cannot ship WAL to a PG15 standby because the on-disk structures don’t match. There is no translation layer. Cross-version physical replication is unsupported and will fail, probably loudly, but the risk is that it fails quietly in a way you don’t notice until you try to fail over and the standby won’t start.

Because everything is replicated — every DDL, every vacuum, every bit of catalog churn — there is zero schema drift. You create a table on the primary, the standby gets it. You add a column, the standby gets it. There’s nothing to sync manually. That’s the big operational win, and it’s also the constraint: you can’t exclude a table. You replicate the whole cluster or nothing , which means if you need to replicate a single database or a handful of tables, you’re out of luck — that’s the domain of logical replication. Physical replication is the tool for high availability and disaster recovery where you want an exact, byte-for-byte standby that can take over with zero application changes. It’s also your only option when you need streaming read replicas that are kept perfectly in sync, because the standby replays every write, vacuum, and hint-bit change exactly as it happened on the primary.

Because the standby replays block-level changes directly to its data files, it remains in a consistent state that can be opened for read-only queries if you enable hot_standby = on. That’s the basis for read-scaling architectures where offloading analytical queries to one or more standbys reduces load on the primary. You set up a physical replication slot to prevent the primary from removing WAL segments the standby hasn’t consumed yet, but slots come with their own risk: if a standby goes away and you forget the slot, the primary’s WAL will pile up until the disk fills. Monitoring is not optional.

All of this works because you’re shipping raw WAL. The standby doesn’t know what a table is. It doesn’t parse SQL. It just replays pages. That’s why DDL, extensions, and even cockroach-level catalog corruption get replicated verbatim. It’s a double-edged sword: the standby is an exact copy, including the bad decisions you made five minutes before you realized them.

How Logical Replication Actually Works

Logical replication starts from the same WAL stream but adds a decoding layer. The primary writes WAL as usual, but a logical decoding plugin (the built-in pgoutput in core PostgreSQL) transforms those binary changes into a structured stream of row-level operations — INSERT, UPDATE, DELETE — along with schema information. The publisher then sends these logical change records to a subscriber that applies them as standard SQL statements against its own tables.

Logical Replication Architecture

[ Primary Database ] 
        │
   Writes WAL
        │
        ▼
[ Logical Decoding ] ▶ Change records (row-level)
        │
        ▼
[ Logical Replication Slot + WAL Sender ]
        │
(Logical replication protocol)
        │
        ▼
[ Subscriber ] ▶ Applies SQL (INSERT/UPDATE/DELETE)

The moment you move from block-level WAL shipping to row-level change application, you gain a set of capabilities that physical replication simply can’t provide:

But these gains come with a price that many teams underestimate until the pager goes off. Because logical replication applies changes by replaying SQL, it is fundamentally slower per transaction than physical WAL shipping. High-throughput OLTP workloads can create replication lag measured in minutes, not milliseconds, if the subscriber can’t keep up. Moreover, DDL is not replicated automatically. Adding a column to a table on the publisher does not create it on the subscriber. You must run that ALTER TABLE on every subscriber yourself, and in the correct order relative to the data changes that reference the new column. Get the timing wrong, and the subscriber will error out and pause replication until you fix it.

Sequence values suffer the same fate. Logical replication replicates the rows inserted by nextval(), but it does not synchronize the sequence’s current value. After a failover to a subscriber, pg_dump-issued sequences will be behind reality, and you’ll get duplicate-key violations the moment the first INSERT hits the old sequence range. The only safe way to handle sequences in a logical failover is to manually advance them after promotion, or to use a UUID key that doesn’t depend on sequence state.

And then there’s conflict resolution. Because the subscriber is writable, a local INSERT on the subscriber and a replicated INSERT from the publisher can create a primary key conflict. Logical replication reacts to this by either stopping the replication entirely (default) or by skipping the conflicting change if you set a conflict resolution policy (pglogical extension on older versions, or native REPLICA IDENTITY tricks). You are the conflict police now, and your application had better be ready to handle the fallout.

Picking the Right Replication System for the Job

The decision between physical and logical replication is not about picking the “better” technology. It’s about acknowledging which problems you actually need to solve and which tradeoffs you are willing to live with long-term. Here’s the framework I’ve seen hold up after dozens of production deployments:

No matter which path you choose, invest early in monitoring replication lag and slot retention. A replication slot that balloons to hundreds of gigabytes because the subscriber is unreachable will crash your primary just as surely as a misconfigured max_wal_size. For a deeper discussion on monitoring strategies and real-world war stories, the community at MyDBA trades notes on exactly these operational traps.

Physical and logical replication are two separate tools built into PostgreSQL for two separate classes of problem. Treating them as options on the same configuration page is the fastest way to learn the difference the hard way. Choose with the scars of those who went before you, and remember: there is no such thing as a free replica.