Concepts covered
PostgreSQL 19 replication - WAIT, sequences and subscriptions
A read scale-out cluster has always had one uncomfortable property: a write commits on the primary,
the client is told it succeeded, and a read routed to a standby a millisecond later cannot see it.
remote_apply synchronous commit fixes this at a price the documentation describes bluntly - much
larger commit delays. PostgreSQL 19 adds a cheaper alternative: do the write on the primary, then
make the standby prove it has caught up before serving the read. That is the WAIT command.
WAIT: read-your-writes on a standby
WAIT is a top-level SQL command that blocks until WAL reaches a target LSN, with a mode deciding
what “reached” means:
| Mode | Waits for | Where it runs |
|---|---|---|
standby_replay (default) |
LSN replayed - visible to queries - on the standby | During recovery only |
standby_write |
WAL written to the standby’s disk (not necessarily flushed) | During recovery only |
standby_flush |
WAL flushed to durable storage on the standby | During recovery only |
primary_flush |
WAL flushed to disk on the primary | Primary only |
The pattern from the documentation: write on the primary, capture the WAL position, then make the replica catch up to it before reading:
-- on the primary:
UPDATE movie SET genre = 'Dramatic' WHERE genre = 'Drama';
SELECT pg_current_wal_insert_lsn(); -- returns, for example, 0/0306EE20
-- on the standby:
WAIT FOR LSN '0/0306EE20'; -- default mode standby_replay
-- status: success
SELECT * FROM movie WHERE genre = 'Drama'; -- now guaranteed empty
pg_current_wal_insert_lsn() rather than a commit LSN is deliberate - synchronous_commit may be
off, so the insert position is the safe bound. TIMEOUT bounds the wait (milliseconds, or a
string with a unit; zero, the default, waits forever) and NO_THROW converts timeout and promotion
outcomes into a status return value of timeout or not in recovery instead of an error - the
right shape for an application that would rather fall back to the primary than fail:
WAIT FOR LSN '0/0306EE20' WITH (MODE 'standby_write', TIMEOUT '100ms', NO_THROW);
The restrictions are real and worth reading before building on this. WAIT cannot run in a
function, procedure, DO block, or once the transaction holds a snapshot - so it must be the first
statement of a transaction, or issued outside one entirely. During recovery it is also rejected if
the session holds a lock and the target LSN is not yet reached: a held lock can make the startup
process wait on this session while this session waits on replay, a deadlock that deadlock detection
cannot see. Wait first, then take locks.
Text equivalent
- Application writes on the primary; the write commits and the WAL record enters the stream.
- Application captures
pg_current_wal_insert_lsn()on the primary. - On the standby,
WAIT FOR LSN ‘lsn’ WITH (TIMEOUT ‘100ms’, NO_THROW)blocks until replay passes the LSN, then returnssuccess(ortimeout/not in recovery). - The read runs on the standby, guaranteed to see the write.
Logical replication finally covers sequences
Until 19, logical replication moved table data and left sequences behind - the subscriber’s
nextval kept issuing values the publisher had already used, which is an outage waiting to happen
after a failover. 19 adds sequence synchronisation:
-- publisher:
CREATE PUBLICATION pub1 FOR ALL SEQUENCES;
-- subscriber:
CREATE SUBSCRIPTION sub1
CONNECTION 'host=pub.example.com dbname=appdb application_name=sub1'
PUBLICATION pub1;
With copy_data = true (the default) a sequence synchronisation worker copies the initial values
at subscription creation and exits when done; ALTER SUBSCRIPTION ... REFRESH PUBLICATION picks up
newly added sequences, and ALTER SUBSCRIPTION ... REFRESH SEQUENCES re-syncs the values of all
known sequences. The worker count is bounded by max_sync_workers_per_subscription.
Two diagnostics matter:
pg_get_sequence_data('s1')on the publisher returnslast_value,is_calledandpage_lsn. Comparepage_lsnagainstpg_subscription_rel.srsublsnon the subscriber to detect drift.- The warning in the documentation is worth memorising: a sequence caches a block of values
(typically 32) before writing a new WAL record, so its page LSN only advances when a whole cached
batch is consumed. Drift inside one cached block is invisible to the LSN comparison - run
REFRESH SEQUENCESon a schedule or after bulk sequence activity, not just when the LSNs differ.
Sequence synchronisation requires the publisher to run PostgreSQL 19 or later; a 19 subscriber against an older publisher gets nothing.
Text equivalent
- Publisher publishes
FOR ALL SEQUENCES; a synchronisation worker copies initial values at subscription creation. REFRESH PUBLICATIONpicks up newly added sequences;REFRESH SEQUENCESre-syncs values of all known sequences.- Sequence caches advance the page LSN only per consumed block, so drift inside one block is invisible - schedule refreshes.
Publications and subscriptions: the rest of the changes
| Change | Syntax or setting | Notes |
|---|---|---|
EXCEPT clause |
CREATE PUBLICATION p FOR ALL TABLES EXCEPT (TABLE audit_log) |
Exclusion survives rename and SET SCHEMA; for partitioned tables name the root, which excludes all partitions. A table excluded in one publication but included in another is still replicated |
| Conflict detection retention | retain_dead_tuples = true on CREATE/ALTER SUBSCRIPTION |
Keeps dead tuples, commit timestamps and origins on the subscriber so update_deleted conflicts can be detected; creates a physical slot named pg_conflict_detection on the subscriber. Requires track_commit_timestamp on the subscriber to actually detect update_deleted |
| Retention bound | max_retention_duration (ms, default 0 = keep until no longer needed) |
Only evaluated while the apply worker runs; the docs warn a non-zero value can remove detection information early and produce wrong results |
| Connection via foreign server | CREATE SUBSCRIPTION s SERVER pg19_pub PUBLICATION pub1 |
Uses postgres_fdw connection parameters; needs a connection_function on the FDW, a user mapping for the subscription owner, and USAGE on the server |
| Per-subscription timeout | wal_receiver_timeout = '60s' in the WITH clause |
Default -1 defers to the global setting; previously only global/role/database settings applied |
| Shutdown drain limit | wal_sender_shutdown_timeout (default -1, wait forever; 0 = do not wait) |
Limits how long shutdown waits for receivers to catch up; settable in primary_conninfo and in a subscription’s options=-c... conninfo |
retain_dead_tuples carries the subtle trap: conflict-detection information cannot be purged while
the subscription is disabled or its apply worker is failing, so it accumulates - watch the xmin of
the pg_conflict_detection slot in pg_replication_slots, and disable the option if a subscription
will be down for a while. Detection of update_deleted is also not guaranteed for changes that
originated on a different node, only for local writes.
wal_level = replica can now serve logical replication
Setting wal_level = logical used to require a restart, which is why many clusters ran replica
and could not add a publication without a maintenance window. In 19, when wal_level is replica
the server can raise the effective level to logical on demand - the release notes describe this as
automatic enablement of logical replication when needed. Three places report the effective level:
the effective_wal_level setting, pg_controldata, and pg_control_checkpoint().
The trade-off is unchanged: logical decoding writes more WAL than replica, so the bill arrives
only once someone actually creates a slot.
New and renamed monitoring surfaces
pg_stat_recovery- new system view with one row reporting standby recovery state:promote_triggered,pause_state,last_replayed_end_lsn,replay_end_lsn,recovery_last_xact_time(the commit time on the primary), andcurrent_chunk_start_time. It returns rows only in recovery and only topg_read_all_statsholders.pg_stat_subscription_stats.sync_error_countrenamed tosync_table_error_count, withsync_seq_error_countadded alongside for sequence synchronisation errors, andupdate_deletedfor conflicts detected underretain_dead_tuples.pg_stat_replication_slotsgainsmem_exceeded_count(timeslogical_decoding_work_memwas exceeded) and slot-sync skip columns (slotsync_skip_count,slotsync_last_skip,slotsync_skip_reason);pg_stat_wal_receiver.statusgains aconnectingvalue.
The full runbook of these and the rest of 19’s monitoring additions is on the monitoring page.
What changed, and in which release
| Change | PG 18 | PG 19 |
|---|---|---|
| Read-your-writes on a standby | synchronous_commit = remote_apply, expensive |
WAIT FOR LSN on the standby |
| Sequences in logical replication | Not replicated; drifted silently | FOR ALL SEQUENCES, sync worker, REFRESH SEQUENCES |
Excluding tables from FOR ALL TABLES |
Not possible | EXCEPT (TABLE ...) |
wal_level = replica |
Logical replication unavailable without restart | Logical capability enabled on demand (effective_wal_level) |
wal_receiver_timeout |
Global, role, or database only | Per subscription |
| Conflict detection on subscriber | Not available | retain_dead_tuples + max_retention_duration |
| Subscription connection string | CONNECTION 'conninfo' only |
SERVER name via postgres_fdw |
Where it breaks
- WAIT inside a transaction that already took a lock - rejected with an error during recovery;
the fix is ordering,
WAITfirst. - WAIT on timeout without NO_THROW - raises an error that aborts the transaction; use
NO_THROWand check the status whentimeoutis a handleable outcome. - Sequence sync against a pre-19 publisher - does nothing; there is no fallback.
retain_dead_tupleson a stalled subscription - retention keeps accumulating because purging needs a running apply worker; watchpg_replication_slotsforpg_conflict_detection.max_retention_durationset too low - the documentation warns premature removal causes incorrect conflict detection, which is worse than no detection.
What this is not for
- Guaranteeing durability of the write itself -
WAITruns after commit; it makes the standby catch up, not the write safer. That remainssynchronous_commit’s job. - Replacing synchronous replication for correctness-critical reads -
standby_writeandstandby_flushgive weaker guarantees thanstandby_replay; onlystandby_replaymeans the row is actually visible. - Sub-second lag budgets without thought - WAIT turns standby lag into client-visible latency; under a lag spike the choice is a timeout, a fallback to the primary, or a stale read.
PostgreSQL release support
5 supported| Release | Latest | Released | End of life | Status |
|---|---|---|---|---|
18 | 18.6 | Sept 2025 | Nov 2030 | Supported |
17 | 17.11 | Sept 2024 | Nov 2029 | Supported |
16 | 16.15 | Sept 2023 | Nov 2028 | Supported |
15 | 15.19 | Oct 2022 | Nov 2027 | Supported |
14 | 14.24 | Sept 2021 | Nov 2026 | Supported |
13 | 13.23 | Sept 2020 | Nov 2025 | End of life |
12 | 12.22 | Oct 2019 | Nov 2024 | End of life |
11 | 11.22 | Oct 2018 | Nov 2023 | End of life |
Data as of 2026-10-01 from endoflife.date. Refresh with node scripts/fetch-versions.mjs.
What to watch
Specific things that would change what this page concludes.
- Whether WAIT's lock-and-snapshot restrictions are relaxed before GA - they currently make it unusable inside functions, procedures and DO blocks
Where PostgreSQL sits
How this component relates to the rest of the stack. Items without a link are not covered here yet.