The idea: when does the primary say OK?

A primary database keeps its changes in a write-ahead log (PostgreSQL WAL, MySQL binlog) and streams that log to its replicas. All three replication modes ship the same log the same way. They differ in one thing only: when the primary tells the client "committed".

Three time-down diagrams with client, P, R1 and R2. Async: P answers OK right after its own WAL write, 2 ticks. Semi-sync: P waits for R1's ack, 6 ticks. Sync: P waits for R2's ack too, 10 ticks
Every mode ships the record to both replicas; only the moment of the OK moves, and with it the commit latency.
  • Asynchronous: OK as soon as the write is in the primary's own WAL. The replicas get it a little later.
  • Semi-synchronous: OK once one replica says it has the record (MySQL semi-sync, PostgreSQL ANY 1 (…)).
  • Synchronous: OK once every listed replica says so (here PostgreSQL FIRST 2 (R1, R2)).

The page runs three identical clusters on one clock: a client, a primary P, a near replica R1 (2 ticks away) and a far replica R2 (4 ticks away). The client sits 1 tick from P and commits w1, waits for OK, commits w2, and so on. The timelines on the right share one tick axis, so the three modes can be compared at a glance.

Commit latency: what you wait for

A commit costs the round trips the primary waits for. Async waits for none: 1 tick to P and 1 back, 2 ticks. Semi-sync adds a round trip to the nearest replica: 1 + 2 + 2 + 1 = 6 ticks. Sync adds a round trip to the slowest replica: 1 + 4 + 4 + 1 = 10 ticks. A client that commits one write at a time gets 10, 3 and 2 commits in the same 21 ticks (Demo 1: commit latency). Many clients in parallel win back throughput, but no amount of parallelism shortens a single commit.

RPO: what a crash loses

The recovery point objective (RPO) is how much committed data you may lose when the primary dies. When it does, a monitor (Patroni, Orchestrator, a cloud control plane) promotes the replica with the most log. Any write the client was told was committed but the new primary never received is gone.

Three rows. Async: the OK reaches the client before R1 has the write, so a crash in that gap loses an acked write. Semi-sync: R1 has the write before the OK is sent, no loss. Sync: R1 and R2 have it before the OK, no loss
Async acks before any replica has the write, so a crash in that window loses it; semi-sync and sync close the window by waiting.

Demo 2: primary crashes stops P at tick 10. Async had just answered OK for w5, but w5 was still on its way to R1: R1 is promoted and w5 is lost ✗. Semi-sync and sync lose nothing: every OK they sent was preceded by a replica's ack. Notice semi-sync's w2: the client never got an answer, yet R1 had it, so it survives. "No answer" means "unknown", not "lost". The old primary's extra WAL entries are thrown away when it is rewound (pg_rewind) or rebuilt.

When a replica is slow or down

Waiting is also a dependency. Demo 3: R1 is down: async does not notice; semi-sync still needs only one ack, but now it must come from the far R2, so each commit takes 10 ticks; sync with FIRST 2 (R1, R2) waits for R1 forever and no write commits at all. That is why real synchronous setups name more standbys than they wait for (FIRST 1 (r1, r2), ANY 1 (r1, r2)) or use a quorum of three.

Semi-sync falls back to async

MySQL semi-sync does not wait forever. After rpl_semi_sync_source_timeout (10 seconds by default, 10 ticks here) with no ack, the primary falls back to asynchronous replication and answers OK anyway; it switches back when a replica catches up. Demo 4: replicas cut off, then a crash cuts the network to both replicas. Semi-sync waits 10 ticks, falls back, then acks w1 to w3 with no copy anywhere. When P dies, async loses 8 acked writes, semi-sync 3, and sync none, because sync committed nothing. Setting rpl_semi_sync_source_timeout very high turns semi-sync into "block instead of lose".

The settings in PostgreSQL and MySQL

ModePostgreSQLMySQL
asyncsynchronous_standby_names = '' or synchronous_commit = localdefault replication
semi-syncsynchronous_standby_names = 'ANY 1 (r1, r2)'rpl_semi_sync_source_enabled = ON, rpl_semi_sync_source_wait_for_replica_count = 1, rpl_semi_sync_source_wait_point = AFTER_SYNC
sync (all)synchronous_standby_names = 'FIRST 2 (r1, r2)'not built in; Group Replication uses a Paxos majority instead
what "has it" meanssynchronous_commit = remote_write (in OS memory), on (flushed), remote_apply (replayed)the replica wrote it to its relay log

Choosing a mode

AsyncSemi-sync (1 of N)Sync (all)
commit latencylocal disk only+ round trip to the nearest replica+ round trip to the slowest replica
acked writes lost on failover (RPO)the replication lag ✗none, unless it fell back to asyncnone ✓
a replica goes downno effectslower, or falls back after the timeoutwrites block
typical useread replicas, cross-region copiesHA pair in one regionsmall clusters with spare standbys, financial ledgers

See also Database Replication for replication lag, stale reads and split brain, MySQL Replication Internals for the binlog, GTIDs and the semi-sync plug-in, Quorum Reads and Writes for W-of-N acks without a leader, and Leader-based vs Leaderless Replication.

What the page leaves out

Many concurrent clients and group commit, the difference between received, flushed and applied on the replica (stale reads), failure detection time and fencing of the old primary, semi-sync switching back on after a replica catches up, the AFTER_COMMIT wait point (where other sessions can see a write that may still be lost), quorum commit across three or more replicas, cascading replicas, and real time: here every network hop is a whole number of ticks and a crash also drops every message on the wire.