Primary–Standby Replication

Primary–standby replication is a replication topology in which one database server acts as the authoritative writer and one or more standby servers follow the primary's changes.

Older material often calls this:

master–standby
master–replica
master–slave

Modern PostgreSQL documentation uses:

primary
standby

so those terms will be used here.

A simple topology is:

                         Applications
                              |
                 +------------+------------+
                 |                         |
              writes                    some reads
                 |                         |
                 v                         v
          +--------------+        +--------------+
          |   Primary    |        |   Standby 1  |
          | read/write   |------->|  read-only   |
          +------+-------+   WAL  +--------------+
                 |
                 | WAL
                 v
          +--------------+
          |   Standby 2  |
          |  read-only   |
          +--------------+

The primary accepts changes.

The standbys receive and replay those changes.

Depending on the configuration, standbys can provide:

read scaling
high-availability candidates
disaster-recovery copies
backup offloading

But simply having a standby does not automatically guarantee:

zero data loss
automatic failover
fresh reads
safe split-brain prevention
historical recovery

Those are separate design decisions.

What problem does primary–standby replication solve?

Consider a database running on one machine:

Application
     |
     v
+-------------+
| PostgreSQL  |
| Primary     |
+-------------+

If the machine fails completely:

Application
     |
     X
+-------------+
| failed      |
+-------------+

the service cannot use the database until that server is repaired or restored elsewhere.

Now add a standby:

               WAL
Primary -----------------> Standby

The standby continuously follows changes.

If the primary fails, the standby may already contain nearly all—or, depending on the durability configuration, all—of the required database state.

That can significantly reduce recovery time.

Replication and failover are not the same thing

Replication means:

another node is receiving database changes

Failover means:

another node becomes the authoritative writer

These are separate mechanisms.

You can have perfectly functioning replication but no automatic failover.

For example:

Primary ------------> Standby
   X

If the primary fails, somebody still has to decide:

Is the primary really dead?

Is the standby sufficiently current?

May we safely promote it?

How will applications find it?

How do we stop the old primary from coming back
and accepting writes?

PostgreSQL provides replication and promotion primitives, but PostgreSQL itself does not supply the complete external failure-detection and automatic-failover system.

Follow one PostgreSQL transaction

Suppose the application executes:

BEGIN;

INSERT INTO orders(order_id, customer_id, amount)
VALUES (104, 27, 89.00);

COMMIT;

On the primary, PostgreSQL does not replicate that SQL string to the physical standby.

Instead, the primary produces Write-Ahead Log (WAL) describing the low-level database changes needed for recovery.

Conceptually:

SQL transaction
      |
      v
PostgreSQL modifies database state
      |
      v
generate WAL records
      |
      v
WAL becomes durable on primary
      |
      v
stream WAL to standby

On the standby:

receive WAL
     |
     v
write WAL
     |
     v
flush WAL
     |
     v
replay WAL
     |
     v
standby database reflects transaction

This pipeline is the key to understanding PostgreSQL physical replication.

PostgreSQL physical replication is WAL replication

PostgreSQL physical streaming replication works by transmitting WAL records from the primary to the standby.

The basic processes are conceptually:

PRIMARY

transactions
    |
    v
   WAL
    |
    v
walsender
    |
    | TCP replication connection
    v
=============================
    |
    v
walreceiver
    |
    v
STANDBY WAL
    |
    v
recovery / replay
    |
    v
standby data files

The standby reconstructs database changes by replaying WAL.

This is why physical replication is tightly connected to PostgreSQL's storage and recovery implementation rather than simply replaying SQL statements.

PostgreSQL describes streaming replication as sending WAL incrementally from the primary to the standby.

Receive, write, flush, and replay are different events

A crucial mental model is:

PRIMARY                         STANDBY

generate WAL
    |
    +---------- send ---------->
                              receive
                                 |
                                 v
                               write
                                 |
                                 v
                               flush
                                 |
                                 v
                               replay
                                 |
                                 v
                         visible database state

These steps mean different things.

Stage Meaning
Sent Primary transmitted the WAL
Written Standby wrote WAL to its local filesystem
Flushed Standby reports WAL durably flushed
Replayed Standby applied the WAL to its database state

PostgreSQL exposes these distinctions through replication positions such as sent_lsn, write_lsn, flush_lsn, and replay_lsn.

LSN: PostgreSQL's progress coordinate

An LSN, or Log Sequence Number, identifies a position in PostgreSQL's WAL stream.

Conceptually:

WAL:

... ---- A/B100 ---- A/B200 ---- A/B300 ---- A/B400 ...

Suppose:

Primary current WAL:      A/B400

Standby written:          A/B380
Standby flushed:          A/B370
Standby replayed:         A/B350

