Replication is how databases survive node failures and scale read throughput. But every replication strategy introduces a fundamental tension: the speed of light limits how fast data can propagate, and the choice between synchronous and asynchronous replication determines whether you prioritize consistency or availability.
PostgreSQL Replication Architecture
PostgreSQL uses physical replication based on WAL shipping:
Primary → WAL records → Standby applies WAL → Standby has copy of data-- Primary configuration
ALTER SYSTEM SET wal_level = replica;
ALTER SYSTEM SET max_wal_senders = 5;
ALTER SYSTEM SET synchronous_standby_names = ''; -- Empty = async
-- Standby configuration
ALTER SYSTEM SET primary_conninfo = 'host=primary port=5432 user=replicator';
SELECT pg_create_physical_replication_slot('standby1');
-- Check replication status on primary
SELECT
client_addr,
state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_lag_bytes,
pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn)) AS replay_lag_pretty
FROM pg_stat_replication;Async vs Sync Replication
Asynchronous Replication
The primary commits without waiting for the standby to confirm. This is the default and most common configuration.
-- Async replication (default)
ALTER SYSTEM SET synchronous_standby_names = '';
-- Primary commits immediately
-- Standby receives WAL asynchronously
-- Lag can be milliseconds to secondsPros: No performance penalty on primary Cons: Data loss window equals replication lag
Timeline:
Primary: [Write] [Write] [Write] [CRASH]
Standby: [Write] [Write] [apply] [missing!]
↑
Lag of 1 write
This data is LOSTSynchronous Replication
The primary waits for at least one standby to confirm before committing.
-- Sync replication to one standby
ALTER SYSTEM SET synchronous_standby_names = 'FIRST 1 (standby1)';
-- Primary blocks until standby1 confirms receipt
-- Sync replication to any 2 of 3 standbys
ALTER SYSTEM SET synchronous_standby_names = 'ANY 2 (s1, s2, s3)';
-- Primary blocks until any 2 standbys confirm
-- Check sync status
SELECT
application_name,
sync_state,
sync_priority
FROM pg_stat_replication;Pros: Zero data loss Cons: Latency increase = network RTT + standby write time
tradeoff / Durability vs Latency
For most applications, async replication with a small lag tolerance is the right choice. Use sync replication only for financial or regulatory requirements where data loss is unacceptable.
Async replication adds zero latency but risks data loss. Sync remote_write adds RTT but guarantees WAL persistence. Sync remote_apply adds RTT + replay time but guarantees the standby has fully applied changes.
Read-After-Write Consistency
The most common consistency issue with async replication: a user writes data, then immediately reads it from a standby that hasn't received the write yet.
-- User A updates their profile on primary
UPDATE users SET email = 'new@example.com' WHERE id = 42;
COMMIT; -- Success
-- User A's next request hits a standby (load balanced)
SELECT email FROM users WHERE id = 42;
-- Returns old email! Standby hasn't applied the write yetSolutions:
# 1. Session stickiness: Route reads to primary for recently-written data
class ReadRouter:
def __init__(self, primary, standbys):
self.primary = primary
self.standbys = standbys
def read(self, user_id, last_write_time):
if time.now() - last_write_time < timedelta(seconds=5):
return self.primary.query(user_id)
else:
return random.choice(self.standbys).query(user_id)
# 2. Wait for replay: Read from standby but wait until it catches up
def read_after_write(standby, primary_lsn, timeout_ms=100):
"""Read from standby, waiting until it has replayed past primary_lsn"""
start = time.now()
while time.now() - start < timeout_ms / 1000:
standby_lsn = standby.execute("SELECT pg_last_wal_replay_lsn()")
if standby_lsn >= primary_lsn:
return standby.query(...)
time.sleep(0.001) # 1ms poll
# Timeout: fallback to primary
return primary.query(...)
# 3. Causal consistency: Use session tokens
def write_with_causal_token(user_id, data):
result = primary.execute(
"UPDATE users SET ... RETURNING xmin",
(data,)
)
return CausalToken(
lsn=result.xmin,
timestamp=time.now()
)
def read_with_causal_token(user_id, token):
for standby in standbys:
if standby.replay_lsn() >= token.lsn:
return standby.query(user_id)
# Fallback: wait for any standby to catch upMonitoring Replication Lag
-- Real-time lag monitoring
SELECT
now() - pg_last_xact_replay_timestamp() AS replication_lag_time,
pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn()) AS replay_lag_bytes;
-- On primary: check all standbys
SELECT
application_name,
state,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS byte_lag,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS lag,
replay_lag -- Time since last WAL replay
FROM pg_stat_replication;
-- Alert on excessive lag
SELECT
CASE
WHEN pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) > 100 * 1024 * 1024
THEN 'ALERT: Lag > 100MB'
WHEN pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) > 10 * 1024 * 1024
THEN 'WARNING: Lag > 10MB'
ELSE 'OK'
END AS status,
application_name,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS lag
FROM pg_stat_replication;Factors Affecting Lag
Replication lag depends on:
- Network bandwidth: WAL shipping speed
- Standby write speed: Disk I/O on standby
- WAL generation rate: Write volume on primary
- Replay parallelism: Number of WAL apply workers
-- Tune replay parallelism
ALTER SYSTEM SET max_parallel_workers = 4; # For WAL replay
ALTER SYSTEM SET max_worker_processes = 8;
-- Monitor replay workers
SELECT
pid,
state,
application_name
FROM pg_stat_activity
WHERE backend_type = 'walreceiver';
-- Check WAL generation rate
SELECT
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')
) AS total_wal_generated;Synthesis
Replication lag is a physical reality — data cannot propagate faster than the speed of light, and disk writes take time. The choice between async and sync replication is a direct tradeoff between write latency and durability guarantees. Read-after-write consistency requires either routing reads to the primary, waiting for standby replay, or using causal tokens. Understanding these tradeoffs lets you design replication strategies that match your consistency requirements without over-engineering.