DEV Community

Cover image for How to Choose the Right Database for Development: Understanding Replication Models
Timofei Ivankov
Timofei Ivankov

Posted on

How to Choose the Right Database for Development: Understanding Replication Models

Introduction

Replication is keeping copies of the same data on several nodes connected over a network. Let me be straight up front: choosing a database almost never starts with the replication model. What usually comes first is the data model and the shape of your queries, your team's expertise, whether a managed version exists in your cloud, and the maturity of the ecosystem. Replication becomes the deciding factor in a few per cent of projects — the ones with hard requirements on geo-distribution, RPO/RTO or write scale. But you always need to understand it, because it is what determines what happens to your data when a node dies and which guarantees you have the right to promise the business.

Flowchart: replication splits into three independent axes. Topology — who may accept a write (single node, several nodes, any node) — determines whether write conflicts are possible. Commit protocol — when the client is told

It solves several problems, and conflating them is harmful, because the solutions differ:

  • keeping data closer to users, cutting query latency
  • staying up when individual nodes fail, raising availability
  • scaling reads by spreading SELECTs across replicas

What replication does not do is scale writes. That takes sharding (partitioning) — cutting the data into non-overlapping pieces across different nodes. It is an orthogonal thing: replication makes copies of the same data, sharding lays out different data. Almost every large system does both: the data is sharded, and each shard is replicated. When you hear "this database scales writes linearly", it is almost always sharding that is meant.

Replication is described by three independent axes, not one.

  • Topology — who is allowed to accept a write: one node, several nodes, or any node.
  • Mechanism — what actually travels over the wire: SQL statements, the physical log, or logical row changes.
  • Acknowledgement mode — when the client is told "ok": asynchronously, semi-synchronously, synchronously, or by quorum.

Real systems are combinations. Galera is "several leaders + logical write-set + synchronous certification". Cassandra at QUORUM is "leaderless + logical mutations + quorum acknowledgement". This is exactly why statements like "Cassandra is AP and Postgres is CP" are meaningless without naming the settings. The discussion below follows the first axis, but most practical decisions are made on the second and third.

The popular formulation "pick any two of three: Consistency, Availability, Partition tolerance" is inaccurate, and Eric Brewer himself publicly disowned it in "CAP Twelve Years Later" (IEEE Computer, 2012). The problem is that P is not a property you choose: networks break, packets are lost, nodes go into a GC pause and look dead. Giving up partition tolerance means declaring that you run on a perfect network, and no such option exists.

The correct formulation (following the Gilbert and Lynch proof, 2002) reads: during a network partition a system must choose — either keep answering requests, at the risk of returning stale or diverging data (A), or refuse service until connectivity returns in order to preserve linearizability (C). About all the remaining time — which is 99.9% of a system's life — CAP says nothing at all.

For everyday decisions, then, PACELC (Daniel Abadi, 2010/2012) is more useful: if **Partition then **A* or C, Else Latency or Consistency*. That is, under partition choose between availability and consistency, and the rest of the time between latency and consistency. The second half is the one you live with every day: every strong guarantee is paid for in network round trips.

System and settings Under partition In normal operation Verdict
PostgreSQL/MySQL, async replicas PA EL PA/EL
PostgreSQL + Patroni, synchronous quorum PC EC PC/EC
MongoDB, w: "majority" PC EC PC/EC
Cassandra, CL=ONE PA EL PA/EL
Cassandra, CL=QUORUM no such cell see below
DynamoDB PC on writes EL on ordinary reads PC/EL
Spanner, CockroachDB PC EC PC/EC

Note that Cassandra occupies two rows. The same database lands in different cells depending on the settings of the request — which is the main argument against the labels "AP database" and "CP database".

And its second row simply has no cell in PACELC, which is worth spelling out. CL=QUORUM is conventionally filed under PC/EC, but that is wrong. Under partition Cassandra does refuse service to the minority (not A), and in normal operation it does pay latency, waiting for the slowest of R replicas (not L). Except that it gives no linearizability in either branch — exactly why is worked out in the leaderless section. So it sacrifices both A and L and gets no C in return: PACELC provides no cell for that. This is a defect of the framework's binary nature rather than a property of the product, and that is how it should be remembered, not by the letters.

The word "consistency" means different things, and half the confusion in this area comes from that.

  • The C in ACID — integrity in terms of business rules: the balance adds up, foreign keys do not dangle. A property of your transactions, unrelated to replication.
  • Transaction isolation — Read Committed, Snapshot, Serializable. About what concurrent transactions see of one another.
  • The consistency model of a distributed system — what a client is guaranteed to see on a read. That is the subject here, and it is what the letter C in CAP denotes.

For the last one, a hierarchy from strong to weak is useful:

  • Linearizability — the system behaves as if there were a single copy: an acknowledged write is immediately visible to every subsequent read. This is the C in CAP.
  • Causal consistency — the order of causally related operations is preserved (a comment will not appear before its post). The strongest model attainable while staying available during partitions.
  • Session guarantees (Terry et al., 1994): read-your-writes — you see your own writes immediately; monotonic reads — data does not "travel backwards in time"; monotonic writes; writes-follow-reads.
  • Eventual consistency — "if writes stop, the replicas will converge eventually". It says neither when, nor that there will be no rollback to an older value along the way.

Three pairs get confused most often and are worth separating. Linearizability is not serializability: the first is about real time (a new value, once read, is not un-read by later reads), the second is about equivalence to some serial order of transactions and does not constrain real time at all. Strict serializability is the sum of both, and that is what Spanner provides. Snapshot isolation does not give serializability — it breaks on write skew, and "we have SI, so our transactions are isolated" is false.

The practical takeaway: do not ask "is this database consistent?". Ask "which consistency model, under which settings, and on which operations".

The taxonomy that follows is the standard one, from chapter 5 of Kleppmann; I did not invent it, and I expand it on current examples with concrete numbers. Within the first model, its modern variant — where the leader is elected by consensus — gets its own treatment: in practice that is the mainstream option today.

Single-leader replication

Schematic of single-leader replication: all client writes go to one leader node, which applies them locally and streams the change log to its followers, while reads can be served from any node.

An illustration of single-leader replication

How it works

In the single-leader model one node of the cluster is designated as the leader (also master, primary). Every write from a client goes exclusively to that leader. The leader performs the write locally, records the change in its log and propagates the change stream to the remaining nodes — the followers (also replica, secondary). Replicas apply the changes they receive to their own copies in the same order in which the leader applied them. Reads can be served from any node, but reading from a lagging replica may return stale data.

From there almost everything is decided by two parameters that usually slip past unnoticed.

The replication mechanism — what actually travels over the wire

Flowchart comparing three replication stream formats. Statement-based ships SQL text, breaks on NOW(), RAND(), UUID() and auto-increments, and is effectively dead. Physical (WAL/redo) ships log page bytes and is cheap on CPU, but leader and replica must run the same major version. Logical (row-based) ships row changes decoupled from physical storage, which enables zero-downtime upgrades, replicating a subset of data, and CDC into external systems.