Then the standby has:

received more WAL than it has replayed

A replica can therefore be durable relatively far forward while its query-visible database state remains further behind.

That distinction becomes very important for synchronous replication.

Asynchronous streaming replication

PostgreSQL streaming replication is asynchronous unless synchronous replication is explicitly configured.

A simplified commit timeline is:

Client                 Primary                Standby
  |                       |                      |
  |---- transaction ----->|                      |
  |                       |                      |
  |                       | local commit         |
  |                       |                      |
  |<------ SUCCESS -------|                      |
  |                       |                      |
  |                       |------ WAL ---------->|
  |                       |                      |
  |                       |                 receive
  |                       |                 replay

The important ordering is:

client receives SUCCESS

before

standby is necessarily caught up

This gives asynchronous replication its principal advantages:

lower commit latency
less dependence on standby health

but creates two consequences:

stale reads

possible loss of recently acknowledged writes
after certain primary failures

PostgreSQL explicitly documents that asynchronous log shipping creates a window in which transactions committed on the primary may not yet exist on the standby.

Concrete stale-read example

Suppose:

Primary:
orders 1 ... 104

Standby:
orders 1 ... 103

The application creates order 104 on the primary:

POST /orders

→ primary

SUCCESS

Immediately afterward the frontend asks:

GET /orders/104

but the load balancer routes the read to a standby.

The standby has not yet replayed the relevant WAL.

It returns:

not found

The application may appear broken even though replication is functioning exactly as configured.

This is replication lag.

Read-your-writes consistency

A user often expects:

I just created the order.

Therefore I should immediately be able to read it.

Asynchronous replica reads do not automatically guarantee that.

Common application designs include:

Read from the primary after a write

write → primary
confirmation read → primary
later ordinary reads → replica

Return the created object from the write request

Instead of:

POST
then
GET

the write response itself contains the committed result.

Wait until a standby reaches a known WAL position

The application or infrastructure can require a replica to catch up before serving a consistency-sensitive read.

Which design is appropriate depends on the business semantics.

Hot standby

A PostgreSQL standby used only for failover does not necessarily have to serve ordinary application queries.

A hot standby is a standby that accepts read-only queries while recovery/replay continues.

For example, after replaying these rows, the standby's orders table contains:

order_id customer_id amount
104 27 89.00
105 28 35.00

This query can be executed on a hot standby:

SELECT *
FROM orders
WHERE customer_id = 27;
order_id customer_id amount
104 27 89.00

The server still cannot accept ordinary application writes while it remains a physical standby.

So:

Primary:
read + write

Hot standby:
read only

until promotion.

PostgreSQL distinguishes warm standbys from hot standbys specifically on whether read-only client queries can be served.

Read scaling

Suppose the workload is:

10,000 writes/sec
100,000 reads/sec

A topology could be:

                     Primary
                    writes
                   /   |   \
                  /    |    \
                 v     v     v
             Standby Standby Standby
               reads   reads   reads

This can distribute some read load.

But there are limits.

Adding more standbys does not automatically increase write capacity because writes still flow through the primary.

Primary–standby replication is therefore often good for:

read scaling

but not equivalent to:

write sharding

Replica queries can compete with WAL replay

A hot standby has two workloads:

application queries

and

WAL replay

Both use:

CPU
memory
disk I/O
cache

A very expensive reporting query can therefore compete with replication.

If replay slows down:

replay_lsn falls further behind

and stale-read lag grows.

A read replica is not unlimited free compute.

Recovery conflicts on a hot standby

There is another complication.

Suppose the primary runs:

VACUUM

and removes row versions that are no longer required on the primary.

Meanwhile, a long-running query on the standby still wants a snapshot involving those versions.

The standby must reconcile:

continue replaying primary WAL

with:

allow this old standby query to keep running

PostgreSQL can eventually cancel standby queries when necessary to allow recovery to continue.

This is why long-running analytical workloads require deliberate standby configuration rather than assuming replicas behave like independent databases.

hot_standby_feedback

One option is:

hot_standby_feedback = on

The standby sends information upstream about snapshots that its queries still need.

That can reduce query cancellations caused by cleanup on the primary.

But there is a cost.

The primary may have to retain dead row versions for longer:

standby needs old row versions
        |
feedback to primary
        |
VACUUM cannot remove them yet
        |
possible primary table bloat

PostgreSQL explicitly warns that hot_standby_feedback can reduce cleanup-conflict cancellations but can cause bloat on the primary.

So it should not be summarized as:

"turn this on to stop replica queries causing bloat"

The trade-off is essentially the reverse.

Synchronous replication changes the commit point

Suppose losing an acknowledged order is unacceptable.

We may require that a standby confirm the transaction before the primary returns:

SUCCESS

