Databases · Replication and Sharding
Where the Commit Returns: Synchronous and Asynchronous Replication Boundaries
What a commit acknowledgement actually guarantees at each replication mode, how quorum commits move the durability boundary, and why a synchronous replica becomes a latency dependency.
A commit is a promise, and the size of the promise is set by where the commit returns. If the server replies as soon as the change is in its own log, the promise is that the change survives the loss of that server and nothing else. If it replies after another machine has the change, the promise is larger. Every replication mode is a statement about which machines must hold the write before the client is told it happened, and the price of a larger promise is paid in commit latency and in the number of machines whose failure can now stall writes.
The acknowledgement boundary is a durability claim
Consider a client that receives a commit acknowledgement. What can it safely do? It can tell a user the order was placed, because a later read is supposed to see it. That expectation is a durability claim, and the claim is only true if every machine that could later serve the read or become the leader has the write, or if the system has another way to guarantee it will not be lost.
Asynchronous replication makes the smallest claim. The leader writes to its own log, applies the change, and acknowledges. Followers receive the log later. The guarantee is bounded by the leader’s own storage: a power loss that destroys the leader’s disk loses acknowledged commits. A crash that merely restarts the leader loses nothing, because the log survives on disk. The distinction matters — asynchronous replication is durable against process failure and not against media loss, unless the leader’s storage is itself replicated.
Synchronous replication makes a claim about a second machine. The leader waits until a designated follower has written the record to its own durable log before acknowledging. Now the loss of either machine alone does not lose the commit, provided the surviving one becomes the leader. The claim is conditional on failover working, which is why leader failover is part of the durability story rather than a separate operational concern.
The intermediate position is the one most often confused. A synchronous replica can be configured to acknowledge after writing to its log but before applying, or after applying. The first is cheaper and sufficient for durability, because the record can be replayed later. The second is only needed if the replica is expected to serve reads of that change immediately, since a read on a replica that has logged but not applied the record still sees the old value.
Counting replicas is not counting failures
A configuration that requires one synchronous standby sounds like it tolerates one failure. It does not, in general. If the leader plus one standby is the requirement, then the failure of the standby stops writes entirely: the leader has no one to wait for, and depending on the implementation it either blocks or silently degrades to asynchronous. The failure of the leader is fine, since the standby is current. So the configuration tolerates the failure of exactly one specific machine and not the failure of an arbitrary one.
The general statement is a quorum inequality. With N nodes and a write requiring W acknowledgements, the system tolerates the loss of N − W nodes without losing data, and it can continue accepting writes as long as at least W nodes are reachable. Reading with R nodes gives the familiar intersection condition: R + W > N guarantees that any read set overlaps any write set, so a read sees the latest acknowledged write. Those inequalities describe independent failure, not correlated failure, and they say nothing about how the surviving nodes elect a leader. Quorum intersection takes the argument apart in more detail, including the cases where the inequality holds and the read is still stale.
PostgreSQL’s synchronous replication expresses the same idea through a standby list with priority and a FIRST/ANY distinction. FIRST k waits for the k highest-priority listed standbys, so the set of machines being waited on is fixed by configuration and the loss of a low-priority standby does not stall writes while the loss of a high-priority one does. ANY k waits for any k of the listed standbys, which converts the requirement into a quorum over the list and makes the failure of any single member tolerable as long as k others remain. The two settings differ in exactly the way the inequality above predicts, and the failure of the wrong machine under FIRST is the classic cause of a write outage.
MySQL’s semi-synchronous plugin is a weaker variant worth naming precisely. It waits for a replica to acknowledge receipt of the event, but the acknowledgement is of receipt, not of durable write in every configuration, and the plugin falls back to asynchronous mode after a timeout rather than blocking indefinitely. The fallback is a deliberate availability choice, and it means the durability guarantee is conditional on the timeout not having fired.
What a synchronous commit costs in latency
The commit path grows by at least one network round trip and one follower-side log write. The round trip is the dominant term on a local network and the only term that matters on a wide-area link. A same-rack round trip is tens of microseconds; a cross-region one is tens of milliseconds, which is two to three orders of magnitude larger. That difference, not any property of the database, is what makes cross-region synchronous replication expensive: every commit pays the inter-region latency, and a transaction with five sequential commits pays it five times.
The leader’s throughput is bounded by the round trip rather than by its own CPU. If a commit takes one round trip and the leader can have many commits in flight, it can pipeline them and the throughput is limited by the follower’s ability to absorb the stream. If the leader serialises commits — some implementations do, for simplicity or to preserve a strict order — then throughput is roughly one commit per round trip, which on a 10 ms link is about a hundred commits per second regardless of how fast the hardware is. Pipelining and group commit exist precisely to break this coupling: several commits are batched into one round trip, and each client’s acknowledgement waits for the batch.
The follower becomes a latency dependency and a capacity dependency. Its log write rate must keep up with the leader’s, and its network path must be as good as the leader’s own commit path. A slow follower does not merely lag; under a strict synchronous mode it slows every commit on the leader. This is why some systems track the slowest acknowledged follower and drop it from the synchronous set once it falls behind by a configured margin, trading the durability guarantee for availability for the duration of the exclusion.
Degrading without lying
The hard part of synchronous replication is not the wait, it is the behaviour when the wait cannot be satisfied. Three strategies exist and they differ in what they tell the client.
Block. The commit waits indefinitely. Durability is preserved exactly; availability is lost as soon as the required set is unreachable. A single-client commit can hang until an operator intervenes, which is unacceptable for a system that must serve requests, and it is the reason most implementations add a timeout.
Timeout and fail. The commit returns an error after a bounded wait. The client cannot tell whether the write eventually landed, so the safe client behaviour is to retry, and retry requires the operation to be idempotent or the client to be able to detect a duplicate. This is the least ambiguous option and the one that pushes work onto the application.
Timeout and degrade. The commit succeeds asynchronously after the timeout. The client is told the write is durable when it is only durable on the leader. This is the option that silently changes the meaning of the acknowledgement, and it is worth knowing which mode a given deployment is in, because the difference only becomes visible during the failure it was supposed to protect against.
The important design point is that the mode must be observable. A system whose durability guarantee changes based on load or reachability without telling anyone has a guarantee that cannot be relied on for anything. Exposing the current synchronous state, and the identity of the machines in the acknowledged set, converts an invisible policy into a measurable one.
# The three outcomes a commit coordinator must distinguish.
def commit(txn, required, deadline):
txn.append_local_log() # durable on the leader
acks = wait_for_acks(txn.position, required, deadline)
if len(acks) >= required:
return "durable" # promise includes the replicas
if strict:
txn.abort() # nothing acknowledged
raise NotDurable(txn.position)
return "leader_only" # promise weakened, must be visible
The read side of the same boundary
A read has its own boundary, and it is usually weaker than the write’s. A read served by the leader sees every acknowledged write, since the leader applied them before acknowledging. A read served by a follower sees the follower’s position, which may be behind the leader’s, so it can miss writes that were acknowledged before the read started. That is replication lag, and it is not a bug in the read path — it is the read path choosing a weaker consistency boundary in exchange for spreading load.
Systems that offer read-your-writes across a replica boundary do so by carrying the write position with the client’s session and asking the replica to wait until it reaches that position. The wait is bounded by the replica’s apply rate; if the replica is far behind, the read either blocks or is served from the leader. Both outcomes are honest, and both cost something. The alternative — routing the read to whichever replica answers fastest — produces the familiar anomaly where a user’s own update is invisible to them moments after it was confirmed.
The boundary also determines what a failover can promise. If commits were acknowledged synchronously, a new leader that was in the acknowledged set has them, and the failover loses nothing. If commits were acknowledged asynchronously, the new leader may lack the tail of the log, and those commits are gone even though clients were told they happened. The gap between the two is exactly the amount of data the asynchronous mode was willing to risk, and it is the number to quote when the question is what a failover costs.
References
- PostgreSQL Documentation — Synchronous Replication — PostgreSQL Global Development Group
- Designing Data-Intensive Applications, Chapter 5: Replication — Martin Kleppmann, O'Reilly Media, 2017
- The Part-Time Parliament — Leslie Lamport, ACM Transactions on Computer Systems 16(2), 1998