Statement-based: the SQL statement itself is shipped to the replica. Compact, but dangerous — any non-determinism breaks the replica. NOW(), RAND(), UUID(), auto-increments and triggers with side effects will produce different results on leader and replica, and the data will silently diverge. MySQL historically worked this way and has used ROW by default since 5.7.

Physical (log shipping / streaming): the bytes of the write-ahead log travel — which bytes changed in which blocks. Maximally reliable and cheap on CPU; the replica matches the leader byte for byte. The downside decides everything: the log is tied to the physical storage format, so leader and replica must be on the same major version. Upgrading PostgreSQL from 15 to 16 through physical replication is impossible. This is exactly how standard streaming replication works in PostgreSQL (since version 9.0; before that there was only file-based log shipping, introduced in 8.2).

Logical (row-based / logical decoding): changes travel at the row level, and the format is decoupled from the physical on-disk representation. That opens up three important things: replication between different major versions (that is, zero-downtime upgrades), replication of a subset of the data, and replication into external systems — into Kafka, into an analytical store, into a search index. All modern CDC (Debezium and relatives) is built on this. In PostgreSQL it is publication/subscription since version 10, in MySQL binlog_format=ROW, and in MongoDB the whole oplog is logical by nature, with change streams built on top of it.

The practical takeaway: if a zero-downtime major upgrade or integration with external systems is anywhere on your roadmap, then sane logical replication becomes a selection criterion no less important than topology.

The acknowledgement mode — when the client is told "ok"

Sequence diagram of a single write path with four possible acknowledgement points. Async: the leader answers right after its local commit, so RPO is greater than zero. Semi-sync: after the first replica acknowledges, RPO near zero at the cost of one RTT. Quorum: after a majority acknowledges, RPO zero while the quorum is alive. Sync-on-all: only after the last replica, which makes availability the product of every node uptime.

It determines your RPO (how much data you lose when a node dies suddenly):

Mode The leader answers the client… RPO on loss of the leader Cost of a write
Asynchronous right after the local commit > 0: everything within the lag is lost 0
Semi-synchronous after acknowledgement from N replicas (usually 1) ≈ 0 on single-node failure +1 RTT to the nearest replica
Synchronous on all after acknowledgement from every replica 0 +1 RTT to the slowest one
Quorum after acknowledgement from a majority 0 while the quorum is alive +1 RTT to the median replica

The key point: the "on all" synchronous mode is not merely slow, it also reduces availability. One replica goes down for maintenance and writes on the leader stop entirely. In production the right answer is therefore almost always quorum: RPO=0 and survival of a minority failure.

# PostgreSQL: quorum-based synchronous replication
synchronous_standby_names = 'ANY 1 (replica1, replica2, replica3)'
synchronous_commit = on     # off | local | remote_write | on | remote_apply
# remote_apply - the replica has also applied the change; needed for read-your-writes,
# but more expensive: we wait for the apply, not just for the log write
Enter fullscreen mode Exit fullscreen mode
# MySQL 8.0.26+: semi-synchronous replication
rpl_semi_sync_source_enabled = ON
rpl_semi_sync_source_wait_for_replica_count = 1
rpl_semi_sync_source_wait_point = AFTER_SYNC   # safer than AFTER_COMMIT
rpl_semi_sync_source_timeout = 1000            # ms until silent degradation to async
Enter fullscreen mode Exit fullscreen mode

Note the last line: on timeout MySQL silently switches to asynchronous mode, and your RPO becomes greater than zero. Monitoring Rpl_semi_sync_source_status is mandatory.

// MongoDB: quorum write (the default behaviour since version 5.0)
db.orders.insertOne(doc, { writeConcern: { w: "majority", j: true, wtimeout: 5000 } })
Enter fullscreen mode Exit fullscreen mode

On orders of magnitude: a synchronous acknowledgement costs one network round trip. Within a single availability zone that is ~0.2–0.5 ms, between zones of one region ~0.5–2 ms — acceptable almost always. Between regions (Frankfurt ↔ Virginia ~85–95 ms, Europe ↔ Sydney ~250–300 ms) it already changes the architecture of the application. Hence the rule: synchronous replication within a region, asynchronous between regions.

Databases that use it

The single-leader model is the most widespread and is supported out of the box by many databases. Practically every classical relational system uses it: PostgreSQL (streaming replication since version 9.0) and MySQL both let you configure one primary server and several replicas. The same approach is used in Oracle (Data Guard) and Microsoft SQL Server (Always On availability groups). Among NoSQL systems MongoDB uses this model: its documentation states directly that the primary node alone accepts all write operations, while secondaries asynchronously replicate the operation log (oplog) and apply it to their own data. The approach is also called active-passive or master-slave replication.

There is no automatic failover in the base configurations.

  • PostgreSQL has no built-in automatic failover at all, in any form. An external layer is required: Patroni, repmgr, pg_auto_failover, or equivalent plumbing inside a managed service.
  • MySQL with classic binlog replication — also none; Orchestrator or MHA are needed.
  • MongoDB — automatic failover exists, because since version 3.2 it uses a Raft-like leader election protocol.

If you deploy PostgreSQL "just with a replica" and expect the cluster to survive the death of the master on its own — it will not.

The modern variant: a leader elected by consensus (Raft/Paxos)

This variant deserves its own discussion, because today it has effectively displaced the classic "master + async replica", and most of the properties described below as drawbacks are absent from it.

Formally it is still a single leader. But the way that leader is elected and acknowledges writes is different:

  • The leader is elected by a majority vote and receives a term (or epoch). It leads for as long as it keeps sending heartbeats; if those stop, a new election begins with a new, higher term number.
  • A write commits by quorum — the leader acknowledges to the client as soon as the write has been accepted by a majority, not by everyone.
  • A node from an old term cannot do damage: writes carrying a stale term number are rejected, which makes classic split-brain impossible.

What this buys compared with the classic scheme:

Problem of classic single-leader What consensus does about it
No automatic failover, external plumbing required Leader election is built into the protocol
RPO > 0 with asynchronous replication RPO = 0: what was acknowledged was accepted by a majority
Split-brain on sloppy fencing Impossible: an old term loses to a newer one
"On all" synchronous mode kills writes A majority suffices; a minority failure does not block

The price is one quorum network round trip per write. Inside a region that is a fraction of a millisecond; a quorum stretched across continents makes every write as long as an inter-regional RTT, and that often comes as an unpleasant surprise.

Where you meet it: etcd, Consul, ZooKeeper (the ZAB protocol) — half the Kubernetes world stands on those; MongoDB since 3.2; MySQL Group Replication / InnoDB Cluster since 5.7.17 (XCom, a Paxos variant, under the hood); Kafka KRaft. A large class of its own is distributed SQL, where sharding is combined with consensus per shard: Google Spanner (a Paxos group per split; TrueTime and commit-wait give strict serializability), CockroachDB (Raft per range), YugabyteDB (Raft per tablet), TiDB/TiKV (Multi-Raft). Contrary to common belief, Amazon DynamoDB belongs here too — more on that in the leaderless section.

Distributed SQL deserves a closer look, because it is the answer to "transactions and write scale at the same time"