Now:

Client             Primary               Standby
   |                  |                     |
   |---- write ------>|                     |
   |                  |                     |
   |                  |----- WAL ---------->|
   |                  |                     |
   |                  |<---- ACK -----------|
   |                  |                     |
   |<---- SUCCESS ----|                     |

This is synchronous replication.

The application now pays additional commit latency in exchange for a stronger cross-node guarantee.

PostgreSQL synchronous replication requires two concepts

A common configuration error is to change only:

synchronous_commit = remote_apply

and assume a standby is now synchronous.

That is incomplete.

PostgreSQL must also know which standby or standbys count as synchronous through:

synchronous_standby_names

For example:

synchronous_standby_names = 'standby1'

with:

synchronous_commit = on

causes transactions to wait for the selected synchronous standby's durable WAL acknowledgement.

PostgreSQL's documentation explicitly states that synchronous replication requires a non-empty synchronous_standby_names; synchronous_commit then determines the acknowledgement point.

synchronous_commit levels

For a configured synchronous standby, the important levels include:

Setting Primary waits for
remote_write Standby writes WAL to its operating system
on Standby flushes WAL durably
remote_apply Standby replays the transaction
local Only local primary WAL flush; does not wait for synchronous standby
off Even local commit acknowledgement can precede WAL flush

The most important distinction is:

on

versus:

remote_apply

With on:

standby has transaction durably in WAL

but a read on the standby may still not see it yet.

With remote_apply:

standby has replayed the transaction

so it has become visible to queries there.

PostgreSQL documents this distinction explicitly.

Durability and read visibility are different

Suppose:

flush_lsn  = 0/5000
replay_lsn = 0/4800

The standby has safely persisted WAL through:

0/5000

but queries currently reflect replay only through:

0/4800

A transaction at:

0/4900

may therefore already be protected against certain failures while still being invisible to standby readers.

This is why:

synchronous

does not automatically mean:

all replica reads are current

unless the chosen acknowledgement policy specifically waits for apply/replay.

remote_apply and immediate standby reads

Consider:

COMMIT order 104

with:

synchronous_commit = remote_apply

and a suitable synchronous standby configured.

The primary waits until the synchronous standby reports that the commit record has been replayed.

Now, after the commit returns:

query synchronous standby
→ order 104 can be visible

PostgreSQL describes remote_apply as waiting for replay specifically so committed changes are visible to standby queries.

The cost is higher transaction latency.

Multiple synchronous standbys

Suppose:

Primary

Standby A
Standby B
Standby C

PostgreSQL can use priority-based synchronization.

For example:

synchronous_standby_names = 'FIRST 2 (a, b, c)'

meaning:

wait for two highest-priority available synchronous candidates

Or quorum-style synchronization:

synchronous_standby_names = 'ANY 2 (a, b, c)'

meaning:

wait for any two of those standbys

PostgreSQL supports both FIRST and ANY selection methods.

Synchronous replication introduces an availability dependency

Suppose commits require one synchronous standby.

Then:

Primary ----X---- Synchronous standby

If the standby becomes unavailable, transactions requiring its acknowledgement may stop completing until the synchronous configuration changes or another qualifying standby takes its role.

So synchronous replication trades:

better durability

for more:

coordination
latency
availability dependency

This is not a defect.

It is the distributed-systems trade-off being explicitly chosen.

Geographic synchronous replication

Suppose the primary is in:

Frankfurt

and the required synchronous standby is in:

Sydney

A transaction now places long-distance network communication on the commit path.

Even if everything is healthy:

Frankfurt
   |
   | WAL
   v
Sydney
   |
   | acknowledgement
   v
Frankfurt

the speed of the network contributes directly to commit latency.

This is why many architectures use:

nearby synchronous standby
+
remote asynchronous disaster-recovery standby

rather than making every distant copy synchronous.

Creating a standby: why a base backup is required

A standby needs a consistent starting database state.

Suppose the primary already contains:

500 GB

of data.

The standby cannot simply begin receiving the next WAL record from an empty directory.

It first needs:

consistent database copy
+
correct point in WAL history

A PostgreSQL base backup provides that starting point.

pg_basebackup is specifically designed to produce a base backup usable for streaming-replication standbys.

Why arbitrary filesystem copies are dangerous

Suppose a PostgreSQL cluster is running while someone copies:

relation file A at 12:00:00

relation file B at 12:00:10

control information at 12:00:20

Transactions can occur between those times.

The copied files may therefore not correspond to one recoverable database state unless the correct PostgreSQL backup protocol is being followed.

Do not bootstrap a physical standby using an arbitrary live filesystem copy.

Use:

pg_basebackup

or another supported PostgreSQL backup mechanism.

pg_basebackup -R

