Overview

  • Context: Synchronous replication needs a rule for which replicas the primary waits for before a commit returns
  • Two Modes: Priority mode (FIRST N) and Quorum mode (ANY N)
  • Where Configured: synchronous_standby_names in the primary’s postgresql.conf
  • Key Idea: Priority picks specific standbys by rank. Quorum accepts any N standbys out of a group
  • Related: See Synchronous vs Asynchronous Replication and Replication Postgres Example (which uses first 1 (standby_1))

How a Synchronous Commit Works (Postgres)

  1. Client sends COMMIT
  2. Primary writes and flushes the commit record to its local WAL
  3. Primary waits for acknowledgements from the standbys chosen by the mode (priority or quorum)
  4. Only then does COMMIT return to the client, and only then do other sessions see the change

Important: While the backend waits in step 3, the transaction still holds its row locks and is not yet visible to other sessions. This is why sync replication affects more than one client’s latency (see Performance Impact).

Priority Mode — FIRST N

Syntax

synchronous_standby_names = 'FIRST 1 (s1, s2, s3)'
synchronous_standby_names = 'FIRST 2 (s1, s2, s3)'

-- Legacy syntax (no keyword) = FIRST 1
synchronous_standby_names = 's1, s2, s3'

How It Works

  • Ordered List: Position in the list = priority. s1 > s2 > s3
  • Sync Set: The top N connected standbys are the synchronous ones
  • Waiting: Commit waits for those specific N standbys. Faster lower-priority standbys don’t count
  • Others: Remaining standbys are potential, ready to be promoted to sync
  • Failover of Sync Role: If s1 disconnects, s2 automatically becomes synchronous

Example (FIRST 1 (s1, s2, s3))

Standbysync_state in pg_stat_replication
s1sync
s2potential
s3potential

Characteristics

  • Predictable: You always know which standby is guaranteed to have the latest commit
  • Simple Failover: Promote the sync standby — it is guaranteed to be up to date
  • Latency Coupling: Commit speed is tied to the chosen standby, even if it is slow
  • Topology-aware: Good when you deliberately want a specific node (for example, the same-AZ DR node) to be the guaranteed copy

Quorum Mode — ANY N

Syntax

synchronous_standby_names = 'ANY 1 (s1, s2, s3)'
synchronous_standby_names = 'ANY 2 (s1, s2, s3)'
synchronous_standby_names = 'ANY 2 (*)'        -- any 2 of all connected standbys

How It Works

  • Unordered Set: No priority — all listed standbys are equal
  • Waiting: Commit waits until any N of the listed standbys acknowledge
  • First N Responders Win: The fastest N replies decide the commit, whichever standbys they are
  • All Listed Standbys: Show sync_state = quorum

Example (ANY 2 (s1, s2, s3))

Standbysync_state
s1quorum
s2quorum
s3quorum
  • Commit 1 may be acked by s1 + s2, commit 2 by s2 + s3. Either way, at least 2 copies exist

Characteristics

  • Resilient: One slow or dead standby doesn’t slow down commits, as long as N others respond
  • Lower Tail Latency: Commit waits for the N-th fastest responder, not a fixed node
  • Harder Failover: No single standby is guaranteed to have every commit. On failover you must pick the standby with the highest LSN (tools like Patroni do this)
  • Math: With ANY N out of M standbys, any group of M - N + 1 standbys includes at least one with every committed transaction

Priority vs Quorum — Differences

AspectPriority (FIRST N)Quorum (ANY N)
Who must ackThe N highest-priority connected standbysAny N standbys from the list
List orderMatters (rank)Ignored
sync_statesync / potentialquorum
Commit latencySlowest of the chosen NN-th fastest of all M
Slow standby impactSlows every commit if it’s in the sync setHidden if N others are faster
Standby failureNext potential standby takes overNo change if N still respond
Which standby has latest data?Known (the sync ones)Not fixed — check LSNs
FailoverSimple — promote a sync standbyPromote the standby with the highest LSN
Best ForFixed DR target, simple opsMultiple equal replicas, low tail latency

Impact on Query Performance

Writes (where the cost is)

  • Commit Latency: Every commit adds about one network round trip plus the standby’s WAL write or flush time
    • Same AZ: ~0.5–2 ms per commit
    • Cross-region: ~30–100+ ms per commit
  • Paid Once per Transaction: The wait is only at COMMIT, not per statement. 1 transaction with 1000 inserts ≫ 1000 auto-commit inserts
  • Single-connection Throughput: Roughly 1 / commit_latency. At 40 ms per commit, a single connection can do at most ~25 commits/sec
  • Concurrency Helps: Many concurrent sessions share WAL flushes (group commit), so total throughput scales with more connections even though each commit is slower