Diagram of a distributed SQL cluster: the application sees one SQL database, while the data is split into shards and each shard is its own consensus group with its own leader and two followers. A single-shard transaction costs one consensus round of roughly 1 to 5 milliseconds; a cross-shard transaction costs 2PC on top of consensus and is 2 to 4 times more expensive.

None of the three base topologies solves it — only a combination of them does: the data is sharded into ranges or hash buckets, each shard becomes its own consensus group with its own leader, and cross-shard transactions go through 2PC on top of consensus. That last part is the central idea, not an implementation detail. Classic 2PC blocks when the coordinator fails: participants that voted "yes" can neither commit nor abort until it comes back. The fix is precisely that the coordinator's state is itself replicated by consensus, so its failure is survived by electing a new one. Without that, NewSQL would look like "2PC, only slower".

What you pay is that a cross-shard transaction costs more than a single-shard one: single-shard is one consensus round (~1–5 ms inside a region), cross-shard with 2PC is at least two rounds plus coordination, typically 2–4× more expensive and noticeably worse in the tail. The choice of sharding key once again determines performance.

And an important correction about guarantees, one that must not be generalised to the class. Of all four, only Spanner provides strict serializability, and it is bought with TrueTime: the API returns an interval with a guaranteed uncertainty bound ε, and a transaction at commit waits for that to elapse (commit-wait, ~2ε) so that the timestamp is guaranteed to lie in the past for any observer. Serializability itself Spanner obtains by ordinary means — 2PL, 2PC and Paxos ordering within the group; TrueTime adds precisely the real-time component. CockroachDB, YugabyteDB and TiDB do without atomic clocks — using hybrid logical clocks with a configurable maximum offset (in CockroachDB --max-offset, default 500 ms) and uncertainty restarts instead of commit-wait. Cheaper, and it works anywhere, but it requires watching NTP: a node whose clock has drifted too far relative to half the cluster shuts itself down. It is the boundedness of the skew, rather than an architectural choice, that keeps all three from being strict.

Two caveats from practice. An even number of nodes is useless: a cluster of 4 survives as many failures as one of 3 (one), while requiring the agreement of three instead of two. Always 3, 5 or 7; for a cheap "half node" there are witness/arbiter roles that vote but store no data. And a quorum commit does not automatically make follower reads linearizable — those need explicit semantics (CockroachDB, for instance, offers follower reads "from the nearest replica, but N seconds behind", and that is an excellent trade-off where it fits).

What you are actually buying

Not simplicity and not maturity — those get named first, but they can be had elsewhere. What you are buying is a single, universally shared order of changes: every acknowledged write went through one node, concurrent transactions were serialized there by ordinary locking and MVCC, and the replicas merely replayed the result. From that follows the thing the model is kept for: the database gives you everything you are used to — multi-statement transactions, foreign keys, unique indexes, SERIALIZABLE. In the other two models this is substantially worse, and global uniqueness is unattainable in principle.

The rest comes as a bonus. The model has been studied for decades and is implemented everywhere; adding a replica means initialising a copy and connecting it to the leader; and to the application the system looks like one database for writes plus extra nodes to speed up reads.

Replicas take SELECT load off the leader, and adding replicas really does increase read capacity. The caveat that breaks plans: every replica applies 100% of the leader's write stream. Under a write-heavy load replicas are bound not by reads but by log application — and at that point new replicas no longer add read capacity, only lag grows. It is made worse by the fact that application is often single-threaded: in PostgreSQL the WAL is replayed by a single startup process, and it cannot be parallelised. MySQL does have parallel application (replica_parallel_workers with replica_parallel_type=LOGICAL_CLOCK, by writeset in 8.0) — one of the few places where MySQL is objectively stronger.

What it costs

Part of the bill is obvious and irreducible. The leader dies and writes stop until the switchover completes; consensus shortens the pause to seconds but does not remove it. Write throughput is bounded by one node: even with dozens of replicas you will not insert faster, and horizontally that is cured only by sharding (which is how distributed SQL is built). And with asynchronous replication the death of the leader destroys the last transactions acknowledged to the user — the leader returned "success" and died milliseconds later, and the new leader knows nothing about them. That last one is fixed by semi-synchronous or quorum mode, and it is the first thing to configure in production.

Beyond that comes what costs money and attention.

Replication lag and stale data on replicas. With asynchronous replication there is a lag between the commit on the leader and the appearance of the data on replicas, and reads from replicas during that window return stale information. This violates several session guarantees at once:

  • read-your-writes: the user saved a profile, moved to the profile page which reads from a replica, and did not see their own changes;
  • monotonic reads: two consecutive requests landed on replicas with different lag, and the data "travelled backwards in time";
  • consistent prefix: under sharding, an answer can arrive before its question.

Causes of growing lag that need monitoring: write spikes, long queries on the replica (in PostgreSQL — recovery conflicts, hot_standby_feedback and max_standby_streaming_delay), a slow replica disk, single-threaded application.

The standard ways to get read-your-writes, in increasing order of cost:

  • Read from the leader after a write within the session. Simple, but it defeats the point of replicas.
  • LSN/GTID routing: remember the log position after the write and route reads only to a replica that has reached it. In PostgreSQL, compare pg_current_wal_lsn() with pg_last_wal_replay_lsn(); in MySQL, WAIT_FOR_EXECUTED_GTID_SET().
  • Causally consistent sessions: in MongoDB this is built in since version 3.6 — the driver passes afterClusterTime, and the secondary waits for the required point before answering.

And readConcern: "majority" in MongoDB does not solve the lag problem. It guarantees durability — what you read will not be rolled back on a leader switch — but not freshness, and it is fully entitled to return stale data. For read-your-writes you need causally consistent sessions or a read from the primary.

Split-brain on a sloppy switchover

Sequence diagram of a split-brain caused by a garbage-collection pause. The leader holds a lease key with a TTL in etcd. An eight-second GC pause freezes it, the TTL expires by wall-clock time, a replica takes the key and becomes leader, and both the new and the frozen old leader then accept writes and acknowledge them to clients. Without fencing, one branch of history has to be thrown away.

If the old leader has not realised it was deposed, it will keep accepting writes that will later have to be thrown away. Fencing is therefore not optional: a leadership lease with a bounded term, revoking the VIP, powering the node off through IPMI or a cloud API, super_read_only. Patroni uses an external distributed store (etcd/Consul/ZooKeeper) as the source of truth, with a leader key and a TTL — a node that loses the key demotes itself.

And here it matters what exactly breaks that lease. Not clock drift, but pauses. That same eight-second GC freezes the leader, the lease expires by wall-clock time, a new leader is elected, and the old one wakes up still believing its lease is valid. The mechanism requires both bounded drift in clock rate and the absence of pauses longer than its margin — this is the canonical scenario and one of the main sources of Jepsen findings.