A convenient initialization command looks conceptually like:

pg_basebackup \
  --host=primary.example.internal \
  --username=replicator \
  --pgdata="$PGDATA" \
  --wal-method=stream \
  --write-recovery-conf \
  --progress

--write-recovery-conf, or:

-R

creates:

standby.signal

and writes the required connection settings into:

postgresql.auto.conf

for the resulting standby.

This is preferable to treating standby. signal and primary_conninfo as unrelated manual steps unless manual control is actually required.

Do not hard-code example passwords into production configuration

The source example embeds a replication password directly into:

primary_conninfo

and uses a literal password in the notes.

That may be acceptable for a throwaway lab, but it is poor production documentation.

Prefer an appropriate secret-management mechanism, such as:

a protected PostgreSQL password file
secret injection
OS credential management
platform secret manager

and restrict permissions appropriately.

For authentication, modern PostgreSQL supports:

scram-sha-256

in pg_hba. conf. Current PostgreSQL documentation distinguishes SCRAM from the older md5 authentication method.

A clearer primary configuration

A minimal conceptual primary configuration might include:

wal_level = replica

max_wal_senders = 10
max_replication_slots = 10

listen_addresses = '...'

with replication authentication in:

pg_hba.conf

for example conceptually:

hostssl replication replicator <standby-network> scram-sha-256

The actual:

network
TLS policy
certificate policy
addresses
authentication

must match the deployment.

Do not copy example CIDRs and credentials blindly.

wal_keep_size is not the same thing as min_wal_size or max_wal_size

The source suggests that newer PostgreSQL versions allow:

min_wal_size / max_wal_size

instead of:

wal_keep_size

That is incorrect.

They control different things.

wal_keep_size

Keeps a minimum recent amount of WAL available for standbys.

min_wal_size / max_wal_size

Primarily participate in checkpoint/WAL-recycling behavior.

PostgreSQL explicitly documents wal_keep_size as independent of max_wal_size.

So they should not be presented as substitute parameters.

Replication slots

A physical replication slot tells PostgreSQL that a consumer may still require older WAL.

Conceptually:

Current primary LSN
            |
            v
-------------------------------------->

             ^
             |
        standby still
        needs WAL from here

slot.restart_lsn

PostgreSQL therefore avoids automatically removing WAL that may still be required by that slot, subject to configured retention limits.

A slot is very useful for disconnected or slow standbys.

But it introduces a major operational risk.

The replication-slot disk-fill problem

Suppose:

Primary generates:
50 GB WAL/hour

Standby goes offline.

Its physical slot remains.

After:

10 hours

the slot might require hundreds of gigabytes of old WAL.

Conceptually:

standby stopped consuming
        |
slot cannot advance
        |
old WAL retained
        |
pg_wal grows
        |
primary disk fills

That can become a primary outage.

PostgreSQL explicitly warns that a slow or failed replication-slot consumer can cause WAL accumulation.

max_slot_wal_keep_size

PostgreSQL provides:

max_slot_wal_keep_size

to place a limit on how far behind a replication slot may force WAL retention.

Without a limit, a slot can potentially retain very large amounts of WAL.

With a finite limit, the trade-off changes:

protect primary disk

but a sufficiently stale standby may lose the WAL it needs and require rebuilding.

PostgreSQL documents the default as unlimited retention and exposes slot WAL status through pg_replication_slots.

A slot does not mean "the standby has replayed everything"

This distinction matters.

A physical replication slot primarily protects WAL that the consumer may still require.

Meanwhile, PostgreSQL separately tracks:

write position
flush position
replay position

through streaming status updates.

So do not interpret:

slot advanced

as:

every query on the standby sees all those changes.

Receive/durability progress and replay/visibility progress are different things.

Monitoring on the primary

A useful starting query is:

SELECT
    application_name,
    client_addr,
    state,
    sync_state,
    sent_lsn,
    write_lsn,
    flush_lsn,
    replay_lsn,
    write_lag,
    flush_lag,
    replay_lag
FROM pg_stat_replication;

For one streaming standby, an example result is:

application_name client_addr state sync_state sent_lsn write_lsn flush_lsn replay_lsn write_lag flush_lag replay_lag
reporting_standby 192.0.2.20 streaming async 0/5000100 0/5000100 0/5000100 0/4FFFE00 00:00:00.050 00:00:00.050 00:00:00.250

This gives a much richer picture than checking only:

state = streaming

For example:

sent_lsn   far ahead of write_lsn

suggests one category of delay.

Whereas:

flush_lsn close to primary
replay_lsn much further behind

suggests replay/application lag.

PostgreSQL exposes separate write, flush, and replay positions and lag metrics specifically because those stages are different.

Monitoring on the standby

Useful functions include positions such as:

SELECT
    pg_last_wal_receive_lsn(),
    pg_last_wal_replay_lsn();

An example result is:

pg_last_wal_receive_lsn pg_last_wal_replay_lsn
0/5000100 0/4FFFE00

Conceptually:

received LSN
    |
    | gap = WAL waiting to be replayed
    v
replay LSN

If that gap grows continuously, the standby may be unable to replay WAL as fast as the primary produces it.

Monitoring should focus on trends rather than merely whether the replication process exists.

Functional replication tests

A simple test is useful:

On the primary:

CREATE TABLE replication_test (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    data text NOT NULL
);

INSERT INTO replication_test(data)
VALUES ('hello standby');

Then on the standby:

SELECT *
FROM replication_test;

Once the insert has replayed, the standby table and query result are:

id data
1 hello standby
id data
1 hello standby

But in an asynchronous topology, documentation should not promise:

"almost instantly"

as a correctness guarantee.

It may normally happen quickly, but lag depends on:

network
WAL generation
standby storage
CPU
replay workload
query conflicts

Asynchronous failover and data loss

Suppose:

T1 commits on primary.

Primary tells application:
SUCCESS.

T1 has not yet reached standby.

Primary suffers unrecoverable failure.

Standby promoted.

The new primary may not contain T1.

This is not database corruption.

It is the durability model that asynchronous replication permits.

The amount of acknowledged data that can be lost in such a failure relates to the system's:

RPO — Recovery Point Objective

If the business requirement is:

RPO = 0

the replication and failover design must explicitly support that requirement.

Failover has several steps

Safe failover is more than:

pg_ctl promote

A complete procedure includes something like:

1. Detect that the old primary is unavailable.

2. Decide that failover is appropriate.

3. Fence the old primary.

4. Choose the best standby.

5. Promote it.

6. Redirect application connections.

7. Confirm service health.

8. Rebuild redundancy.

9. Rejoin or rebuild the old primary.

Each step solves a different problem.

Promotion

Current PostgreSQL supports promotion with:

pg_ctl promote

or SQL:

SELECT pg_promote();
pg_promote
true

Promotion ends standby recovery and turns the server into a normal read/write primary.

Do not base current operational documentation on manually creating an undocumented:

promote.signal

file.

Use the supported promotion interfaces.

Split brain

The most dangerous failover failure is:

Old primary                     New primary
still accepting writes          promoted standby

Applications may send:

write X → old primary

write Y → new primary

Now there are two divergent histories.

This is split brain.

PostgreSQL's failover documentation explicitly warns that after promotion there must be a mechanism ensuring the old primary learns it is no longer primary; otherwise both systems can believe they own the primary role, leading to data loss.

Fencing

Safe HA requires making sure the losing node can no longer process authoritative writes.

This is often called:

fencing

or, in older cluster terminology:

STONITH

Possible mechanisms include:

power fencing
cloud-instance fencing
network isolation
storage fencing
cluster-manager leases

The exact mechanism depends on infrastructure.

The essential property is:

Before the new writer is trusted, the old writer must no longer be capable of creating a competing authoritative history.

Application redirection

Promotion does not automatically teach every application:

the writer moved from Node 1 to Node 2

The architecture needs a routing mechanism such as:

proxy
virtual IP
service discovery
DNS
HA endpoint
connection-pool integration

For example:

Before:

db-primary.internal → Node 1

After:

db-primary.internal → Node 2

This routing layer is part of the HA system even though it is not PostgreSQL replication itself.

Planned switchover versus unplanned failover

These terms are useful to separate.

Switchover

Planned role change:

old primary healthy
standby caught up
traffic controlled
roles exchanged deliberately

This can often be done with minimal uncertainty.

Failover

Triggered by an unexpected primary failure:

primary unreachable
its exact final state may be uncertain

Failover requires much stronger failure-detection and fencing logic.

A tested switchover procedure does not automatically prove that unexpected failover is safe.

What happens to the old primary?

Suppose:

Node 1 = old primary
Node 2 = promoted new primary

After promotion, Node 2 begins creating WAL on a new timeline.

Node 1's previous history and Node 2's new history have diverged.

The old primary cannot simply restart and resume behaving as primary.

It must be converted into a standby following Node 2.

pg_rewind

A full new base backup is one option.

But PostgreSQL also provides:

pg_rewind

for suitable failover situations.

pg_rewind compares the diverged cluster histories and copies the changes needed to make the old primary usable as a follower of the new primary.

PostgreSQL explicitly describes bringing an old primary back after failover as a typical pg_rewind use case.

So this source recommendation:

wipe old primary and always take a complete new base backup

is unnecessarily absolute.

Sometimes a new base backup is necessary.

