DB Internals AtlasPostgreSQL 18.6 Home About

Replication

Synchronous replication

With synchronous replication a COMMIT only returns once one or more chosen standbys confirm they have the transaction's WAL. synchronous_commit decides how far that confirmation must go.

In plain words

Normal streaming replication is asynchronous: the primary says "committed" as soon as the change is safe on its own disk, and the standby catches up a moment later. If the primary dies in that moment, the last few transactions are lost.

Synchronous replication closes that gap. You name standbys in synchronous_standby_names, and each COMMIT waits until enough of them say "I have it". synchronous_commit says what "I have it" means: handed to the standby's operating system (remote_write), written to disk (on), or already visible to queries on the standby (remote_apply).

The price is latency and an availability trade-off: while the configuration still requires acknowledgements from unavailable synchronous standbys, commits can wait indefinitely in SyncRep until a suitable standby responds, the configuration changes, or the wait is interrupted. Pick the settings below and watch which acknowledgement COMMIT waits for.

For DBAs and operators

Naming the standbys

Each standby identifies itself with application_name in its primary_conninfo (default: cluster_name, else walreceiver). Forms of synchronous_standby_names:

  • 's1' or 's1, s2': the old form, same as FIRST 1 (s1, s2).
  • FIRST 2 (s1, s2, s3): priority. The first two connected standbys in list order are sync; the rest are potential and take over automatically if one disappears.
  • ANY 2 (s1, s2, s3): quorum. Any two acknowledgements are enough; all are shown as quorum.
  • '*' matches any standby. An empty string (default) means asynchronous.

Only the primary reads it, changes need just a reload, and it can be changed at any time, which is how you "unblock" a hung cluster.

What COMMIT waits for

synchronous_commitlocal WAL flushsync standbys
offno (walwriter flushes later)no
localyesno
remote_writeyeswritten to standby OS (survives a standby PostgreSQL crash, not an OS crash)
on (default)yesflushed to standby disk
remote_applyyesreplayed, so visible to standby queries

It can be set per session or per transaction: SET LOCAL synchronous_commit = local for a bulk load, remote_apply for read-your-writes on replicas.

Monitoring

SELECT application_name, state, sync_state, sync_priority,
       write_lag, flush_lag, replay_lag
  FROM pg_stat_replication;
-- sessions stuck waiting:
SELECT pid, wait_event, now() - xact_start AS waiting, query
  FROM pg_stat_activity WHERE wait_event = 'SyncRep';
LOG: standby "s1" is now a synchronous standby with priority 1LOG: standby "s2" is now a candidate for quorum synchronous standby

When the sync standby is gone

Commits hang in wait event SyncRep; the change is already durable locally and visible to nobody else until the wait ends. Cancelling the backend does not roll back:

WARNING: canceling wait for synchronous replication due to user requestDETAIL: The transaction has already committed locally, but might not have been replicated to the standby.

Fixes: bring a standby back, list more candidates (FIRST 1 (s1, s2)), or as an emergency set synchronous_standby_names = '' and SELECT pg_reload_conf(). Tools like Patroni manage this automatically.

Synchronous replication does not make standbys read-your-writes consistent unless you use remote_apply, and it does not prevent split brain by itself.

For developers: inside the source

Where it lives

Almost everything is in syncrep.c, with the waiting side in the committing backend and the releasing side in the walsenders.

backend: RecordTransactionCommit()            access/transam/xact.c
  XLogFlush(XactLastRecEnd)                local flush first
  SyncRepWaitForLSN(XactLastRecEnd, true)  replication/syncrep.c
    SyncRepQueueInsert(mode); WaitLatch(…, WAIT_EVENT_SYNC_REP)
walsender: ProcessStandbyReplyMessage()
  SyncRepReleaseWaiters()
    SyncRepGetSyncRecPtr() → SyncRepWakeQueue(false, mode)
  • SyncRepWaitForLSN() returns at once unless SyncRepRequested() (max_wal_senders > 0 and synchronous_commit above local) and WalSndCtl->sync_standbys_status has SYNC_STANDBY_DEFINED. Otherwise, under SyncRepLock, it sets MyProc->waitLSN, syncRepState = SYNC_REP_WAITING and links the PGPROC into WalSndCtl->SyncRepQueue[mode], kept sorted by LSN. The mode is SYNC_REP_WAIT_WRITE, SYNC_REP_WAIT_FLUSH or SYNC_REP_WAIT_APPLY from remote_write, on and remote_apply. Two-phase commits wait through the same function (twophase.c).
  • SyncRepReleaseWaiters() runs in a walsender after each standby reply. SyncRepGetSyncRecPtr() picks the positions that are safe: for FIRST n (SYNC_REP_PRIORITY) SyncRepGetOldestSyncRecPtr() takes the minimum over the chosen standbys; for ANY n (SYNC_REP_QUORUM) SyncRepGetNthLatestSyncRecPtr() takes the n-th highest. New values go into WalSndCtl->lsn[mode]; SyncRepWakeQueue() marks waiters SYNC_REP_WAIT_COMPLETE and sets their latches. The "is now a synchronous standby" log lines are printed here too.
  • SyncRepGetCandidateStandbys() and SyncRepGetStandbyPriority() decide who is sync; each walsender's priority is stored in WalSnd.sync_standby_priority (shown as sync_priority).

Configuration and cancellation

check_synchronous_standby_names() parses the GUC with a small bison grammar (syncrep_gram.y, syncrep_scanner.l) into SyncRepConfigData (num_sync, syncrep_method, member_names). The checkpointer owns the shared "is sync rep defined" flag: on reload it calls SyncRepUpdateSyncStandbysDefined(), and when the list becomes empty it wakes every queued backend, which is why emptying the setting unblocks hung commits.

In the wait loop, QueryCancelPending produces the "canceling wait for synchronous replication due to user request" WARNING and returns (the commit record is already flushed); ProcDiePending emits "canceling the wait for synchronous replication and terminating connection due to administrator command" and the backend exits. Neither can roll back.

Experiment

  • src/test/recovery/t/007_sync_rep.pl covers priority, quorum and * lists and checks sync_state.
  • Stop the synchronous standby and watch pg_stat_activity.wait_event = 'SyncRep'; pg_stat_replication.sync_state shows the walsender view.
  • Docs source: doc/src/sgml/high-availability.sgml, section synchronous-replication.

Parameters that shape this

synchronous_standby_names synchronous_commit wal_sender_timeout wal_receiver_status_interval max_wal_senders application_name

Related topics

Streaming replication WAL sender WAL receiver Write-ahead log (WAL) WAL writer Replication slots Hot standby and recovery conflicts