The first minute of an incident: three things to know in advance. They are not invented on the spot.

  • Do not destroy the evidence. Before fixing anything, take a disk snapshot (or copy the log) from the old primary. pg_rewind cuts off a branch of history, and after it the acknowledged transactions cease to exist. A snapshot costs a minute; recovering the impossible costs nothing, because it is impossible.
  • If there turn out to be two primaries, that is split-brain, and the first action is to isolate one of them: revoke the VIP, the security group, power it off. Before any attempt to fix the data, not after.
  • What not to do: promote the second node without being certain the first is dead and isolated; return the old primary to the cluster without pg_rewind or a rebuild — you will get two branches of history; drop a replication slot "to free up space" without understanding who is hanging off it.

Stale reads from the leader itself — that is, reading from the leader does not by itself give linearizability. A deposed leader that has not yet learned of it serves stale data with complete confidence. This is cured by lease/ReadIndex mechanisms in Raft systems, or, in MongoDB, by readConcern: "linearizable" — which performs a bookkeeping write to confirm leadership and is therefore expensive.

The default for most systems — and what pushes you out of it

The model fits everywhere that strict consistency and integrity are needed and the writes fit into one node: finance, accounting, orders, CMS, any application with invariants and transactions. That is the overwhelming majority of systems.

At the same time, "fits into one node" deserves a sober estimate: a modern server comfortably handles tens of thousands of transactions per second. Most systems that "outgrew Postgres" in fact outgrew unoptimised queries and missing indexes.

Replication here is used mainly to raise availability (hot standby replicas in case of failure) and to spread read load. Geographically distributed systems often start with exactly this model: the primary leader in one region, and only replicas in the others for fast local reads, while writes still go to the main region. Data integrity is paid for in write latency for remote users — a trade-off that is justified more often than it seems: reads outnumber writes by an order of magnitude.

Asynchronous single-leader is not CP

The two variants of the model behave in opposite ways under partition, and they must not be conflated.

Classic asynchronous replication. The claim "under a network partition, writes stop in order to preserve consistency" is false for it. A master cut off from the other nodes, not yet aware that it is cut off, will keep accepting and acknowledging writes — and after the switch to a new leader that data will have to be thrown away. Such a configuration is neither C nor A in the strict sense: it guarantees neither linearizability nor the durability of what it acknowledged. That, and not "slow failover", is what makes it dangerous.

The consensus variant really is CP. A leader that has lost contact with the majority cannot commit a new write and demotes itself after a timeout. The minority stops serving writes, the majority elects a new leader and carries on. The system sacrifices the availability of the minority for linearizability — exactly the choice CAP describes.

Read availability is high in both variants: even when some replicas are unreachable, the remaining ones keep answering (with the caveat about data freshness). Fault tolerance comes from having copies: with quorum acknowledgement, acknowledged data survives the death of a minority of nodes with certainty; with asynchronous acknowledgement, with a probability that depends on the lag.

The ceilings: reads, writes, latency

Scalability. Reads scale horizontally up to a ceiling set by the write volume (replicas apply the entire change stream). Writes do not scale at all — only vertically, or through sharding, which means moving to distributed SQL or to manual data partitioning.

It is convenient to write down here, once, the formula that will hold for all three topologies:

write ceiling ≈ N / (number of copies of each write)

Full replication, where every write travels to all N nodes, gives a ceiling of one node no matter how many nodes there are. At RF=3 on thirty nodes the ceiling is the same as on ten. From this follows immediately both why sharding remains the only way to scale writes, and why partially replicating configurations are a legitimate intermediate design rather than a half-measure.

Development complexity. The lowest of all the models. There is no need to think about conflicting updates — the system guarantees the order of transactions. The one real difficulty is handling replication lag correctly on reads, and that is a design decision to be made in advance rather than discovered in production.

Latency. Write latency is the network hop to the leader plus the commit on it; in synchronous or quorum mode one RTT is added. For globally distributed clients, users far from the leader get increased write latency. Reads can be very fast from the nearest replica, but when freshness is required you have to go to the leader.

Availability under failure — in numbers:

Parameter Value
RPO, asynchronous replication > 0, equal to the lag (usually ms; minutes during an incident)
RPO, semi-synchronous / quorum ≈ 0 on single-node failure
RPO, consensus 0 for acknowledged writes on a minority failure
RTO, manual switchover minutes to hours
RTO, Patroni (defaults ttl=30, loop_wait=10) ~30 s; tuning brings it to 10–15 s
RTO, MongoDB (electionTimeoutMillis=10000) ~10–14 s
RTO, etcd (election timeout 1000 ms) ~1–2 s
Synchronous acknowledgement within an availability zone +0.2–0.5 ms
Synchronous acknowledgement between zones of a region +0.5–2 ms
Quorum across three regions of a continent +30–70 ms

Multi-leader replication

Three multi-leader exchange topologies. A ring preserves ordering better but one node failure breaks the delivery chain. A star is simple to set up but the hub is a single point of failure again. All-to-all needs O(N squared) connections and its paths run at different speeds, so an UPDATE can arrive before its own INSERT.

Ring, star and all-to-all: the three ways multi-leader nodes exchange changes

How it works

In the multi-leader model (also master-master, active/active) there is no single designated master — every node in the cluster can accept writes. Each acts independently for its own local clients, after which the changes are delivered asynchronously to the rest: nodes exchange change logs with one another over a ring, star or all-to-all topology. The system aims for every participant to eventually receive every change. There is no single order of operations, and that determines all of the model's properties.

And the main point first, because it decides whether this direction is worth looking at at all: multi-leader does not increase write throughput

This is a direct consequence of the formula from the previous section: here every write has N copies, so the ceiling is N/N, that is, one node. In a cluster of three leaders each accepts its own third of the writes — but must then apply the writes of the other two, and ends up doing exactly the same amount of work as a single master. In fact slightly more: certification, version metadata and exchange traffic have been added.

The only way to actually scale writes is sharding: different data is written to different nodes, and then a node does not have to apply anyone else's. But that is partitioning rather than multi-leadership, and it works in any topology.

So multi-leader is needed not for performance but for local write latency and for surviving a link failure between sites. Everything else is overhead, and the model should be judged by those two criteria.

Consistency and conflicts

Sequence diagram of a write conflict. A client in Frankfurt and a client in Virginia update the same profile simultaneously on the EU and US leaders, and both are told

The main difficulty of multi-leader is that the same data can be modified simultaneously on different nodes. Because replication is asynchronous, mutually contradictory transactions are discovered after the fact, during the log exchange — when both have already been applied locally and both have been acknowledged to their clients. There is no rolling back: the user has already been told "saved".

In a single-leader database this does not happen: the second concurrent transaction either waits or fails with an error, but it does not cause the data to diverge. And if you defer acknowledgement until global agreement, the point of multiple leaders disappears — it reduces to distributed consensus. Multi-master implementations therefore rely on asynchronous conflict detection and subsequent resolution.

Subtler and worse is the violation of causality in the topology. In an all-to-all topology different network paths have different speeds, and the message about an UPDATE of a row can overtake the message about its INSERT: the replica receives an update to a row that does not exist. Ring and star topologies preserve order better, but they have a different problem — the loss of one node breaks the delivery chain. Production systems solve this with version stamps and buffering "until the cause arrives", but this is far from implemented everywhere, and it is worth checking explicitly whether such a mechanism is present.

Conflict resolution strategies