Sometimes pg_rewind is substantially faster.

Replication timelines

Promotion creates a new PostgreSQL timeline.

Conceptually:

Timeline 1:

-----------------------------X
                              \
                               \
Timeline 2:                     +----------------->
                                 new primary

The timeline records that the database history forked.

This is essential because after failover there may be WAL from:

old primary history

and:

new primary history

and PostgreSQL must distinguish them.

For HA standbys, PostgreSQL recommends following the latest recovery timeline so downstream standbys can follow a promoted server.

Cascading replication

Standbys do not always have to connect directly to the primary.

You can have:

Primary
   |
   v
Standby A
   |
   +------> Standby B
   |
   +------> Standby C

This is cascading replication.

Potential benefit:

reduce primary network fan-out

Potential cost:

B and C depend on A

and gain an additional replication hop.

Also note that PostgreSQL's synchronous-standby selection on a primary concerns directly connected standbys rather than arbitrary downstream cascading standbys.

Replication is not backup

Suppose the application accidentally executes:

DELETE FROM orders;

The primary generates WAL describing the deletions.

The standbys faithfully replay that WAL.

Soon:

Primary:
orders gone

Standby 1:
orders gone

Standby 2:
orders gone

Replication worked correctly.

It did not protect the historical data.

You still need:

base backups
WAL archiving
point-in-time recovery
restore testing

for historical recovery.

WAL archiving and streaming replication solve different problems

Streaming replication sends WAL to currently connected standbys.

WAL archiving retains WAL in an archive for recovery purposes.

A robust system can use both:

Primary
   |
   +------ streaming ------> HA standby
   |
   +------ WAL archive ----> durable archive storage

The standby supports:

fast failover

The archive supports:

point-in-time recovery
recovery from older backup
additional disaster-recovery workflows

These mechanisms complement rather than replace each other.

archive_mode is not merely a replication performance tweak

Settings such as:

archive_mode = on

and:

archive_command = ...

belong to a deliberate continuous-archiving/PITR design.

They should not be presented simply as:

extra hardening for replicas

The archive must be:

durable
monitored
restorable
capacity-managed

and its retention must align with base-backup strategy.

PostgreSQL recommends an archive accessible independently of the primary when using WAL archives for standby/recovery purposes.

Clock synchronization

Accurate clocks are strongly recommended in distributed infrastructure for:

logs
metrics
certificate validity
monitoring
incident reconstruction
time-based features

But this source statement:

"replication breaks if clocks drift"

is too strong.

Basic WAL streaming is based on WAL positions, not on synchronized wall clocks.

Some time-sensitive PostgreSQL features do depend on clocks.

For example, recovery_min_apply_delay compares primary commit timestamps with standby time, and PostgreSQL notes that unsynchronized clocks can affect that delay calculation.

So the accurate guidance is:

Synchronize clocks operationally, but do not teach clock synchronization as a fundamental prerequisite for basic physical streaming replication.

Avoid generic Linux tuning prescriptions

Advice such as:

vm.swappiness = 1

kernel.shmmax >= shared_buffers

should not be presented as universal prerequisites for PostgreSQL replication.

Kernel and memory tuning depends on:

Linux distribution
PostgreSQL version
memory size
workload
storage
containerization
huge pages
deployment platform

Replication works because the PostgreSQL replication configuration is correct—not because a generic sysctl recipe was copied.

Tune only from measured workload requirements and current platform documentation.

checkpoint_timeout is not a standby catch-up setting

The source recommends:

checkpoint_timeout = 15min

with an explanation implying this helps replicas catch up.

That is misleading.

checkpoint_timeout controls checkpoint timing.

Checkpoint configuration affects:

WAL volume
I/O behavior
crash-recovery work
checkpoint frequency

It is not a direct:

"make replica catch up"

parameter.

Replica catch-up depends on:

WAL generation
network throughput
standby WAL write/flush throughput
replay speed
standby workload

Do not tune checkpoint behavior primarily from a replication slogan.

backup_label is not a tuning parameter

Likewise:

backup_label

should not appear in a table of configuration settings beside:

checkpoint_timeout
archive_mode
hot_standby_feedback

backup_label is part of PostgreSQL backup/recovery machinery, not a setting you normally "enable" for replication hardening.

Point-in-time recovery uses:

base backup
WAL archive
recovery target configuration

as a complete recovery design.

Corrected PostgreSQL lab topology

For a teaching environment:

Primary:
db1.example.internal

Standby 1:
db2.example.internal

Standby 2:
db3.example.internal

Avoid hard-coding production documentation around:

10.0.0.10
10.0.0.11
10.0.0.12

unless those addresses actually belong to the lab.

