Runbook: Add or Remove a Synchronous Standby
1 · Prerequisites
Confirm every item is in place before any state change.
- At least two healthy standbys if synchronous replication is being enabled, because a single synchronous standby makes the primary depend on it
- The application_name of each standby as it appears in pg_stat_replication, since synchronous_standby_names matches on that
- A written statement of which property is being bought — durability — and an acknowledgement that it is being traded against availability
- Agreement from the service owner, because this changes the cluster availability characteristics and is not a database-team decision alone
- A measured baseline of commit latency and throughput before the change
- A tested procedure for removing the requirement quickly, because that is the emergency action when a standby is lost
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · Confirm how many standbys are streaming, and their names.
SELECT application_name, state, sync_state, sync_priority, pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS bytes_behind FROM pg_stat_replication ORDER BY application_name; - · **Confirm each standby's
application_nameis meaningful.** The default iswalreceiver, identical for every standby, andsynchronous_standby_namescannot distinguish them. Fix the names before configuring anything. - · Count the standbys against the quorum you intend.
ANY 1 (a, b)needs two standbys to survive losing one. With one standby, every commit hangs when it goes away, indefinitely, by design. - · Measure the baseline. Commit throughput and latency, under representative load, before the change. Measured on one cluster: 824 tps at
synchronous_commit = localagainst 552 aton— a cost of roughly 25 to 33 percent for waiting on the standby. - · Confirm the standbys can keep up. A synchronous standby that lags makes every commit wait for it.
bytes_behindunder load, not at rest, is the number that matters. - · Check the network path between primary and standbys. Synchronous commit adds a round trip to every transaction, so latency between the hosts becomes latency in the application.
- · Write down the emergency action and where it is documented:
ALTER SYSTEM SET synchronous_standby_names = ''; SELECT pg_reload_conf();. Somebody will need it at 21:40.
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Choose the mode explicitly.
ANY N (list)is a quorum: any N of the listed standbys must acknowledge.FIRST N (list)is priority-ordered.ANY 1 (a, b)is the usual right answer, because it survives losing either standby. - 2Quote names containing a hyphen inside the string literal.
synchronous_standby_namesparses its value as a name list.'db-standby-01'producesERROR: invalid value for parameter "synchronous_standby_names" ... syntax error at or near "-". The correct form is'"db-standby-01"'. - 3Set the value and reload.
ALTER SYSTEM SET synchronous_standby_names = 'ANY 1 (sync1, sync2)'; SELECT pg_reload_conf();This is reloadable and needs no restart. - 4Confirm the standbys took the expected role.
SELECT application_name, state, sync_state, sync_priority FROM pg_stat_replication ORDER BY application_name;A quorum configuration reportssync_state = 'quorum'; a priority configuration reportssyncandpotential. - 5**Choose
synchronous_commitdeliberately, and know what each level waits for.**remote_writewaits for the standby to write;onwaits for it to flush to disk;remote_applywaits for it to apply, which is what makes a read on the standby immediately consistent. Each level costs more than the last. - 6**Set
synchronous_commitper role or per transaction where the workload allows it.** A bulk-loading job that can tolerate loss can useSET LOCAL synchronous_commit = 'local'without weakening the guarantee for everybody else. - 7Measure again under the same load and compare against the baseline. Report the cost honestly; a 25 percent throughput reduction is a real price and the service owner agreed to pay it for a reason.
- 8Test the failure mode deliberately, in a window. Stop one standby and confirm commits continue. Measured on a quorum of two with one stopped:
INSERT 0 1in 0.022 seconds, andpg_stat_replicationshowing the remaining standby still inquorum. - 9Test the unsatisfiable case too. Stop the second standby and confirm commits hang. This is the guarantee being honoured, not a bug — and a team that has seen it will not restart the primary during the next incident.
- 10Learn to recognise the hang.
SELECT pid, state, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event = 'SyncRep';returns the waiting backends. Reads continue working throughout, and no error appears in the server log, because nothing has failed. - 11Add the standby-maintenance step. Removing a standby from
synchronous_standby_namesbefore rebooting it costs one reload and removes an entire class of incident. - 12**Alert on
wait_event = 'SyncRep'and on connected synchronous standbys falling below the quorum requirement.** The second fires before the last one goes, which is the alert that gives you a window.
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓
pg_stat_replicationshows the intended standbys with the intendedsync_state—quorumfor a quorum configuration,syncandpotentialfor a priority one. - ✓Commit latency under representative load is measured and recorded against the baseline.
- ✓Stopping one standby leaves commits working, confirmed by an actual write and its timing rather than by reasoning about the configuration.
- ✓Stopping every synchronous standby makes commits hang at
IPC/SyncRep, confirmed once in a window so the team recognises it. - ✓Reads continue to work during that hang, which is the observation that distinguishes this from a database failure.
- ✓
SELECT count(*) FROM pg_stat_activity WHERE wait_event = 'SyncRep';is zero during ordinary operation. - ✓The emergency action — clearing
synchronous_standby_namesand reloading — has been exercised and its effect measured. On one cluster the next commit completed in 0.021 seconds. - ✓The standby-maintenance runbook includes the step that removes a standby from the synchronous set before it is rebooted.
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶Clear the requirement:
ALTER SYSTEM SET synchronous_standby_names = ''; SELECT pg_reload_conf();. It is reloadable and takes effect immediately; every waiting transaction completes at once. - ↶Understand what that does. The cluster is now asynchronous, and a primary failure from that moment can lose recently committed transactions. Record the time and set a reminder to restore the setting.
- ↶Cancelling individual waiting backends is the narrower option:
SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE wait_event = 'SyncRep';. Each cancelled transaction returns success to its client while being potentially absent from the standby, and PostgreSQL says so:the transaction has already committed locally, but might not have been replicated to the standby. - ↶Prefer changing the setting once, deliberately, over cancelling transactions one at a time. The setting is a decision; the cancellations are a series of small silent ones.
- ↶If the change is being reverted because the latency cost was unacceptable, record the measured cost. That number is what the next proposal will be evaluated against.
- ↶A cluster left asynchronous after an incident has quietly abandoned the durability guarantee it was built for. Nothing will remind you; put it in the incident's follow-up actions.
6 · Escalation
When the runbook isn't enough, contact:
- · Only one standby exists and synchronous replication has been requested: escalate to the service owner. A single synchronous standby makes the primary depend on it, and a routine reboot stops every write.
- · The latency cost is unacceptable to the application: escalate with the measured numbers. The choice between throughput and durability belongs to the service owner, and it is better made against evidence.
- · A standby cannot keep up under load and is holding commits: escalate to the platform owner. A synchronous standby on slower storage or a longer network path makes every transaction wait for the slowest component in the estate.
- · Commits are hanging and nobody can reach a standby: escalate to the incident owner and present both options — wait, or clear the requirement and accept asynchronous durability. That is a business decision under time pressure and it should be made by the person accountable for it.
- · Somebody proposes disabling
synchronous_commitglobally to relieve a performance problem: escalate to the data owner. That trades a durability guarantee for a symptom, and the trade must be explicit. - · The estate has a compliance requirement for zero data loss: escalate to whoever owns it before changing anything. Synchronous replication is one part of meeting such a requirement and the configuration details matter to the auditor.
Synchronous replication buys durability by making the primary depend on something else. Whether that is a good trade depends entirely on how many things it depends on.
One standby is a dependency; two are a quorum
The quoting trap
ERROR: invalid value for parameter "synchronous_standby_names": "db-standby-01"
DETAIL: syntax error at or near "-"
The value is parsed as a name list, so a name containing a hyphen needs double quotes inside the string literal:
ALTER SYSTEM SET synchronous_standby_names = '"db-standby-01"';
What each level waits for
synchronous_commit | The commit returns once the standby has… |
|---|---|
local | (nothing — local flush only) |
remote_write | written it to the OS |
on | flushed it to disk |
remote_apply | applied it, so a read on the standby sees it |
Measured throughput on one cluster, four eight-second runs:
synchronous_commit=local 824 tps
synchronous_commit=remote_write 810 tps
synchronous_commit=on 552 tps
synchronous_commit=remote_apply 647 tps
What the failure looks like
$ psql -c "SELECT pid, state, wait_event_type, wait_event, left(query,40) AS query FROM pg_stat_activity WHERE wait_event = 'SyncRep';" pid | state | wait_event_type | wait_event | query
-----+--------+-----------------+------------+-------------------------------
179 | active | IPC | SyncRep | INSERT INTO t DEFAULT VALUES;
188 | active | IPC | SyncRep | INSERT INTO t DEFAULT VALUES;Reads keep working. No error appears in the server log, because nothing has failed — the primary is patiently doing what it was told.
An application that appears frozen while the database answers SELECTs
normally is this, and IPC / SyncRep with an empty
pg_stat_replication is this and nothing else.
The waiting transactions are already durable
Blast radius
| Action | Reversible? | What it costs if wrong |
|---|---|---|
| Setting a quorum with two standbys | Yes, with a reload | 25–33% of commit throughput |
| Setting a single synchronous standby | Yes, with a reload | Every write, whenever that standby is unavailable |
Clearing synchronous_standby_names | Yes, with a reload | The durability guarantee, until it is restored |
Cancelling a SyncRep wait | No | A transaction reported as successful that may not be replicated |
| Rebooting a synchronous standby without removing it first | Yes | Every write, for the duration of the reboot |