Last Write Wins. The write with the larger timestamp wins. Simple — and almost always wrong. When clocks have drifted apart by seconds (and NTP drift between sites is normal), the "last" one will be the write from the clock that has run furthest ahead, not the one that was actually last. And the losing write is destroyed silently: not logged, not preserved, and the user is told nothing — they saw "saved", and the data is gone. LWW is used by DynamoDB Global Tables and Cassandra. It is a deliberate trade of simplicity and speed for silent data loss. If silent data loss is unacceptable to you, LWW is not an option.

Version vectors and version merging. Each replica keeps a counter, and by comparing vectors the system determines whether change B was causally after A or concurrent with it. Concurrent versions are both preserved (siblings) and handed to the application for explicit resolution — or merged automatically by a known rule. Nothing is lost, but the application must be able to merge, and that work cannot be skipped. This is how the original Dynamo worked. Riak later moved to dotted version vectors, which eliminate false conflicts.

CRDTs (conflict-free replicated data types). Structures built so that merging concurrent changes is mathematically deterministic and loses no contributions: counters (G-Counter, PN-Counter), sets (OR-Set), registers, lists for text. Conflicts do not arise by construction.

The limitation that decides everything here: a CRDT solves the merge problem, not the invariant problem. A warehouse stock counter will converge beautifully to the correct sum — and will just as calmly go negative, because the condition "stock ≥ 0" is inexpressible in a CRDT without coordination. Live implementations: Riak Data Types, Redis Enterprise CRDB, Azure Cosmos DB, and Automerge and Yjs for collaborative editors.

Partitioning ownership (preventing conflicts at the design level). The best way to deal with conflicts is to make them impossible. Assign every entity to one "home" node: EU users write to Frankfurt, US users to Virginia, and a given user's data is edited only at home. There are no conflicts because there are no concurrent writes to the same object. Formally this is sharding again — by geography. It is what makes a multi-leader configuration manageable. Many databases have settings that force particular tables or partitions to be single-master even inside a multi-master cluster.

Conflicts are not the whole story. Global uniqueness and auto-increments do not work in multi-leader in principle — this is not a shortcoming of particular implementations but a consequence of the absence of coordination. Two nodes will independently hand out id=1000 or register the same e-mail address, and on merge you get a collision with nothing left to resolve it. The cure is UUID/ULID, per-node key ranges, or a separate identifier service. Multi-object transactions are practically unavailable: atomicity on one node does not give atomicity after the merge.

The one property everything else is paid for

There is exactly one unique property here: when the link between sites fails, each side keeps accepting writes from its own clients. No other topology can do that — consensus in the minority will refuse, single-leader without a leader will stall. The failure of an individual node also does not stop writes: clients move to a neighbour and carry on.

Everything else the model is credited with grows from that same property. A user in Tokyo writes to the Tokyo node in 2 ms instead of 250 ms to Frankfurt — for interactive applications that is the difference between "works" and "does not work". Several data centres serve load simultaneously instead of idling in wait for a switchover. And the extreme case: notes, calendars, task managers, field applications with no connectivity — here every device is the leader of its own copy, and CouchDB was designed for exactly this. Offline is the model's most honest and least disputable niche.

The bill for that property

No global invariants. Uniqueness, non-negative balances, "exactly one active booking", a balance that adds up — none of these can be expressed. This is not "difficult", it is unsuitable: if your domain has such invariants, multi-leader is out without discussion.

Conflicts you will have to resolve by hand. The application needs merge handlers, conflict logs, notifications to the responsible systems. Even with automatic strategies (LWW, CRDT) the risk of an incorrect merge remains, especially for interrelated entities. On top of that there is the constant background of eventual consistency: different nodes hold different state at any given moment, and a client reading in two locations will see two versions.

Operations and debugging. Bidirectional replication is harder to configure: you have to prevent updates from circulating forever (usually with source identifiers) and be able to diagnose divergence — the question "why do nodes A and B hold different data" turns into an investigation. Testing distributed failures, that is, breaking the link between data centres under load, is a hard problem in its own right.

Coordination overhead, and here one has to be precise. The total network volume at the same write rate is roughly the same as in single-leader: every change still has to be delivered to N−1 nodes. The cost is not in the bandwidth but in conflict metadata and version vectors, in the cost of certification and merging, and in the quadratic growth of connection count in an all-to-all topology. The last of these is solved by a ring or a star, but then propagation latency grows and a dependency on intermediate nodes appears.

When it is justified, and when it is self-deception

Geographically distributed active data centres (active-active). Companies unwilling to keep a passive standby configure several data centres to work simultaneously: primary active databases in Europe and the US each serve their own region, with changes replicated between them. This survives the loss of one data centre without downtime and improves response time for local users. Multi-master inside a single data centre makes no sense — consensus is the right fit there.

Offline mode and device synchronisation. CouchDB together with PouchDB is one of the few mature stacks for offline-first applications. Its model is honest about conflicts: a revision tree, a deterministic "winner" for reads by default, but all competing versions are preserved in _conflicts, and the application is obliged to sort them out. Nothing disappears silently — the price is that you write the resolution code.

Integrating different systems: bidirectional replication between databases during migrations and source consolidation. External tools help here — Oracle GoldenGate, pglogical and the like.

On implementations, with precise wording. Galera Cluster (MariaDB Galera, Percona XtraDB Cluster) is often described as "synchronous multi-master replication". It is more accurate to say virtually synchronous, certification-based: the transaction executes locally, on commit the write-set is broadcast to all nodes and certified in a global order, but it is applied on the other nodes asynchronously. Hence the practical consequences: a read from another node can return stale data unless wsrep_sync_wait is set, and a conflict reaches the application at commit time as a deadlock error, so the application must be able to retry. Plus the constraints: InnoDB only, primary keys mandatory on every table, large transactions problematic. The Galera developers' own recommendation for hot tables is to write to a single node, which is fairly telling.

MySQL also has a built-in option: Group Replication in multi-primary mode (since 5.7.17) — certification on top of the Paxos-like XCom. PostgreSQL has no multi-master in the standard distribution; the options are EDB Postgres Distributed (formerly BDR/2ndQuadrant, commercial), the open pgEdge/Spock, pglogical, Bucardo. All of them require careful attention to conflicts.

AP by construction

Multi-leader systems emphasise availability and partition tolerance, sacrificing strict consistency. Under a network partition each segment goes on serving its own clients autonomously — the classic choice of A over C, and in CAP terms such systems are classified as AP. The price is temporary divergence between segments and conflicts that will have to be resolved after convergence.

Variants exist in which, on detecting a partition, some nodes block operations to avoid conflicts — but then the model's only real advantage is lost, and it is more sensible to take consensus instead. If you choose multi-leader, you have to be prepared to live with divergence in exchange for availability; otherwise the choice was made wrongly.

RPO here is always greater than zero

At the node level fault tolerance is high: the failure of an individual server has almost no effect, the rest keep accepting writes, and the loss of a whole data centre does not destroy data provided it was duplicated. But if a node dies before it has replicated its transactions, those changes survive nowhere. RPO here is always greater than zero — unlike quorum schemes, where what was acknowledged survives the death of a minority with certainty. This is not a failure mode but a normal property of the model.