Lock Contention (hidden cost)

  • Locks Held Longer: Row locks are held until the sync ack arrives
  • Hot Rows: A frequently updated row (counter, balance, inventory) can be updated at most ~1 / commit_latency times per second
  • Effect: Other transactions queue behind it, so sync replication can make unrelated-looking queries slow if they touch the same hot rows

Priority vs Quorum Latency Example

Standbys: A = 1 ms, B = 2 ms, C = 40 ms (cross-region)

ConfigWaits ForCommit Latency
FIRST 1 (A, B, C)A~1 ms
FIRST 1 (C, A, B)C~40 ms
FIRST 2 (A, C, B)A and C~40 ms
ANY 1 (A, B, C)Fastest = A~1 ms
ANY 2 (A, B, C)2 fastest = A, B~2 ms
ANY 3 (A, B, C)All~40 ms
  • Takeaway: Quorum automatically avoids the slow node. Priority only does if you rank it low

Reads

  • Reads on Primary: Not slowed down directly. They only wait if they’re blocked on locks held by commits waiting for acks
  • Reads on Standby: Can still be stale with the default setting — on only guarantees the WAL is flushed on the standby, not replayed
  • Read-your-writes on Standby: Needs synchronous_commit = remote_apply (more latency)

synchronous_commit Levels (the second setting)

synchronous_standby_names chooses which standbys to wait for. synchronous_commit chooses how far they must go before acking.

LevelPrimary waits untilData loss on primary crashStandby read sees data?Latency
offNothing (not even local flush)Last few ms of commitsNoLowest
localLocal WAL flush onlyPossible (async to standbys)NoLow
remote_writeStandby has written WAL to OS (not fsynced)Only if primary and standby OS crashNoMedium
on (default)Standby has flushed WAL to diskNoneNot guaranteedHigher
remote_applyStandby has replayed WALNoneYesHighest

Per-Transaction Override

-- Unimportant write (logs, analytics events): skip sync wait
BEGIN;
SET LOCAL synchronous_commit = local;
INSERT INTO page_views ...;
COMMIT;
  • Use Case: Keep critical tables sync, but let low-value writes skip the wait
  • Can also be set per user or role: ALTER ROLE analytics SET synchronous_commit = local;

Failure Behaviour & Gotchas

  • Not Enough Standbys = Writes Hang: If fewer than N sync standbys are available, commits block forever. Postgres does not fall back to async automatically (MySQL semi-sync does, after rpl_semi_sync_master_timeout)
    • Fix: Have more standbys than N (e.g. ANY 1 of 2, or FIRST 1 of 2), or let an HA tool (Patroni) change the setting
  • Cancelled Waits: If the client cancels or times out while waiting, Postgres warns: “canceled wait for synchronous replication… the transaction has already committed locally”. The data is committed on the primary but may not be on any standby
  • Same Names: application_name in the standby’s primary_conninfo must match a name in the list (as in the Postgres example: application_name=standby_1)
  • Reload Only: Changing synchronous_standby_names only needs SELECT pg_reload_conf();, not a restart

Monitoring

SELECT application_name, state, sync_state, sync_priority,
       write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
  • sync_state: sync, potential, quorum, or async
  • flush_lag: The lag that matters for synchronous_commit = on
  • replay_lag: How stale reads on the standby are

Decision Framework

Choose Priority (FIRST N) When

  • One specific standby must always hold the latest data (DR node)
  • You want simple, predictable failover
  • Standbys have similar latency, or you can rank the fast ones first

Choose Quorum (ANY N) When

  • You have 3+ similar standbys and want durability without being slowed down by the slowest one
  • Standbys are spread across AZs or regions with uneven latency
  • An HA tool handles failover (picking the highest LSN)

Summary

  • Priority (FIRST N): Waits for the top-N ranked standbys. Predictable, simple failover, but bound by those nodes’ speed
  • Quorum (ANY N): Waits for any N standbys. Lower and steadier latency and tolerates slow nodes, but failover must check LSNs
  • Performance: Cost is per commit (network RTT + standby flush), and longer lock hold times hurt hot rows the most
  • Tuning Knobs: N, standby placement, synchronous_commit level, per-transaction overrides
  • Gotcha: Too few available standbys = writes block. There is no automatic async fallback in Postgres