The logical topology is what matters:

                     db1
                  PRIMARY
                  /      \
                 /        \
              WAL          WAL
               v            v
             db2            db3
          HOT STANDBY    HOT STANDBY

Step 1 — create a replication role

On the primary:

CREATE ROLE replicator
WITH LOGIN REPLICATION
PASSWORD '<managed-secret>';

For a real deployment, store and deliver the secret securely rather than publishing a reusable password in documentation.

Step 2 — permit replication connections

Example conceptually:

# pg_hba.conf

hostssl replication replicator <standby-1-address>/32 scram-sha-256
hostssl replication replicator <standby-2-address>/32 scram-sha-256

The exact authentication policy should follow the environment's security model.

After changing pg_hba. conf, reload PostgreSQL as appropriate.

Step 3 — primary replication parameters

For example:

wal_level = replica

max_wal_senders = 10

max_replication_slots = 10

If physical slots will be used, create one per standby:

SELECT pg_create_physical_replication_slot('standby1');
SELECT pg_create_physical_replication_slot('standby2');

Example results (the returned LSN values depend on the cluster):

slot_name lsn
standby1 0/3000000
standby2 0/3000000

Then monitor them.

A slot is not a "set it and forget it" feature.

Step 4 — initialize each standby

Stop PostgreSQL on the target standby and ensure its target data directory is suitable for replacement.

Then run pg_basebackup, for example:

pg_basebackup \
  --host=db1.example.internal \
  --username=replicator \
  --pgdata="$PGDATA" \
  --wal-method=stream \
  --write-recovery-conf \
  --progress

If you want pg_basebackup itself to use a named slot, configure the relevant slot option as appropriate for the deployment.

-R/--write-recovery-conf creates standby. signal and writes connection settings into postgresql. auto. conf.

Step 5 — configure slot identity if used

For Standby 1:

primary_slot_name = 'standby1'

For Standby 2:

primary_slot_name = 'standby2'

Each independent physical standby should normally have its own slot if slots are being used.

Otherwise multiple consumers would not independently preserve their own required WAL positions.

Step 6 — start the standby

Start PostgreSQL.

The standby:

recognizes standby.signal

connects using primary_conninfo

starts WAL recovery

starts streaming when possible

The primary should then show a corresponding connection in:

SELECT application_name, state, sync_state
FROM pg_stat_replication;

Example result after the standby connects:

application_name state sync_state
standby1 streaming async

Step 7 — verify the pipeline, not only connectivity

Check:

SELECT
    application_name,
    client_addr,
    state,
    sync_state,
    sent_lsn,
    write_lsn,
    flush_lsn,
    replay_lsn
FROM pg_stat_replication;

Example result (position values are illustrative):

application_name client_addr state sync_state sent_lsn write_lsn flush_lsn replay_lsn
standby1 192.0.2.11 streaming async 0/5000100 0/5000100 0/5000100 0/4FFFE00

You want to understand:

Is WAL being sent?

Is the standby writing it?

Is it flushing it?

Is it replaying it?

A connection marked:

streaming

is good, but it is only one part of replication health.

Configuring synchronous replication correctly

Suppose standby1 uses:

application_name=standby1

in its connection information.

The primary could use:

synchronous_standby_names = 'FIRST 1 (standby1, standby2)'

and:

synchronous_commit = on

Now commits requiring the default synchronous behavior wait for durable acknowledgement from the chosen synchronous standby.

If the application specifically requires standby read visibility before commit returns:

synchronous_commit = remote_apply

may be appropriate.

The important point is that:

synchronous_standby_names

selects the participating standby set, while:

synchronous_commit

controls the wait level.

Monitoring table

Question PostgreSQL signal
Is standby connected? pg_stat_replication. state
Is it synchronous? sync_state
How far has primary sent? sent_lsn
How far has standby written? write_lsn
How far has standby flushed? flush_lsn
How far has standby applied? replay_lsn
How much WAL does a slot retain? pg_replication_slots. restart_lsn, wal_status, safe_wal_size
Are standby queries being canceled? pg_stat_database_conflicts on standby
Is receiver active? pg_stat_wal_receiver on standby

This is far more useful operationally than a single:

replication = yes/no

indicator.

Failover procedure summary

A manual emergency sequence is conceptually:

1. Confirm old primary is not allowed to keep serving writes.

2. Choose the intended standby.

3. Check its replication/recovery state.

4. Promote:
      pg_ctl promote
   or:
      SELECT pg_promote();

5. Confirm it accepts writes.

6. Redirect applications.

7. Re-establish remaining standbys.

8. Rewind or rebuild the old primary.

9. Restore HA redundancy.

The exact automation belongs to an HA manager rather than ad hoc shell commands.

What PostgreSQL alone does and does not provide

PostgreSQL provides:

WAL generation
streaming replication
hot standby
replication slots
synchronous replication
promotion
timelines
pg_rewind
monitoring views

PostgreSQL by itself does not provide the complete external system for:

failure detection
quorum-based primary ownership
fencing
virtual-IP movement
application endpoint failover
cluster orchestration

PostgreSQL's own failover documentation explicitly says external system software is required for primary failure detection and notification.

Tools and platforms can provide that orchestration layer.

Primary–standby across technologies

The same architecture appears under different names.

Technology Writer role Follower role Change mechanism Stronger acknowledgement
PostgreSQL physical replication Primary Standby WAL streaming/replay Synchronous standby
MySQL Source/primary Replica Binary log / relay log Semisynchronous replication
SQL Server Availability Groups Primary replica Secondary replica Transaction-log transport/redo Synchronous commit
MongoDB replica set Primary Secondary Oplog replication Configurable write concern

The concepts recur:

one current writer

replicated change stream

followers apply it

optional stronger acknowledgement

promotion/election after failure

but the exact correctness and failover semantics differ by engine.

Advantages and limitations

Benefit Important qualification
High availability Requires safe failover orchestration and fencing
Lower RTO Only if standby is healthy and sufficiently caught up
Read scaling Reads may be stale and can compete with replay
Better durability Depends on asynchronous vs synchronous acknowledgement
Maintenance flexibility Depends on topology and failover procedure
Geographic copy Network latency and replication lag matter
Simple write ownership Primary can become a write bottleneck

The source correctly identifies read offloading, failover, and write-primary bottlenecks as central characteristics of the topology.

Common misconceptions

Claim Correct interpretation
"We have a standby, therefore no data can be lost. " Async replication can leave an acknowledged-write loss window
"Synchronous means the standby query sees the write. " Only if acknowledgement waits through replay, such as remote_apply
"A replication slot means the standby is current. " It manages WAL retention; replay position is separate
"wal_keep_size is replaced by max_wal_size. " They control different things
"Clock drift breaks basic streaming replication. " WAL positions drive basic streaming; clocks matter for specific time-based behavior and operations
"hot_standby_feedback prevents primary bloat. " It can prevent standby cleanup conflicts by allowing more primary bloat
"Promotion solves failover. " Promotion is only one step; fencing and routing are also required
"Old primary must always be wiped. " A new base backup may be needed, but pg_rewind can often rejoin a diverged former primary
"Replication is backup. " Logical mistakes replicate too
"More standbys increase write throughput. " They mainly add redundancy/read capacity in a single-primary design

Final mental model

Do not memorize primary–standby replication as:

Primary copies database to standby.

Use this model:

APPLICATION WRITE
       |
       v
     PRIMARY
       |
       v
generate WAL
       |
       v
local durability
       |
       +-----------------------------+
       |                             |
       | streaming                   |
       v                             |
    STANDBY                          |
       |                             |
     receive                         |
       |                             |
      write                          |
       |                             |
      flush                          |
       |                             |
     replay                          |
       |                             |
       v                             |
read-visible state                   |
                                     |
          <--- commit policy decides
               where SUCCESS occurs

Then ask five questions.

Where does COMMIT SUCCESS occur?

after primary only?

after standby write?

after standby flush?

after standby replay?

Where are reads served?

primary?

standby?

both?

How stale may standby reads be?

milliseconds?

seconds?

not stale at all?

What happens when the primary disappears?

who detects it?

who chooses the new primary?

who fences the old one?

who redirects clients?

How do we recover historical data?

standby?
No.

backup + WAL archive + PITR.

If those questions have clear answers, the replication architecture is understandable.

If the only documentation says:

"we have two replicas"

you still know very little about its actual availability and durability guarantees.

References

The source notes provide the original primary/standby architecture, PostgreSQL streaming-replication example, base-backup setup, replication-status check, manual promotion, replication-slot discussion, and HA tuning suggestions.

Current PostgreSQL high-availability documentation describes physical WAL streaming, hot standbys, synchronous replication, cascading replication, standby setup, and the distinction between synchronous write/flush/apply acknowledgement.

Current pg_basebackup documentation describes base backups for standby initialization and -R/--write-recovery-conf, which creates standby. signal and writes connection information to postgresql. auto. conf.

Current PostgreSQL monitoring documentation distinguishes sent_lsn, write_lsn, flush_lsn, and replay_lsn, allowing receive/durability lag to be distinguished from replay lag.

PostgreSQL's replication-slot documentation describes restart_lsn as the oldest WAL still potentially required by the consumer and documents the risk/limits associated with retained WAL.

PostgreSQL's failover and pg_rewind documentation describes the need to prevent the old primary from continuing as a writer and explains how a diverged former primary can often be rewound and reused as a standby after failover.