Once connectivity returns, a convergence phase begins — potentially with a large number of conflicts. At that moment it may be necessary to suspend some operations temporarily or put the application into read-only mode until the divergences are cleared. That phase has to be planned in advance rather than met for the first time during an outage.

Scale, complexity, latency

Scalability. For writes there is none (see the arithmetic above); gains come only from sharding the data across "home" nodes. A realistic number of active leaders is 2–4. Clusters with dozens of active masters are not encountered, and not by accident.

Development complexity. The highest of all the models. You have to anticipate conflict situations, implement resolution strategies, do without global uniqueness and transactions, and sometimes build a separate coordination layer (distributed locks, identifier generators, manual recovery). Testing distributed failures is mandatory and laborious. The model is used by experienced teams and only where there is a justified need.

Latency. The local write is minimal, as on a single node, and that is the main prize. But data written in one region appears in another with the replication delay: RTT plus apply time, that is, tens to hundreds of milliseconds. If a user in the US updates their profile and a second later their friend in Europe opens the page, there is a good chance of seeing the old version. Multi-master improves local latency but does not remove inter-regional propagation delay.

Availability under failure — in numbers:

Parameter Value
Cluster write throughput ≈ that of one node (does not grow without sharding)
Local write latency as on a single node — the main advantage
Time until a write is visible in another region RTT + apply: tens to hundreds of ms
RPO on losing a node before replication > 0 always
RTO ≈ 0: the remaining nodes keep accepting writes
Write availability on a link failure between DCs preserved in each segment
Realistic number of active leaders 2–4

An old rule worth keeping in mind: the safest multi-master is the one the application writes to as if it were a single master. If you have arrived at that, you probably do not need multi-master.

Leaderless replication

Diagram of a quorum read and write over three replicas. A write waits for W=2 acknowledgements and a read queries R=2 replicas, so W + R = 4 > N = 3 and the sets are guaranteed to overlap in at least one replica holding the latest value. The overlap is not linearizability: it breaks on sloppy quorum and hinted handoff, two concurrent writes, a read racing a write, a partially failed write that is never rolled back, and a node restored from an old snapshot.

How it works

The leaderless model does away with the notion of a master node entirely: every node is equal, and a client operation is handled by a set of several nodes. On a write the client (or a coordinating node) sends the request to several replicas at once and waits for acknowledgement from W of them. On a read it queries R replicas and picks the freshest version.

With a total of N copies, the condition W + R > N means that the read set and the write set overlap in at least one node — that is, the read will see at least one replica holding the latest write. The typical configuration is N=3, W=2, R=2. Relaxing the parameters to W=1, R=1 gives you maximum speed and availability while losing the freshness guarantee entirely. That is precisely where the model's flexibility lies: the balance between consistency and availability is configurable, and in some systems per request.

Lagging replicas are caught up by two mechanisms. Read repair: a version divergence is detected on a read, and the fresh version is written back to the lagging replica. Anti-entropy: a background process compares data ranges using Merkle trees and levels out divergences. Plus hinted handoff: a coordinator that could not reach the intended replica records a hint and delivers the change later.

And the main point about quorums: W + R > N does not give linearizability. It is a useful heuristic, not a guarantee. The situations where it fails (the list follows Kleppmann, ch. 5):

  • Sloppy quorum. If, when the "home" replicas are unavailable, the system writes to any W available nodes, the overlap of the sets is not ensured and the rule falls apart. That is exactly what hinted handoff does. The mechanism is useful, but presenting it as a strengthening of consistency is wrong — it weakens it. A nuance for Cassandra: its quorum is stricter than Dynamo's — hints do not count towards the consistency level at any level except the single CL=ANY. That is, QUORUM in Cassandra remains strict, while ANY does not. The difference between them is the difference between "the data is definitely on two replicas" and "the data may be sitting as a hint on a node that has nothing to do with it".
  • Two concurrent writes. The quorum does not determine which one won; that is decided by conflict resolution, which most often means LWW with all its consequences.
  • A concurrent read and write. The reader may see the new value, or may see the old one — there is no guarantee.
  • A write that reached fewer than W nodes is not rolled back. The client got an error, but the data was partially written, and subsequent reads may see it.
  • Restoring a node from an old snapshot reduces the number of replicas holding the new value below W.

The conclusion: if you need linearizability, quorums will not give it to you — you need consensus. In Cassandra that means lightweight transactions on Paxos (since version 2.0), and they are several times more expensive than an ordinary write.

Since there is no leader and no global order of operations, two clients can update the same record simultaneously on different nodes. As in multi-leader, conflict resolution strategies are required. Most often LWW is used: every update is stamped with a timestamp, and on merge the version with the highest stamp is chosen. This is what Apache Cassandra does. It is fast and simple, but the losing value disappears without trace — with all the clock-skew risks described in the previous section. Alternatively, Riak supports keeping siblings — several competing versions handed to the client for manual resolution; nothing is lost, but usage gets heavier.

The trade-off dial, and its price

The first advantage usually named for this model is the absence of a single point of failure. That is a consequence, not the essence. The essence is that the consistency level is set per operation: a bonus deduction is written at QUORUM, telemetry at ONE, in the very same table. One database serves workloads with completely different prices for consistency, and you do not need two stores. No other model offers that.

The consequences are pleasant. There is no leader, so there is nothing to re-elect: a node drops out, the coordinator goes to another replica, the client notices nothing, and a rolling restart of the whole cluster without a single error becomes routine. A new node picks up part of the ranges and joins on its own, which simplifies autoscaling. Replicas are laid out across data centres, and LOCAL_QUORUM gives a quorum inside your own DC with asynchronous exchange between them: local latency, survival of a region loss, eventual consistency between regions. Just note that this is already a hybrid — leaderless inside the DC and, in effect, multi-leader between DCs, with all the conflict properties of the latter.

Horizontal scalability — but the reason has to be named correctly. The scale comes from sharding, not from leaderlessness. Cassandra hashes the partition key and distributes ranges across nodes. Each key is written only to the RF nodes responsible for its range, and not to the whole cluster. In terms of the same formula, this is the only one of the three models where the denominator is not N: the ceiling here is N/RF and grows with cluster size. Leaderless replication within a range simply does not get in the way of that.

The same reasoning gives the assessment of the claim "double the nodes and you double the performance". For operations by partition key it is close to the truth. It falls apart on queries without a partition key, hot partitions, excessively large partitions, compaction load, and tombstone-heavy patterns. That is, it is not a property of the database but a property of the data model: design it correctly and the claim works as a guide; design it otherwise and it does not work at all.

The bill for that flexibility

Weak consistency by default. If the quorums are not set to strict mode, the data converges gradually: client A wrote and got an acknowledgement, client B read from another node almost immediately and saw the old value. The application has to be designed for eventual consistency — reads may not reflect the last commit, retries may produce duplicates, and both concurrent operations may be applied.

