Overview
Streaming replication keeps a standby up to date by continuously sending WAL (write-ahead log) records from the primary as they're generated, rather than waiting for a log file to fill, and it's asynchronous by default: a commit on the primary succeeds without waiting for the standby, typically under a second behind, but genuinely at risk of losing the most recent commits if the primary crashes before they replicate. Synchronous replication trades that risk for added commit latency, and its three synchronous_commit levels each guarantee something different: remote_write waits only for the standby to receive the record, on waits for the standby to flush it to disk, and remote_apply waits for the standby to actually replay it, the level that guarantees a standby read immediately after commit sees the change. Replication lag isn't something to assume is small, it's measured directly by comparing WAL positions on the primary and standby.
Quick Reference
| Setting | Waits for | Durability |
|---|---|---|
| Asynchronous (default) | Nothing from the standby | Small window of possible data loss on primary crash |
synchronous_commit = remote_write | Standby receives + hands to its OS | Lost only if standby OS itself crashes before its flush |
synchronous_commit = on | Standby flushes to its own disk | Lost only if primary and standby crash simultaneously |
synchronous_commit = remote_apply | Standby replays and makes visible | Standby reads see the commit immediately |
Syntax
# postgresql.conf on the standby
primary_conninfo = 'host=10.0.0.5 port=5432 user=replicator'
# On the primary, to require synchronous replication
synchronous_standby_names = 'standby1'
synchronous_commit = onExamples
-- Measure actual replication lag instead of assuming it's small.
SELECT pg_current_wal_lsn() AS primary_position; -- run on primary
SELECT pg_last_wal_replay_lsn() AS standby_position; -- run on standby
-- A growing byte gap (pg_wal_lsn_diff) between these two values is real, actionable lag.Asynchronous replication can lose the most recent commits
The primary confirms a commit to the client without waiting for the standby. If the primary crashes before that WAL record replicates, the transaction the client was told succeeded is gone from the standby. This is documented, expected behavior for asynchronous replication, not a bug.
Visual Diagram
Common Mistakes
- Assuming asynchronous replication has "basically no risk" because the typical lag is small, rather than understanding it's a real, non-zero data-loss window on primary crash.
- Choosing
synchronous_commit = onwithout understanding it adds real round-trip latency to every commit, then being surprised by the throughput cost. - Reading from a standby immediately after a primary commit under asynchronous or
remote_writereplication and expecting to see that change, when onlyremote_applyguarantees that. - Never actually monitoring replication lag via WAL position, and only discovering it was large during a failover that lost more data than expected.
Performance
- Synchronous replication's minimum added latency per commit is the round-trip time to the standby; this is a direct, unavoidable cost of the durability guarantee, not a tunable inefficiency.
- Replication lag under asynchronous replication grows under primary write load, network delay, or standby resource pressure, and widening lag is a leading indicator of standby health problems before they become a failover data-loss incident.
Best Practices
- Choose the
synchronous_commitlevel deliberately based on the actual durability requirement, not by defaulting to either extreme. - Monitor replication lag directly via WAL position comparison, not by assuming it stays small.
- Use
remote_applyspecifically when read queries against a standby need to see a transaction immediately after the primary reports it committed. - Understand and accept asynchronous replication's data-loss window explicitly as a trade-off for lower commit latency, rather than treating it as risk-free.