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)
Client sends COMMIT
Primary writes and flushes the commit record to its local WAL
Primary waits for acknowledgements from the standbys chosen by the mode (priority or quorum)
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).
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))
Standby
sync_state in pg_stat_replication
s1
sync
s2
potential
s3
potential
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))
Standby
sync_state
s1
quorum
s2
quorum
s3
quorum
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
Aspect
Priority (FIRST N)
Quorum (ANY N)
Who must ack
The N highest-priority connected standbys
Any N standbys from the list
List order
Matters (rank)
Ignored
sync_state
sync / potential
quorum
Commit latency
Slowest of the chosen N
N-th fastest of all M
Slow standby impact
Slows every commit if it’s in the sync set
Hidden if N others are faster
Standby failure
Next potential standby takes over
No change if N still respond
Which standby has latest data?
Known (the sync ones)
Not fixed — check LSNs
Failover
Simple — promote a sync standby
Promote the standby with the highest LSN
Best For
Fixed DR target, simple ops
Multiple 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)
Config
Waits For
Commit 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.
Level
Primary waits until
Data loss on primary crash
Standby read sees data?
Latency
off
Nothing (not even local flush)
Last few ms of commits
No
Lowest
local
Local WAL flush only
Possible (async to standbys)
No
Low
remote_write
Standby has written WAL to OS (not fsynced)
Only if primary and standby OS crash
No
Medium
on (default)
Standby has flushed WAL to disk
None
Not guaranteed
Higher
remote_apply
Standby has replayed WAL
None
Yes
Highest
Per-Transaction Override
-- Unimportant write (logs, analytics events): skip sync waitBEGIN;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