The same global invariants missing in multi-leader are missing here too — but they break differently. INSERT ... IF NOT EXISTS works through Paxos, which is several round trips and several times more expensive than an ordinary write, and on hot keys it also scales poorly. A plain x = x + 1 in an eventually consistent store loses updates: if two increments received close timestamps, one of them wins. You need counter columns, with their own restrictions — they cannot live in the same table as ordinary columns and cannot be rolled back.

Tombstones are operational pain number one. A delete does not delete: it writes a marker that lives for gc_grace_seconds (864000 by default, that is 10 days) and only then gets collected by compaction. The consequences: deleted data continues to occupy space; a query passing over a large number of tombstones degrades, up to timing out; and, most importantly, if repair did not complete within gc_grace, deleted data comes back to life. Anti-entropy repair itself is a heavy, recurring procedure on large datasets, one that has to be planned and monitored.

The cost of quorum operations. A write waits for acknowledgement from W nodes, which means it takes as long as the slowest of the required ones. Inside a single data centre that is single-digit milliseconds — noticeable but acceptable. A quorum stretched between regions adds tens to hundreds of milliseconds, so operations that require consistency are kept inside a region.

The mechanics of a quorum read work like this. The coordinator sends a full data request to one replica (usually the fastest by snitch) and digest requests to the rest, waits for CL responses, compares the digests, and on divergence requests the full data and performs a blocking read repair. It cannot answer "as soon as I saw the fresh version" — it is obliged to wait for CL responses, otherwise the quorum guarantee does not hold. (In Cassandra 4.0 the probabilistic background read repair was removed; the blocking one remains.) There is also a latency optimisation: speculative retry duplicates the request to another replica if the first one answers too slowly.

Potential data loss with unlucky settings. Freedom of configuration is also freedom to get it wrong. At W=1 a write is acknowledged after landing on one node, and that node's death before propagation destroys the data for good. At R=1 it is easy to read a stale value. And even a correct W+R>N is no panacea: under a network partition the smaller part of the cluster will not assemble a quorum and will refuse service. For example, at N=5, W=3, R=3 and a 3+2 split, the two-node segment becomes unavailable — consistency preserved for the larger component at the cost of availability for the smaller. The fundamental CAP choice does not go anywhere; leaderless merely gives you a tool for making it dynamically.

No global transactions and a constrained data model. Most leaderless databases are key-value or wide-column stores without complex transactions, joins or arbitrary WHERE clauses. A table is designed for a specific query, data is denormalised and duplicated, and a new kind of query means a new table and a data migration. For a team used to SQL this is the most underestimated line item, and a mistake in the data model at the start is cured by re-loading the entire dataset.

Key-addressed streams yes, bank ledgers no

Leaderless works excellently in large-scale services where the request rate is enormous and the required consistency is eventual: clickstream analytics and logs, monitoring and metrics, event feeds, IoT telemetry, time series. Here the loss of a small amount of data or a delay in convergence is not critical, while downtime of the system is unacceptable.

It is worth being precise about actual deployments, because the classic list was assembled during the project's early years and has changed since. Cassandra was created at Facebook for Inbox Search, but Facebook Messages moved to HBase in 2010, and inside Meta today Cassandra lives in Instagram. Twitter announced in 2010 that tweets were not migrating to it, staying on MySQL/Gizzard and later moving to Manhattan. What remains in place from the classic list is Netflix, where Cassandra has for many years been the primary operational store.

There are not many live representatives of the model today. Apache Cassandra and ScyllaDB (rewritten in C++, with Raft for schema and topology in 5.x) are active. Riak is the canonical Dynamo implementation with siblings and CRDTs, but Basho went into receivership in 2017: the code is open and the project lives on community effort, so adopting it for a new system takes a separate justification. Voldemort (LinkedIn) is archived; LinkedIn moved to its own Venice and Espresso.

DynamoDB deserves separate treatment: it shares its name with the model's progenitor but not its architecture.

  • Dynamo — the Amazon paper of 2007: a leaderless store with sloppy quorum, vector clocks and siblings. It is what gave rise to the model, inspiring Cassandra and Riak.
  • DynamoDB — the commercial service launched in 2012. According to Amazon's own paper at USENIX ATC 2022 ("Amazon DynamoDB: A Scalable, Predictably Performant, and Fully Managed NoSQL Database Service"), a table is split into partitions, each partition is served by a replication group, a leader is elected inside it through Multi-Paxos, and all writes go through that leader.

Architecturally this is sharding plus consensus replication with a leader per partition — the same construction as CockroachDB or Spanner. What it shares with the Dynamo of the paper is the name and the style of the API.

That also explains the rest of the service's behaviour: why ConsistentRead=true is possible at all (we read from the leader) and why it costs twice as much as an ordinary read (1 RCU against 0.5 per 4 KB), and why TransactWriteItems with genuine atomicity exists (since 2018). Global Tables, meanwhile, is a separate feature for multi-region active-active replication with LWW conflict resolution, and that one does belong to multi-leader. One system on two different levels: consensus inside a region, multi-leadership between regions.

Leaderless is not suitable where a strict order of operations and invariants are required — bank accounts, stock balances, bookings. You can work around this, but at the price of scaffolding that usually costs more than picking a suitable database in the first place.

The AP label depends on the settings of the request

By default leaderless systems are classified as AP: when connectivity is impaired, the cluster tries to keep answering, sometimes with stale data, rather than stopping. But, as the PACELC table in the introduction shows, the label depends on the settings: at CL=ONE Cassandra behaves as AP, at CL=QUORUM as PC/EC, and at CL=ALL effectively as CP. Consistency here is not a property of the database but a parameter of the request, and that is its strength.

Partition tolerance is a base property: the system is designed to survive a network split and to recover afterwards. Under partition the majority of nodes forms a quorum and carries on, while the minority either refuses service (with a strict quorum) or keeps serving requests at the price of future conflicts (with a relaxed one).

There is no RTO as a quantity here, and RPO depends on W

Fault tolerance is very high: the data is duplicated across several nodes, and to lose it every replica of a particular range has to fail. When some nodes fail, the cluster automatically routes requests to the remaining copies — the coordinator does not wait for a dead node but goes straight to the others, so the user often does not notice the incident at all. Leader switchover does not exist as a phenomenon, so there is no RTO as a separate quantity either.

A caveat about RPO: it equals zero only with quorum writes. At W=1 an acknowledged write lives on a single node, and its sudden death means irreversible loss. This is the most common misconfiguration — setting W=1 for speed and assuming that replication protects the data.

Scale, latency, development

Horizontal scale here comes from sharding, and leaderless replication ensures that node failures do not get in its way: there are no central bottlenecks. Hence the niche — large volumes and high load with a simple access pattern.

The data model is designed from the queries rather than from the domain, and that is the main thing to be ready for. On latency, such databases are optimised for small quorums; as consistency requirements grow, latency grows too, but it can still beat a trip to a single leader across an ocean.

In numbers:

Parameter Value
Typical configuration N=3, W=QUORUM(2), R=QUORUM(2)
Failures survived at N=3, W=R=2 1 node
RTO ≈ 0; leader switchover does not exist as a phenomenon
RPO at W=QUORUM 0 on a minority failure
RPO at W=ONE > 0: the node died before propagation, the data is gone
QUORUM write latency inside a DC single-digit ms
LWT (Paxos) latency several times an ordinary write
gc_grace_seconds default 864000 (10 days) — repair must fit inside it

