PostgreSQL 16 moves logical decoding onto standby servers
Standby decoding changes where replication work runs, while parallel apply changes how subscribers process large transactions. PostgreSQL 16 also exposes I/O statistics for understanding those workloads.
Logical replication work does not have to run on the PostgreSQL primary anymore. PostgreSQL 16 allows a standby to decode logical changes and send them downstream. That changes where the database spends resources on a change stream, and gives operators another choice when the primary already has enough work serving writes. It also creates a dependency on the catalog history that the standby needs to interpret those changes.
The September 14 release announcement presents standby logical replication alongside parallel application of large transactions. These changes address different parts of a replication path. Standby decoding moves publishing work, while parallel apply changes processing at the receiving end. Describing both as faster replication misses the topology decision introduced by the first and the worker configuration required by the second.
I default to PostgreSQL for relational systems and value being able to inspect its behavior. These additions interest me because they expose specific decisions about resource placement. The new pg_stat_io view provides another way to understand database work while choosing where replication processing belongs.
A standby decodes storage changes into logical records
The logical-decoding documentation at the 16.0 tag describes decoding as converting write-ahead-log records into an application-oriented representation. The write-ahead log describes changes at the storage level. An output plugin turns those changes into the form a consumer expects, such as a stream of tuples.
A logical slot tracks a stream for one database. It persists independently of the connection consuming it, and separate slots can track separate consumers at different positions. Only one receiver can consume a particular slot at a time. Moving decoding to a standby does not remove these state-management requirements, because the consumer still needs a coherent position in the database change stream.
Version 16 permits creating that logical slot on a hot standby. The release announcement gives workload distribution as the use case, with a standby taking on logical publishing instead of a busy primary. Upstream, the primary still produces the underlying changes. The standby receives the physical stream and performs the downstream logical interpretation.
That placement distinguishes physical recovery from the logical representation delivered to the next system. It creates a useful processing location without implying that the standby has become an independent source of application writes. The new capability concerns extracting and publishing the changes available through its recovery stream.
Catalog retention connects the standby to the primary
Logical decoding needs catalog information to understand recorded changes. The tagged documentation recommends hot_standby_feedback so that the primary does not vacuum catalog rows the standby still requires. If required rows disappear anyway, the logical slot becomes invalid. The decoder cannot continue safely merely because the corresponding WAL bytes remain present.
The same documentation strongly recommends a physical replication slot between the primary and standby. Without that slot, feedback protects the relevant state only while the connection remains alive. A restart can break that connection, leaving the primary unaware of the standby's catalog-retention requirement. That is a specific reason the topology needs more than enabling a feedback setting.
Reducing the primary's wal_level below logical also invalidates existing logical slots on the standby once it detects the change in the WAL stream. The setting on the primary therefore remains part of the logical publisher's requirements even though the logical consumer connects to another machine. Moving execution does not make the source information optional.
This is a tradeoff in retained state as well as CPU placement. Slots keep required WAL and catalog rows from being removed, including when a consumer is disconnected. The documentation notes that this consumes storage and that unused slots should be dropped. A standby that publishes changes remains coupled to upstream retention policy through the information it needs to decode them.
Slot creation can wait for primary activity
Creating a logical slot requires information about currently running transactions. The primary has that information directly, while a standby obtains it from the primary. Consequently, slot creation on an idle system can wait for activity that makes the required state available. The tagged documentation identifies pg_log_standby_snapshot on the primary as a way to accelerate that process.
This explains an otherwise surprising operational symptom. An idle primary does not necessarily mean immediate slot creation on its standby, because the problem is obtaining transaction-state information rather than finding spare processor time. Understanding the dependency gives the wait a more precise explanation than treating it as generic replication lag.
Crash recovery also affects delivery semantics. The documentation says slot positions persist at checkpoints, so a crash can cause recent changes to be sent again after restart. Logical decoding clients must avoid harmful effects from processing a message twice. Moving the slot to a standby does not create an exactly-once guarantee for an external consumer.
Parallel apply is an explicit subscription choice
The 16.0 subscription documentation defines three streaming modes. With the default off, the publisher fully decodes a transaction before sending it as a whole. Selecting on means the subscriber stores incoming streamed changes in temporary files and applies them after receiving the commit. For parallel, available parallel apply workers can apply incoming changes directly.
A lack of an available parallel worker does not require abandoning the transaction. The documented behavior falls back to temporary files and applies the changes after commit. The mode therefore describes an execution opportunity subject to worker availability, rather than a promise that every streamed transaction always uses another worker.
The distinction between streaming and committing remains essential. Receiving changes while a transaction is in progress does not authorize presenting an uncommitted transaction as committed application data. The subscription option changes how work can proceed before the publisher's transaction completes. Its purpose is to reduce the serial work remaining at the receiver for large transactions.
My preference is to keep those stages separate when evaluating replication behavior. Publishing location, transfer progress and subscriber apply work contribute different delays. A faster decoder cannot by itself resolve an apply bottleneck, and a parallel apply configuration cannot recreate catalog state that an upstream server has removed.
I/O statistics identify work by process and context
The monitoring documentation pinned to 16.0 defines pg_stat_io as cluster-wide statistics grouped by backend type, I/O object and context. Those dimensions make it possible to distinguish kinds of database activity instead of assigning every read or write to one undifferentiated total. Impossible combinations do not appear as rows.
A minimal query can inspect the operation counters without assuming that timing collection is enabled. These columns belong to the new view in this release.
SELECT backend_type, object, context, reads, writes, op_bytes
FROM pg_stat_io;
The counters have units. Each read or write operation uses the size specified by op_bytes, which the release documentation relates to the build's block size for relation operations. Multiplying operation count by the relevant unit answers a different question from counting operations alone. This matters when translating database counters into a claim about transferred data.
Timing columns require track_io_timing to be enabled. The documentation also warns about comparing timing and operation totals when timing was enabled for only part of the interval since the last reset. A zero timing total does not automatically establish that the database completed its recorded I/O without spending time on it.
The new view is not a complete account of every filesystem operation. At release, it tracks relation I/O but excludes relation operations that bypass shared buffers, giving a tablespace move as an example. Some valid rows contain null counters for operations that the backend or object never performs. Those nulls differ from a measured count of zero.
Other changes act on distinct workload stages
The release announcement also identifies parallel full and right joins, improved plans for aggregates using DISTINCT or ORDER BY, and incremental sorting for SELECT DISTINCT. Those are planner and execution changes for query workloads. Bulk loading through COPY receives separate improvements, while logical subscribers gain B-tree lookup options for tables without primary keys.
Together these changes broaden the available places to reduce work, but each remains tied to a concrete operation. A deployment can now publish logical changes from a standby and use parallel apply at a subscriber while inspecting process-level I/O counters on each server. The source and subscriber still need their own resource accounting, because distributing replication work changes which machine performs it without removing the work itself.