Conclusion

Decision tree for choosing a replication model. It starts with who will operate the system: with nobody to fix it at three in the morning, the answer is managed single-leader. From there it branches on whether the domain has global invariants, whether the writes fit into one node, the required RPO, whether writes are needed during a link failure between sites or offline, and whether access is key-only at terabyte scale — leading to single-leader with quorum or async, distributed SQL, multi-leader, or leaderless.

There is no single answer, but the choice is far more determined than it looks.

If strict consistency, transactions and data integrity matter to you and the write load fits into one server — take a single-leader database: PostgreSQL, MySQL, MongoDB. That is the overwhelming majority of systems, and there is no need to be shy about the choice: a modern server handles tens of thousands of transactions per second. You get familiar transaction semantics, integrity, and read scaling through replicas.

But do the two things that usually get postponed: configure quorum or semi-synchronous acknowledgement (otherwise you lose acknowledged data when the master dies) and install automatic failover — Patroni for PostgreSQL, Orchestrator or InnoDB Cluster for MySQL. A bare replica without automatic switchover is a manual-recovery tool, not fault tolerance. One question tests it: what happens at three in the morning if the master dies? If the answer contains the words "the on-call engineer logs in and…", your RTO is measured in hours.

If a single writing node is genuinely exhausted — move to distributed SQL: CockroachDB, YugabyteDB, TiDB, Spanner. You keep transactions and uniqueness, paying with consensus latency, the cost of cross-shard transactions and noticeably higher operational complexity. The key word is "genuinely": taking Spanner or CockroachDB for a load that one PostgreSQL handles is a common and expensive mistake.

If the system is spread across several active sites and has to keep working when the link between them breaks, or if users make changes offline — look at multi-leader: CouchDB, MySQL Group Replication in multi-primary, Galera, pgEdge, DynamoDB Global Tables. Apply it deliberately. It will not add write throughput. You will lose global uniqueness and multi-object transactions. Conflicts will have to be designed, implemented and tested — budget time for that. The best strategy is to assign every entity to a "home" node, so that concurrent writes to the same object become a rare exception.

If you are facing web-scale problems — terabytes of data, hundreds of thousands of operations per second, a simple key-based access pattern, acceptable eventual consistency — leaderless fits: Cassandra or ScyllaDB. Make sure your application's data model maps onto key-value or wide columns, and that someone on the team understands compaction, repair and tombstones: without that the cluster degrades within six months. For device telemetry Cassandra does excellently — it will keep absorbing data even if some nodes drop out. For bank ledgers, using an AP store will require scaffolding and will most likely prove unjustified.

A summary table for comparison:

Single leader (async) Consensus (Raft/Paxos) Multi-leader Leaderless (quorum)
Write scale 1 node 1 node per shard 1 node (sharding required) linear, thanks to sharding
Write conflicts impossible impossible the main problem present, usually LWW
Global uniqueness yes yes no only through LWT
Transactions yes yes practically none no
RPO > 0 0 > 0 0 at W=quorum
RTO minutes seconds ≈ 0 ≈ 0
Write latency local to the leader +1 quorum RTT local everywhere +1 quorum RTT
Survives a link failure between DCs no majority only yes, both sides majority only
Development complexity low low high medium to high
PACELC PA/EL PC/EC PA/EL configurable

And the questions whose answers almost uniquely determine the model. The first one cancels half of the rest, which is why it comes first:

  • Who is going to operate this? A team with nobody to diagnose replica divergence at three in the morning cannot afford either multi-leader or leaderless — regardless of what the points below say.
  • How much data are you willing to lose when a node dies suddenly (RPO)? Zero requires a quorum. Seconds allow semi-sync. "A little" means async, but make sure the business has confirmed it.
  • How long may writes be unavailable (RTO)? Seconds require consensus or leaderless. Minutes are covered by a single leader with automatic failover. Hours allow manual switchover.
  • Are writes really needed in several regions? Users in different regions are not yet an argument: local replicas serve reads perfectly well. The argument is when write latency affects the product, or when a link failure must not stop the business.
  • Are there global invariants? If yes, multi-leader and leaderless are out, and that is not a question of effort.
  • Do the writes fit into one node? Measure the peak rate and the volume before complicating the architecture.
  • Does the application tolerate stale reads? If not, decide in advance how: reads from the leader, LSN/GTID routing, or causally consistent sessions.
  • What kinds of queries do you need? Joins and aggregates mean the relational model. Key-only access opens up all of NoSQL.
  • Do you need zero-downtime upgrades and CDC? If yes, you need sane logical replication. A separate criterion, not derivable from the topology.

What holds true whichever model you choose

A replica is not a backup. Replication — any replication, including consensus with RPO=0 — does not protect against logical corruption. A DELETE without a WHERE, a DROP TABLE in the wrong console, a badly rolled-out migration, an application bug: all of it replicates correctly, quickly and straight to every copy. The more reliable your replication, the faster and more reliably your mistake reaches every replica.

What does protect against it is something else:

  • PITR — a base backup plus the log archive, restoring to a point before the mistake. This is a separate RPO axis, and it has to be set independently of the replication one.
  • A delayed replica. recovery_min_apply_delay = '1h' in PostgreSQL, SOURCE_DELAY in MySQL. Cheaper than a full restore, and it gives you an hour to notice the DROP TABLE.
  • Periodic logical backups — the only protection against corruption at the storage-format level, which a physical backup will faithfully copy along with the defect.
  • Verifying the restore. A backup you have never restored from is a hypothesis, not a backup.

And how one actually gains confidence that the guarantees are real. Everything written above about guarantees is derived from vendor documentation and papers, and that source cannot be trusted. The industry has developed three ways of checking.

  • Failure testing — Jepsen (Kyle Kingsbury, jepsen.io): distributed databases are run under network partitions, process pauses and clock jumps, and time after time a gap is found between the declared and the actual guarantees. Practical advice: find the report for your database and your version before you rely on a guarantee. The absence of a report is information too.
  • Formal verification. Raft, Paxos and MongoDB's replication have been verified in TLA+ and Ivy. This answers the question "why do we believe the protocol is correct", but what was verified is the protocol, not your build.
  • Deterministic simulation testing. FoundationDB, TigerBeetle, Antithesis: the cluster is run inside a simulation where time, network and disks are controlled, and failure scenarios are enumerated with a reproducible seed.

The cheapest thing you can do today is a drill. Break the network between zones (iptables -j DROP, tc netem for degradation rather than an honest failure), SIGSTOP the leader for thirty seconds, shift the clock, fill the disk. A failover that has never been tested is a hypothesis.

And back to where this started

The replication model is rarely the first criterion when choosing a database — that was said at the very beginning, and it still holds. But it is the only criterion that determines which promises about data durability you have the right to make to the business. Everything else you can change along the way: the data model you will bend with queries, a managed service you will swap for self-hosted, people you will train. This one, only by migration.

And if you cannot answer the questions above about your current configuration right now — you did not choose it, it just happened.

Top comments (0)