Skip to content

Fundamentals

Consistency

How nodes agree, from linearizable reads and CAP to clocks, consensus, Raft, two-phase commit, and sagas.

Concise article — AI can help

Copy this prompt and paste it into your AI chat:

Help me study this article for practical system design.
Article: https://tech0ver.github.io/sysdes/fundamentals/consistency.md
Ask my goal and preferred depth: a quick recap, a walkthrough, or a deeper discussion. Use the article examples, flag your additions, and let me set the pace.

Or give your AI the article link with your own prompt:

Every design that keeps data on several nodes must decide what a read may return and how the nodes settle on one answer. To defend it, say what each operation must see and how the nodes agree, how a change across nodes finishes safely, and what each guarantee costs.

A wallet app keeps balances on one leaderLeaderThe replica that accepts writes and decides their order. It sends each change to the followers through the replication log. Products also call it the primary or the source.Read more: Replication: Keeping a copy on another node with followersFollowerA replica that applies the changes of a leader in the same order. It serves reads and can become the leader after a failover. Products also call it a secondary, a standby, or a read replica.Read more: Replication: Keeping a copy on another node in other zones. Anna sends Ben $50. The transfer commitsCommitThe end of a transaction that makes all of its changes permanent at once. Other transactions see the changes only after the commit. A committed change is final: undoing it takes a new transaction that does the opposite.Read more: Transactions: Changing several rows at once on the leader, and a notification service pushes “Anna sent you $50” to Ben’s phone. Ben opens the app. His balance comes from a follower that has not applied the transfer yet, and the money is not there.

A push arrives before the follower has the transfer Anna sends Ben $50. The leader commits the transfer and answers Anna. A push tells Ben that Anna sent him $50. Ben reads his balance from a follower that has not applied the transfer yet and gets $100. The transfer reaches the follower only later. ANNA LEADER FOLLOWER BEN Ben: $100 SEND $50 TO BEN Ben: $150 SENT PUSH: ANNA SENT YOU $50 READ BALANCE $100 TRANSFER, LATE

Read-your-writes does not help: the write is Anna’s, and Ben reads because a signal reached him by another path. Ben needs a store that acts as if each record had only one copy.

Linearizability gives that. All reads and writes fit one order, as if they ran one at a time on a single copy, and each takes effect at some moment between its request and its answer. So once a write has finished, a read that starts later returns that value or a newer one, whichever copy it reaches. Reads that overlap the write may return the old value or the new one, but once a read has returned the new value, a read that starts after it cannot return the old one, unless another write has set it back. Some product docs call this strong consistency; check what exactly they promise.

Reads that overlap a write The balance is 100, and a write sets it to 150. Read A overlaps the write and returns 150. Read B starts after read A has returned and still overlaps the write. Read C starts after the write has finished. Left, linearizable: B and C return 150. Right, not linearizable: B returns 100, going back after A has already seen 150. Linearizable the balance was 100 each box runs from the request to the answer WRITE READS write 150 read A: 150 read B: 150 read C: 150 B and C return 150 Not linearizable the balance was 100 each box runs from the request to the answer WRITE READS write 150 read A: 150 read B: 100 read C: 150 B goes back to 100

It costs coordination between nodes, so ask for it only where an old answer does harm:

  • A read that follows a signal about a write: a push, a queue message with a record ID, a link to a file that was just uploaded. The receiver reads at once, from whatever copy it reaches.
  • A check that guards a rule: a balance that must not go below zero, a handle that only one user may take. A fresh read alone is not enough here, since two requests can both read “enough money” before either writes. Make the check and the change one atomicAtomicityThe rule that a set of changes is applied fully or not at all. A failure in the middle never leaves part of the changes behind. A transaction gives it to several rows, and most stores give it to a single record.Read more: Transactions: Changing several rows at once operation, such as a conditional write or compare-and-setCompare-and-setA write that succeeds only if the value is still the one the writer read. If another writer changed it first, the write does nothing, and the writer reads again and retries.Read more: Transactions: Two writers on one value, or a transactionTransactionA group of reads and writes that the database applies as one unit. Either all of its changes happen or none do, and concurrent transactions do not see each other's half-done work.Read more: Transactions: Changing several rows at once at a fitting isolation levelIsolationHow much concurrent transactions are kept from seeing or overwriting each other's work. Each level, from read committed to serializable, blocks a different set of anomalies. Stronger levels cost waits or retries.Read more: Transactions: Seeing unfinished changes (more on checking, then writing). If that operation is linearizable, it works on a state that includes every write that finished before it.
  • Deciding who acts: a lockLockA mark that reserves a row or a range of rows for one transaction and makes conflicting transactions wait. A write takes an exclusive lock on its row until the transaction ends, and readers can share a shared lock. Locks prevent conflicting writes but cause waits and deadlocks.Read more: Transactions: Seeing unfinished changes, or the choice of a leader. As with a rule, “read that it is free, then take it” must be one conditional operation.

A history view or a like count may lag by a second if users can live with that.

One node can give linearizable operations, though reads from an old snapshotSnapshotThe state of data at one moment. An in-memory store saves one to disk for recovery and replays the log written after it. A transaction can read from one, so data does not change under it.Read more: Storage engines: Back to memory or from a cacheCacheA copy of data or results that is faster to read than the original and can be rebuilt from it. It cuts latency and load on the source while it answers most reads. Losing it makes a system slower but not wrong.Read more: Caching: When a cache pays off in front of it still return older data. Among the setups from replicationReplicationKeeping a copy of the same data on several nodes. Copies let the data survive the loss of a node, spread reads, and sit close to users. The hard part is keeping them in step while the data changes.Read more: Replication: Keeping a copy on another node, one leader gives them when reads and writes go to it, failoverFailoverMoving work to another instance when the active one fails. In replication, a follower that has every confirmed write becomes the leader. A timeout only suspects the failure, so the old leader must be fenced.Read more: Replication: Replacing a failed leader keeps every confirmed write, and the leader confirms that it still leads before it answers: a leader cut off from the others may not know that it was replaced. A follower gives them only when it first learns from the current leader which writes have finished and waits until it has applied them. Independent leaders cannot, since each can accept a write before the others learn about it. LeaderlessLeaderless replicationReplication where any replica accepts writes and clients write to and read from several replicas. No leader promotion is needed when a replica fails. Reads and writes go on while enough replicas answer; concurrent writes still need a conflict rule.Read more: Replication: Writing without a leader quorumsQuorumThe number of replicas that must answer a read or a write. With N replicas, W write answers, and R read answers, W + R > N makes the read and write sets overlap, so a read usually reaches the latest confirmed write. It is not a strong guarantee.Read more: Replication: Writing without a leader cannot either, even with W + R > N: a write that reached only some replicasReplicaOne copy of the data in a replicated system. In a leader-based design, a replica is either the leader or a follower.Read more: Replication: Keeping a copy on another node can show up in one read and be missing from a later one (more on quorums).

Which replication setups give linearizable operations Four panels. One leader: a client reads and writes on the leader, which confirms with the followers that it still leads before it answers; linearizable. Follower read: the follower asks the leader how far writes have finished, then waits until it has applied them; linearizable. Independent leaders: each accepts a write before the other learns about it; not linearizable. Leaderless quorum with N = 3, W = 2, R = 2: a write of v2 is still running and only replica A has saved it; a read of A and B returns v2, and a later read of B and C returns v1; not linearizable. One leader reads and writes go to the leader Follower Leader Follower Linearizable Client READ, WRITE CONFIRMS IT STILL LEADS BEFORE IT ANSWERS Follower read the client reads from a follower Leader Follower Follower Linearizable Client READ ASKS HOW FAR WRITES FINISHED WAITS UNTIL IT HAS APPLIED THEM Independent leaders each region has its own leader Leader Leader Not linearizable Client Client WRITE $50 WRITE $80 LATER EACH ACCEPTS A WRITE BEFORE THE OTHER LEARNS OF IT Leaderless quorum N = 3, W = 2, R = 2 Not linearizable Client A: v2 B: v1 C: v1 WRITE v2, STILL RUNNING READ A, B: v2 LATER READ B, C: v1

Here “consistency” means what a read may return when data has several copies. In transactions, the same word means that data follows its rules (more on consistency in transactions). Serializable isolationSerializableThe isolation level at which concurrent transactions end as if they had run one after another. It rules out every anomaly, at the cost of waits or retries. It covers only transactions that run at this level.Read more: Transactions: Running as if one at a time is also something else: transactions act as if they ran one after another, but their order need not match real time, so serializable alone does not promise fresh reads. Linearizability is about each record and real time: a read returns a value at least as new as every write that finished before the read began.

Use linearizable reads where a read follows a signal about a write. Use atomic conditional writes or a fitting transaction where a check guards a rule or decides who acts. Serve other reads from the nearest copy that meets their own guarantee.

Keeping one order of operations across copies needs the copies to reach each other.

The wallet now serves users in the US too. The leader stays in Europe, and the US region has followers. The link between the regions goes down for ten minutes. A user in New York sends $20, and the US nodes cannot reach the leader.

A break that splits nodes into groups that cannot reach each other is a network partition. The US side has two options: refuse the transfer until the link is back, or accept it on a local copy and reconcile later, when the same money may already have been spent in Europe. A node cannot promise the latest value when it cannot reach the node that decides it.

Refusing or accepting a transfer on the side without the leader Account 42 has $30 in both copies, and the link between Europe and the US is down. A user in Europe and a user in the US each send $20 from it. Left: the US follower refuses its transfer, so only one is accepted and the balance becomes $10. Right: the US copy accepts the transfer as a local write; the merge after the partition keeps both transfers, and the balance is -$10. Refuse on the side without the leader account 42, $30 in both copies EUROPE Leader balance $30 US Follower balance $30 SEND $20 SEND $20 accepted refused Balance: $10 Accept on both sides account 42, $30 in both copies EUROPE Leader balance $30 US US copy balance $30 SEND $20 SEND $20, LOCAL WRITE accepted accepted Merge keeps both: −$10

CAP states this as a theorem: during a network partition, a system with copies cannot both stay linearizable and complete every request on every node that has not failed. C stands for linearizability. A stands for availability in a narrow sense: every request to a working node eventually completes as the operation defines it. A correct “not enough money” counts; an “unavailable” error or a made-up refusal does not. A is not an uptime percentage, and it sets no time limit. P stands for partition tolerance, and a design cannot drop it, because networks do break. So the popular “pick two of three” is misleading: the choice is between C and A, and only while the network is split.

A node sees a partition only as a timeout: it cannot tell a broken link from a slow one. A node that keeps waiting for the other side chooses consistency, and a node that gives up and answers from its own copy chooses availability. Nodes may also disagree on whether a partition is happening at all.

The choice can differ per operation, so make it for each one:

  • Transfers on the side without the leader: refuse them. Users see an error, and that side accepts no transfers that conflict with Europe’s.
  • Balances and history: answer from the local copy and show that it may be behind. Stale readsStale readA read that returns an older version of data than the latest write. It happens on a replica that has not applied the write yet. Decide for each read whether an old answer is acceptable.Read more: Replication: Reading from a copy that is behind leave nothing to merge later.
  • Small payments: accept them up to a limit and settle after the partition, the way an ATM keeps working offline. This works only when the loss is capped and a later fix, such as a fee, is acceptable. A stricter variant gives each side its own share in advance, such as a spending limit per region, so neither side can break the rule.
  • Card top-ups on the side without the leader: keep them pending and do not charge the card, because this side cannot record the top-up where every region will see it. The side with the leader keeps charging.

When both sides accept writes that conflict, the copies must merge them after the partition, and the system must repair any rule that the merge broke (more on resolving conflicting writes). Accepting writes on both sides means listing those rules and how to repair each one.

Refusing writes during a partition avoids conflicting writes but turns a network problem into errors for users. Accepting them keeps users working but needs a merge and a repair for every rule that conflicting writes can break.

Calling a whole database “CP” or “AP”. Behavior depends on its settings and on each operation: the same store can refuse writes on the minority side and still serve stale reads there. Say what each operation does during a partition.

There is no partition, but in this setup every transfer from New York still goes to the leader in Europe and waits for its answer: a round tripRound tripThe time for a message to reach the other side and for the answer to come back. Also called RTT (round-trip time). Every handshake costs at least one round trip, so distance between client and server adds up fast.Read more: MDN: Round trip time across the ocean on every write. Reads that must be linearizable go there too.

A transfer from New York waits for Europe; a read that allows stale data does not Left: a client in New York sends a transfer to the leader in Europe and waits for its answer, a round trip across the ocean. Right: the client reads its balance, where stale data is allowed, from the US copy nearby; it waits only for a local round trip. Transfer: to the leader CLIENT, NY US COPY EU LEADER TRANSFER DONE Balance read, staleness allowed CLIENT, NY US COPY EU LEADER READ $150 CROSS-REGION ROUND TRIP LOCAL ROUND TRIP

PACELC adds this to CAP: if there is a Partition, a system trades Availability against Consistency; Else, it trades Latency against Consistency. The second trade-off is the one a system meets every day. Keeping one order of operations means that some nodes must hear from others before they answer, and that wait adds to the latency. A design can move the wait, for example off each read, but cannot remove it. In his 2012 paper, Daniel Abadi put Dynamo-style stores such as Cassandra, in their default setup, in PA/EL: they give up consistency in both cases. VoltDB was PC/EC: it pays for consistency in both.

Between “one copy” and “any copy” lie guarantees that cost less. Azure Cosmos DB offers five of them as settings, from strongest to weakest:

  • Strong: linearizable. Across regions, a write waits until the regions it must reach have confirmed it.
  • Bounded staleness: a read may be behind, but by no more than a set number of versions or seconds.
  • Session: read-your-writesRead-your-writesThe guarantee that a user's reads include the user's own completed writes. Read a user's own data from the leader, or only from replicas that have applied the user's last write.Read more: Replication: Seeing your own writes and writes follow readsWrites follow readsThe guarantee that a write applies after the writes its author had read. A reply is never applied before the post it answers. One leader with an intact log gives it to a user who waits for each write.Read more: Replication: Seeing writes in order within one client’s session.
  • Consistent prefixConsistent prefix readsThe guarantee that readers see writes in the order they happened. One query to one replica of one log gives it. It breaks when a page combines reads taken at different points, such as different shards or two queries.Read more: Replication: Seeing writes in order: reads see writes in order, perhaps late (more on consistent prefix reads). In Cosmos DB, this order holds for writes made together in one transaction.
  • Eventual: copies converge when writes stop, with no promise in between.
What a read may return at each Cosmos DB level Top: one record has versions v1 to v5, v5 is the latest, and the reader itself wrote v2. Strong: the read returns v5. Bounded staleness, in an example bound of two versions: v3, v4, or v5. Session: its own v2 or newer. Bottom: one transaction writes t1 and t2 together. Consistent prefix: a read sees neither or both, never t1 alone. Eventual: any of the three, including t1 alone. ONE RECORD: v1 … v5, v5 IS THE LATEST; THE READER WROTE v2 Strong v1 v2 v3 v4 v5 ONLY THE LATEST Bounded staleness v1 v2 v3 v4 v5 AT MOST 2 VERSIONS BEHIND (EXAMPLE) Session v1 v2 v3 v4 v5 ITS OWN v2 OR NEWER ONE TRANSACTION WRITES t1 AND t2 TOGETHER Consistent prefix neither t1 alone t1 and t2 ALL OF IT OR NONE Eventual neither t1 alone t1 and t2 PART OF IT IS POSSIBLE solid: a read may return it; dashed: it may not guarantees hold within one logical partition

Its docs name session as the most widely used level. Its guarantees hold within one logical partition, and the docs list further limits for each level.

In the wallet, transfers pay the round trip, because the conditional debit must see every earlier transfer. Ben’s balance after the push must include Anna’s transfer, which read-your-writes does not cover. So the notification carries a token for the transfer, such as the log positionLog positionA point in a replication log, such as the number of the last change a replica applied. A new follower starts from the position of its snapshot. The gap between the leader and a follower shows how much log is still to apply.Read more: Replication: Starting a new copy that the database returned at commit, and the balance read on the nearest copy waits until that copy has applied the log up to it. A balance opened without such a signal comes from the nearest copy and shows when it was last updated. History and notifications use a weaker guarantee.

A read that waits for the position named in a push The leader commits Anna's transfer at log position 812 and pushes a notification to Ben that carries the position. Ben reads his balance from the nearest copy and asks for a state that has applied at least 812. The copy has applied the log only up to 810, so it waits until entries 811 and 812 arrive and are applied, then returns $150. LEADER NEAREST COPY BEN commit at 812 applied: 810 PUSH: ANNA SENT $50, POSITION 812 READ: APPLIED ≥ 812 WAITS LOG 811, 812 applied: 812 $150

Pay for linearizability only on the operations that need it, and give the others the weakest guarantee that users will not notice. Put the leader, or the majority of copies that confirms a write, close to most of the writers.

History and notifications are now served from each user’s own region. Anna, in Europe, asks Ben to pay her back $30 for dinner. Ben, in the US, gets a push about the request and pays at once. The payment is written in the US before the request has replicated there, so a friend of Ben’s in the US sees “Ben paid Anna $30” without the request it answers.

A read from one log, at one point in it, sees the writes of that log in order. The problem appears when writes travel by different paths, as with several logs or several regions that accept writes. Ben’s payment depends on the request: he saw it before he paid. A write depends on the client’s earlier operations and on the writes the client had read, and through them on everything those depended on. Two writes where neither depends on the other are concurrent.

Causal consistency guarantees that every node shows writes that depend on each other in the order they happened. Concurrent writesConcurrent writesWrites to the same data where neither knew about the other. Time does not matter: edits hours apart on two offline devices are concurrent. With several leaders or no leader, they conflict and must be resolved.Read more: Replication: Resolving conflicting writes may show up in different orders on different nodes. It includes the guarantees a user gets for their own writes, such as read-your-writes and writes follow reads, and extends them to writes that one user made after seeing another user’s write.

A common way to provide it: each client keeps the versions it has read and written, including those that came with a push, and sends them with every later read and write. A copy serves the read only when it already has all of them, and a region shows a received write only after everything it depends on has arrived. The costs are extra data on every write and a delay until the dependencies arrive, which can last as long as a partition. When a region confirms writes before they reach other regions, losing that region loses confirmed writes.

A payment shown before or after the request it answers Anna's request #7 is written in Europe and replicates to the US slowly. A push carries #7 to Ben, who pays in the US; the payment carries the dependency on #7. A friend of Ben's reads from the US. Left: the US shows writes as they arrive, so the friend sees the payment without the request. Right: the US stores the payment but does not show it until #7 arrives, then shows both. Shown as it arrives EUROPE BEN US FRIEND request #7 PUSH #7 PAY, AFTER #7 payment REQUEST #7 PAYMENT ONLY #7, PAYMENT The friend sees the effect first Shown after its dependencies EUROPE BEN US FRIEND request #7 PUSH #7 PAY, AFTER #7 payment REQUEST #7 NOT SHOWN NOTHING NEW #7, PAYMENT Cause before effect

Causal consistency can keep working during a network partition for a client that stays with a copy that already has everything the client saw. A client that moves to a copy without those writes must wait or go back. Sequential consistency is stronger: all operations fit one order that keeps each client’s own order, but that order need not follow real time between clients. After Anna’s write has finished, Ben may still read the old value, though he never goes back from a value he has seen. Like linearizability, it makes some nodes refuse requests during a partition.

A read of an old value after a finished write The balance was 100. Left, in real time: Anna writes 150, and the write finishes. Then Ben reads 100, the old value, and later reads 150. Right: sequential consistency accepts this, because one order exists that keeps each client's own order: Ben reads 100, Anna writes 150, Ben reads 150. Linearizability does not accept it, because its order must match real time. In real time the balance was 100 ANNA BEN write 150 read: 100 read: 150 Linearizable: not allowed One order that fits KEEPS EACH CLIENT'S ORDER 1. Ben read: 100 2. Anna write 150 3. Ben read: 150 Sequential: allowed

Jepsen’s map shows which guarantees can, in theory, keep working during a network partition:

Guarantee What a read may return In theory, during a network partition
Linearizable a value from one order of all operations that matches real time some nodes must refuse
Sequential a state from one order of all operations that keeps each client’s order some nodes must refuse
Causal writes that depend on each other in order works for a client that stays with a copy holding its past
Read-your-writes at least the client’s own writes works for a client that stays with one copy
Monotonic readsMonotonic readsThe guarantee that a user never sees older data after seeing newer data. Keep each user on one replica while it is up, and track the newest position the user has seen to stay safe after a switch.Read more: Replication: Not going back in time nothing older than the client saw before works on every node
Eventual any version, converging when writes stop works on every node

Use causal consistency for data that people react to, such as messages, comments, and requests, when it is served from several regions. Keep linearizability and atomic conditional writes for checks and decisions.

The history page sorts transfers by the time each server wrote them. Anna gets a refund for a payment. The payment was written on a server whose clock runs 200 ms ahead, and the refund on a server with a correct clock 100 ms later, so the history shows the refund before the payment.

A clock that runs ahead puts a refund before its payment Server A's clock runs 200 ms ahead; server B's clock is correct. Server A writes the payment at real time 10:00:00.000 and stamps it .200. Server B writes the refund 100 ms later and stamps it .100. Sorted by stamp, the history shows the refund first. SERVER A, CLOCK +200 MS SERVER B, CORRECT CLOCK REAL TIME payment real .000, stamp .200 refund real .100, stamp .100 HISTORY, BY STAMP 1. .100 refund 2. .200 payment

Operating systems give two kinds of clock:

  • A time-of-day clock returns the date and time. A sync service such as NTP, the Network Time Protocol, corrects it against servers with better clocks, and a correction can move it forward or back. It also jumps after a pause of the virtual machine, a manual change, or a leap second.
  • A monotonic clock only moves forward. It measures how much time has passed on one machine, such as a timeout. Its values mean nothing on another machine.
Measuring a duration across a leap second with each clock A request starts and its answer comes 0.3 seconds later; a leap second falls between them. The time-of-day clock reads 23:59:59.800 at the start, steps back one second, and reads 23:59:59.100 at the answer, so the duration comes out as -0.7 seconds. The monotonic clock reads 5000.8 and 5001.1 seconds, so the duration is 0.3 seconds. Time-of-day clock start 23:59:59.800 answer 23:59:59.100 LEAP SECOND: STEPS BACK 1 S Duration: −0.7 s Monotonic clock start 5000.8 s answer 5001.1 s KEEPS COUNTING Duration: 0.3 s

Time-of-day clocks on different machines are not guaranteed to agree. Clock skew is the difference between their readings at one moment, and clock drift is the difference in how fast they run, which makes the clock skew grow between syncs. At an assumed drift of 200 parts per million, a clock can gain or lose 6 ms in 30 seconds, or about 17 seconds a day without syncing. NTP often keeps clocks within a millisecond or so on a quiet local network and within tens of milliseconds over the internet, but these are typical values, not guarantees. An ordinary clock reading does not come with a reliable error bound.

Two clocks drifting from real time between syncs Time runs down over 90 seconds, with a sync at 0, 30, and 60 seconds. Across is the offset from real time in milliseconds. Clock A runs fast and gains 6 ms in each 30 seconds; clock B runs slow and loses 4 ms in each 30 seconds. Each sync sets both back to real time. The slope of each line is its drift; the gap between the two clocks at one moment, 8 ms at 24 seconds, is the clock skew. REAL TIME CLOCK B, SLOW CLOCK A, FAST 0 S 30 S SYNC: BACK TO 0 60 S SYNC: BACK TO 0 90 S AT 24 S: SKEW 8 MS DRIFT: +6 MS PER 30 S DRIFT: −4 MS PER 30 S -6 -4 -2 0 +2 +4 +6 OFFSET FROM REAL TIME, MS

A time-of-day clock that goes back breaks code that measures durations with it. On January 1, 2017, a leap second moved the clock back, and Cloudflare’s DNSDomain Name SystemThe system that turns a domain name into an IP address. Before a client can connect to example.com, it asks DNS for the address. Answers are cached for the time set by their TTL. DNS can also spread users across servers or regions by returning different addresses.Read more: Networking: Finding the server servers computed a negative duration. The code that chose an upstream server crashed on it, and about 0.2% of DNS queries failed at the peak.

Timestamps from time-of-day clocks cannot order events that are closer together than the clocks’ error. Two errors follow:

  • A later event sorted before an earlier one, as with the refund.
  • Last write wins keeping the wrong write: a node with a clock that runs behind cannot overwrite a value from a node whose clock runs ahead, and its writes are dropped without an error.
Last write wins drops the later write from a slower clock Server A's clock runs 200 ms ahead; server B's clock is correct. Server A writes v1 at real time .000 with stamp .200. Server B writes v2 100 ms later with stamp .100. Last write wins compares the stamps, keeps v1, and drops v2, the later write, without an error. SERVER A, CLOCK +200 MS SERVER B, CORRECT CLOCK REAL TIME write v1 real .000, stamp .200 write v2 real .100, stamp .100 LAST WRITE WINS kept: v1 (.200) dropped: v2 (.100)

Measure timeouts and durations with a monotonic clock. Do not order events from different nodes by their time-of-day timestamps alone. Monitor clock skew and take a node out of service when its clock drifts too far.

When the clocks cannot order events, the order must come from the events themselves.

The refund service records the refund only after it has received the payment. That message gives an order that no clock error can break.

Within one process that does one thing at a time, events have an order. Between processes, one event comes before another only through messages. Happens-before is that order: A happens before B if A is earlier in the same process, if A is the sending and B the receiving of one message, or if a chain of such steps connects them. When neither happens before the other, the events are concurrent, whatever the clocks say.

Events linked by a message, and an event that is concurrent with them Three processes: payments, refunds, and reports. Payments writes a payment and sends a message; refunds receives it and then writes the refund. The payment happens before the refund through this chain. Reports runs a report with no message to or from the others, so it is concurrent with all four events. PAYMENTS REFUNDS REPORTS payment written message sent PAYMENT DONE received refund written report run ONE CHAIN: HAPPENS BEFORE NO PATH: CONCURRENT

A Lamport clock gives every event a number that respects happens-before:

  1. Each process keeps a counter and adds one for each event, sending included.
  2. Every message carries the sender’s counter.
  3. On receiving a message, a process sets its counter to the larger of its own and the received one, plus one.

If A happens before B, A gets a smaller number. Concurrent events may get the same number, so the pair (counter, process ID) breaks ties and gives all events one sort order that every process computes the same way. In the wallet, the refund is written after its server has received the payment, so the refund gets a larger number and sorts after the payment, whatever the clocks show.

Lamport numbers on the same events, and the one sort order they give Payments (P1): payment 1, message sent 2. Refunds (P2): a check 1, then it receives the message with 2, so the receive gets max(1, 2) + 1 = 3, and the refund gets 4. Reports (P3): report 1, export 2. Sorted by (number, process ID): (1, P1), (1, P2), (1, P3), (2, P1), (2, P3), (3, P2), (4, P2). The refund sorts after the payment. P1, PAYMENTS P2, REFUNDS P3, REPORTS 1 payment 2 sent CARRIES 2 1 check 3 received: max(1, 2) + 1 4 refund 1 report 2 export SORTED BY (NUMBER, PROCESS ID) 1, P1 payment 1, P2 check 1, P3 report 2, P1 sent 2, P3 export 3, P2 received 4, P2 refund

If A happens before B, A’s number is smaller, but a smaller number alone does not prove that A happened before B. To tell concurrent events apart, a vector clock keeps one counter per process:

  • A process adds one to its own counter for each event and sends the whole vector with each message.
  • A receiver takes the larger value of each counter, then adds one to its own.

Vector A comes before B when no counter of A is larger than B’s and at least one is smaller. When each has a counter larger than the other’s, the events are concurrent. With two processes, [2, 0] and [1, 1] are concurrent, and [3, 2] comes after both. It is the same idea as a version vector per record, and it costs a counter per process in every message.

Vector clocks show which events are concurrent Two processes. P1's first event is [1, 0], and it sends a message to P2. P1's next event is [2, 0]. P2 receives the message and gets [1, 1]. Neither of [2, 0] and [1, 1] is before the other, so they are concurrent. P2 then sends a message back, which is an event too: [1, 2]. P1 takes the larger value of each counter and adds one to its own: [3, 2], which comes after both. P1 P2 [1, 0] [2, 0] [1, 1] received [1, 2] sent [3, 2] received [2, 0] AND [1, 1]: CONCURRENT · [3, 2]: AFTER BOTH
Lamport clock Vector clock
Size one counter one counter per process
A happens before B A’s number is smaller no counter of A is larger, and one is smaller
A and B are concurrent not detected detected
Use for one sort order of known events, such as a log merged from several nodes finding writes that conflict, to merge them

Logical clocks order the events that a node already knows about. Anna and another user in the US claim the handle @anna at the same moment, and their requests get the numbers 41 and 42. The region that holds 42 cannot say “yours”: a node it cannot reach may hold 41. Sorting the requests it knows does not prove that no earlier request is missing.

Both regions must give one answer for @anna: one user gets it, the other gets “taken”, and the answer must not change, even if a node crashes right after answering.

Consensus is that agreement. Several nodes propose values and decide on one: no two nodes decide differently, a decision never changes, and the decided value is one that some node proposed. Every node that keeps working eventually decides, under the conditions below. The first three rules are safety: nothing contradictory is ever decided. The last is liveness: a decision eventually comes.

The algorithms in use assume that nodes may crash but do not lie, and they decide by majority. Any two majorities of the same group share at least one node. The protocol keeps durableDurabilityThe promise that a confirmed write survives a crash or a power loss. A write is durable only after a flush, such as fsync, puts it on disk. The store chooses whether to confirm a write before or after that flush.Read more: Storage engines: Surviving a crash votes and logs on every node, so a later majority always learns about an earlier decision and cannot replace it. To keep working after F nodes crash, a group needs at least 2F + 1 voting nodes: 3 survive one crash, and 5 survive two. A group of 4 also survives only one, so groups usually have an odd size. Majorities overlap only within one set of voting nodes, so add or remove voters only through the store’s own procedure for it.

Two majorities of five nodes share a node Five nodes. Nodes 1, 2, and 3 decide that @anna goes to Anna. Later, nodes 3, 4, and 5 form a majority. Node 3 is in both, so the later majority learns about the decision and cannot replace it. DECIDES: @anna → ANNA Node 1 Node 2 Node 3 Node 4 Node 5 A LATER MAJORITY Node 3 is in both
How many crashes groups of 3, 4, and 5 survive Three groups. Three nodes need 2 for a majority and survive one crash. Four nodes need 3 and also survive only one crash. Five nodes need 3 and survive two crashes. 3 nodes MAJORITY: 2 Survives 1 crash 4 nodes MAJORITY: 3 Survives 1 crash 5 nodes MAJORITY: 3 Survives 2 crashes

During a network partition, the side with a majority keeps deciding, and the side without one stops: a majority protocol makes the CAP choice for consistency. For one decision, the group never chooses two values, but it can stop deciding while it cannot form a majority.

A deterministic algorithm cannot promise to finish in a network where messages can take any time, even if only one node may crash. Real algorithms keep decisions safe at all times, and they make progress once messages arrive in time for long enough to keep a stable leader. A wrong timeout delays a decision but never makes the group decide twice.

Consensus can implement a value written once, a compare-and-set, and a log of commands that every node appends in the same order. Other systems build locks and leader election for apps on these decisions. In state machine replication, every replica applies the same deterministic commands in the same order from the same state, so all of them reach the same state. Anything that could differ between replicas, such as a timestamp or a random ID, is chosen once and stored in the command. The wallet’s ledger can be such a log: every replica applies the same transfers in the same order and ends with the same balances.

Replicas that apply one log in one order reach one state All replicas start from Anna $100, Ben $0, Chris $0. One log of commands: 1, transfer t-501, Anna to Ben, $50; 2, transfer t-502, Ben to Chris, $20; 3, fee f-17 of $1 from Anna, at 10:02, with the time chosen once and stored in the command. The whole log goes to every replica; each applies it in order and ends with Anna $49, Ben $30, Chris $20. LOG, AGREED BY CONSENSUS 1 t-501: Anna → Ben $50 2 t-502: Ben → Chris $20 3 f-17: Anna fee $1, 10:02 start: Anna 100 · Ben 0 · Chris 0 the fee's time is chosen once and stored THE WHOLE LOG, IN ORDER Replica 1 Anna 49 · Ben 30 · Chris 20 Replica 2 Anna 49 · Ben 30 · Chris 20 Replica 3 Anna 49 · Ben 30 · Chris 20

Raft, Multi-Paxos, and ZAB give a design the same thing: a log agreed by a majority, with at most one leader that can commit at a time. etcd and CockroachDB use Raft, Spanner uses Paxos, and ZooKeeper uses ZAB, ZooKeeper Atomic Broadcast.

Use consensus for decisions that must be unique and must stay made: who holds a name, a lock, or the leader role, and what order the writes come in. Run it on 3 or 5 voting nodes in different zones, from a proven store, not your own code.

The wallet moves the data that needs one order, the ledger and the handles, to a group of three nodes that run Raft. History and requests still replicate as before. One node of the group is the leader: it orders the writes, and the others follow. When the leader fails, the other two must pick a new one, and only one.

Raft numbers its leaders by term: each election starts a new term, and a term has at most one leader. An election goes like this:

  1. A follower that has heard nothing from the leader for its election timeout becomes a candidate. It starts a new term, votes for itself, and asks the others for votes. Each node picks its timeout at random from a range, again for each election. The Raft paper uses 150 to 300 ms as an example for one data center; production systems set the range from measured delays.
  2. Each node votes once per term, and saves its vote to disk before it answers, so a restart cannot make it vote twice. It votes for the first candidate that asks, unless that candidate’s log is behind its own: a log is behind when its last entry has an older term, or the same term and a lower position.
  3. A candidate with votes from a majority becomes the leader of the term and sends heartbeats so that the others stay followers. When two candidates split the votes, nobody wins, and a new term starts after another random timeout. The random timeouts make a repeat unlikely.
An election after the leader fails Node 1 leads term 3 and sends heartbeats, then fails. Node 2's election timeout of 210 ms runs out first; node 3's is 280 ms. Node 2 starts term 4, saves a vote for itself, and asks node 3 for a vote, which resets node 3's timer. Node 3 saves its vote for node 2 to disk and answers yes. With two of three votes, its own and node 3's, node 2 leads term 4. NODE 1 NODE 2 NODE 3 leader, term 3 HEARTBEAT fails TIMEOUT 210 MS 280 MS RESET TERM 4, VOTES FOR ITSELF VOTE FOR ME? TERM 4 on disk: voted node 2 YES leader: 2 of 3 votes

The vote rule keeps finished writes. A write finishes only once a majority has saved it, and a winner needs votes from a majority. Every voter refuses a candidate whose log is behind its own, and together with the rules for appending to the log, this makes every new leader hold every finished write.

The vote rule keeps a finished write All entries are from term 3. Entry 7 is saved on nodes 1 and 2, a majority, so it has finished. Node 1 fails. In term 4, node 3, whose log ends at 6, votes for itself and asks node 2, which refuses because node 3's log is behind. In term 5, node 2 votes for itself and asks node 3, which votes yes, so node 2 becomes the leader and has entry 7. LOGS, ALL ENTRIES FROM TERM 3 Node 1 1 2 3 4 5 6 7 Node 2 1 2 3 4 5 6 7 Node 3 1 2 3 4 5 6 ENTRY 7 IS ON A MAJORITY NODE 1 FAILS; TWO ELECTIONS Term 4: node 3 asks node 2 no: your log is behind Term 5: node 2 asks node 3 yes: node 2 leads, with entry 7

The term also stops an old leader. A leader that was paused or cut off may still believe it leads. Every message carries its term, and nodes reject messages with an older term, so when the old leader contacts a node that knows a newer term, it learns the term and steps down. This is fencing built into the protocol.

An old leader learns the newer term and steps down Node 1 leads term 3 and pauses. Node 2 becomes the leader of term 4 and sends heartbeats to node 3, which moves to term 4. Node 1 wakes up and still believes it leads: it sends an append for term 3 to node 3. Node 3 rejects it and answers with term 4, and node 1 steps down to follower in term 4. NODE 1 NODE 3 NODE 2 leader, term 3 paused leader, term 4 HEARTBEAT, TERM 4 term 4 APPEND, TERM 3 REJECTED: TERM 4 follower, term 4

Simpler schemes, such as “the node with the highest ID that you can reach is the leader”, elect one leader on each side of a network partition: split brainSplit brainTwo nodes acting as leader at the same time. Both accept writes, and the writes conflict. It happens when an old leader was only slow or cut off.Read more: Replication: Replacing a failed leader. Majority votes avoid it, because only one side can have a majority.

A new election on each side of a partition under two schemes Five nodes are split into a side of three (N1 to N3) and a side of two (N4, N5), and each side holds a new election. Left, the node with the highest ID it can reach becomes the leader: N3 on one side and N5 on the other, two leaders. Right, a leader needs votes from a majority: N3 wins on the side of three, and the side of two cannot elect a leader. An old leader cut off on the small side may still believe it leads, but it cannot commit. Highest reachable ID a new election after the split N1 N2 N3 LEADER N4 N5 LEADER 3 OF 5 2 OF 5 Two leaders Majority vote a new election after the split N1 N2 N3 LEADER N4 N5 3 OF 5 2 OF 5 NO MAJORITY Only the side of 3 elects one

While there is no leader, the group accepts no writes. A simple failover takes about one election timeout plus the election, and split votes or partial failures make it longer. The timeout must be well above the normal round trip between the nodes, or slow messages start needless elections, and well below the time between failures. In 2020, a Cloudflare switch stopped forwarding some traffic while other paths still worked: one etcd node lost its link to the leader but could still reach the third node. It kept starting elections that it could not win, and the group refused writes until the switch recovered, about six minutes later. Raft implementations such as etcd’s offer a pre-vote step against such cases: a node first asks whether it could win and starts a new term only if a majority would vote for it.

A node cut off from the leader only, without and with pre-vote Three nodes. The link between the leader N1 and N3 is down; N2 reaches both. N3's log is behind N2's, so N3 cannot win. Left, without pre-vote: N3 keeps starting elections for terms 5, 6, and so on; N2 refuses each vote but moves to the new term, and N1 steps down when it sees that term, so writes are refused. Right, with pre-vote: N3 first asks whether it could win; N2 says no, no term changes, and the leader keeps working. Without pre-vote N1 LEADER, TERM 4 N2 N3 LOG BEHIND N2 VOTE? TERM 5, 6, … N2: NO, BUT MOVES TO THAT TERM N1 SEES IT AND STEPS DOWN Writes refused With pre-vote N1 LEADER, TERM 4 N2 N3 LOG BEHIND N2 PRE-VOTE: COULD I WIN? N2: NO NO NEW TERM; N1 STAYS THE LEADER The leader keeps working

Run 3 or 5 voting nodes in different zones. Set the election timeout from measured round trips and disk delays. Expect writes to pause for about an election timeout plus the election when a leader fails, and longer after split votes.

Anna sends Ben $50, and the transfer goes to the Raft leader. The leader must make it durable on a majority before it tells Anna that the money is sent.

The leader appends the transfer to its log and sends it to the followers; the same message serves as the heartbeat. Each follower saves the entry and answers. Once a majority, the leader included, has saved an entry from the leader’s current term, that entry and every entry before it are committed: every future leader will have them. The leader applies the transfer to the balances, answers Anna, and tells the followers in its next message how far the log is committed, so they apply it too.

An entry of the current term commits once a majority has saved it The leader of term 4 gets Anna's $50 transfer, saves it as entry 8 of term 4, and sends it to both followers. Follower 1 saves it and answers first: with the leader, two of three nodes have entry 8, so it is committed. The leader applies it to the balances and answers Anna. Follower 2 saves it later. The next message from the leader tells the followers that the log is committed up to 8. ANNA LEADER, TERM 4 FOLLOWER 1 FOLLOWER 2 SEND $50 saved 8 (term 4) APPEND 8 APPEND 8 saved 8 SAVED committed: 2 of 3 applied 8 SENT saved 8 COMMITTED UP TO 8 COMMITTED UP TO 8

An older entry that a new leader finds on a majority is not committed by that alone. It becomes committed when an entry of the new leader’s own term is committed after it, which is why a new leader starts by committing an empty entry.

An older entry commits only with an entry of the current term Columns are log positions 1 to 6; each cell shows the term in which that entry was written. Node 1 has just become the leader of term 3. Left: entry 5, from term 2, is on nodes 1 and 2, a majority, but it is not committed by that alone. Right: the leader appends an empty entry 6 in term 3 and saves it on nodes 1 and 2; once entry 6 is committed, entry 5 is committed with it. Entry 5 on a majority node 1 leads term 3 POSITION 1 2 3 4 5 6 Node 1 t1 t1 t2 t2 t2 Node 2 t1 t1 t2 t2 t2 Node 3 t1 t1 t2 t2 Entry 5: not committed yet After an empty entry of term 3 node 1 leads term 3 POSITION 1 2 3 4 5 6 Node 1 t1 t1 t2 t2 t2 t3 Node 2 t1 t1 t2 t2 t2 t3 Node 3 t1 t1 t2 t2 Entry 6 commits, and 5 with it each cell: the term in which the entry was written

An entry that only a failed leader had saved is not committed and may disappear: the new leader does not have it, and Anna never got an answer for it. Each message from the leader names the position and term of the entry before the new ones. A follower whose log does not match there refuses, and the leader retries from an earlier position. Where an incoming entry conflicts with a follower’s entry, the follower drops that entry and everything after it, then appends the leader’s entries.

A leader repairs a follower whose log differs Columns are log positions 1 to 6; each cell shows the term of the entry. The leader has 5 and 6 from term 3. The follower has 5 and 6 from term 2, written by a leader that failed. The leader sends entry 6 with the previous entry (5, term 3); the follower does not match there and refuses. The leader retries from entry 5 with the previous entry (4, term 2), which matches. The follower drops its 5 and 6 and appends the leader's. POSITION 1 2 3 4 5 6 Leader t1 t1 t1 t2 t3 t3 Follower, before t1 t1 t1 t2 t2 t2 Follower, after t1 t1 t1 t2 t3 t3 APPEND 6, PREVIOUS (5, t3): NO MATCH APPEND 5–6, PREVIOUS (4, t2): MATCH

The leader may also commit the transfer and fail before it answers. Anna’s app retries with the new leader, and without a check the money moves twice. The fix is an idempotency keyIdempotency keyA unique ID the client attaches to a request, so the server can recognize a retry of it. If the same key comes again, the server returns the saved result instead of doing the work twice. Used for payments and orders.Read more: Networking: When a call fails stored in the log with the transfer: a repeat with the same key gets the first answer.

A retry after a leader failed finds its key in the log Anna sends $50 with idempotency key k-77. The old leader commits the transfer with the key and fails before it answers. Anna's app retries with the same key on the new leader, which finds k-77 in the log and returns the first answer instead of moving the money again. ANNA OLD LEADER NEW LEADER SEND $50, KEY k-77 committed with k-77 fails before it answers NO ANSWER RETRY: SEND $50, KEY k-77 k-77 in the log SENT, THE FIRST ANSWER

Reads need care too. A follower may be behind, and a leader may have been replaced without knowing it. Raft gives linearizable reads in two ways:

  • A read barrier, called ReadIndex in some implementations. The leader notes how far the log is committed, confirms with a heartbeat to a majority that it still leads, waits until it has applied the log up to that point, then answers. This costs a round trip, but no disk write. A follower can serve such a read too: it gets that point from the leader, which confirms its role first, and waits until it has applied the log up to there.
  • A lease: a right that lasts a set time. The followers that answer a heartbeat will not start an election until their election timeout runs out, so no other leader can appear during that time, and the leader answers reads without the round trip. Each heartbeat that a majority answers renews the lease; when renewals stop, the leader stops answering reads on its own. This is safe only when the implementation limits how fast clocks may drift, makes a new leader wait until the old lease has run out, and keeps a paused leader from using a lease that has already run out.
A linearizable read with a read barrier and with a lease Left, read barrier: the leader notes that the log is committed up to 812, confirms with a heartbeat to a majority that it still leads, waits until it has applied 812, and answers $150. Right, lease: the followers answer a heartbeat, and each will not start an election for the next 10 seconds, so the leader holds a lease for that time. A read inside the lease gets $150 at once, with no round trip. When the lease ends, the next heartbeat that a majority answers renews it; without one, the leader stops answering reads locally. Read barrier CLIENT LEADER FOLLOWERS READ committed to 812 HEARTBEAT STILL LEADER APPLY 812 $150 Lease CLIENT LEADER FOLLOWERS HEARTBEAT OK: NO ELECTION FOR 10 S LEASE 10 S READ $150, AT ONCE LEASE ENDS the next answered heartbeat renews it; without it, no more local reads
A linearizable read served by a follower The client reads from a follower. The follower asks the leader how far the log is committed. The leader confirms with a heartbeat to a majority that it still leads and answers 812. The follower waits until it has applied the log up to 812, then answers $150. CLIENT FOLLOWER LEADER OTHER FOLLOWER applied: 809 READ COMMITTED UP TO? HEARTBEAT STILL LEADER 812 APPLY TO 812 $150

Every write pays a round trip to the nearest majority and a disk write on each of those nodes. With the three nodes in three zones of one region, the round trip is short. With one voting node in each of three regions, any two regions form a majority: losing one region loses no committed write, and every write waits for the nearest other region. With voting nodes in only two regions, the region that holds the majority stays writable when the link breaks, and the other cannot write.

Three ways to place three voting nodes Each small box is a voting node; the outlined pair is one majority. Left: three zones A, B, C in one region, with a short round trip between them. Middle: one voting node in each of three regions; any two regions form a majority, so one region can fail without losing a committed write, and every write waits for the nearest other region. Right: two nodes in Europe and one in the US; when the link breaks, Europe holds the majority and stays writable, and the US cannot write. One region, 3 zones REGION ZONE A ZONE B ZONE C SHORT ROUND TRIP Fast writes One node per region EU US ASIA ANY 2 REGIONS: MAJORITY Survives one region failure Two regions, 2 + 1 EUROPE US EUROPE STAYS WRITABLE The US cannot write small box: a voting node; outlined pair: a majority

Give every client request an idempotency key. Use the read barrier where a stale answer does harm, lease reads only where the implementation enforces their timing rules, and plain follower reads where a stale answer is fine.

Each night the wallet sends payouts to banks. The job runs on three machines in case one fails, but only one of them may send. The map of which shardShardOne part of a dataset that is split across nodes; sharding is the split itself. Each row lives in exactly one shard, and each shard has its own leader and followers, so data and writes can grow past one machine.Read more: Sharding: Splitting a table across nodes holds which accounts must also exist in one version that every router trusts (more on finding the shard for a request).

Both can share a coordination service: a small consensus group, such as etcd, ZooKeeper, or Consul, that other systems use for such decisions. It stores small keys with linearizable writes and compare-and-set, and adds:

  • Leases for clients. A client renews its session with heartbeats, and when the renewals stop, the service deletes the keys tied to the session. A lock is such a key: the first client to create it holds the lock, and when the holder dies, the key expires and another client takes over. Electing a leader for an app works the same way.
  • Watches: a client is told when a key changes, instead of pollingPollingThe client asks the server for new data again and again on a timer. Polling is simple and works everywhere. The cost is delay (up to one interval) and many empty requests.Read more: Networking: Pushing new messages.
  • A number that grows with every change: the revision in etcd, the transaction IDTransaction IDA number a database gives each transaction that writes, in increasing order. In PostgreSQL it has 32 bits and wraps around, so vacuuming must freeze old rows before it runs out.Read more: PostgreSQL: Running out of transaction IDs, or zxid, in ZooKeeper.
A coordination service holding a lock and a shard map A coordination service runs on three nodes, N1 to N3. It holds two small keys: the payout lock, held by runner A through session 81, at revision 1043; and the shard map, version 8, at revision 1040. Runner A renews its session with heartbeats; runners B and C watch the lock. When the shard map changes, the service tells the two routers that watch it. COORDINATION SERVICE N1 N2 N3 KEYS payout lock: runner A session 81 · revision 1043 shard map: version 8 revision 1040 Runner A HEARTBEAT Runner B WATCH LOCK Runner C WATCH LOCK MAP CHANGED: v8 Router 1 Router 2

A lease does not stop a holder that pauses. The payout job stops for a long garbage collection, its lease runs out, a second machine takes the lock and starts sending, and then the first one wakes up and sends too. The growing number solves this. Each grant has a fencing token: the number of the change that granted the lock, which is larger for every later grant. The bank or database that receives the work durably keeps the largest token it has accepted and, in the same atomic operation as the change, rejects any older one. This stops a stale holder at a receiver that has already seen a newer token; it does not stop the stale process itself.

A fencing token stops a paused lock holder at the receiver Runner A gets the lock with token 33 and pauses for a long garbage collection. Its lease runs out, and runner B gets the lock with token 34 and sends a payout with token 34; in one atomic step the receiver checks the token and saves 34 as the largest. Runner A wakes up and sends a payout with token 33; the receiver rejects it, because 33 is older than 34. RUNNER A LOCK SERVICE RUNNER B RECEIVER LOCK? YES, TOKEN 33 paused A's lease ran out LOCK? YES, TOKEN 34 PAY, TOKEN 34 check, save 34 PAY, TOKEN 33 REJECTED: 33 < 34

A bank’s APIApplication Programming InterfaceA contract that lets one program use another program. An API says which requests a program accepts and what it returns. In system design, it usually means the network endpoints that clients and other services call.Read more: Wikipedia: API cannot check tokens. For a fixed payout, a payout ID helps instead: the bank processes each ID once, if its API promises that and says for how long it remembers the IDs. That protects the payout itself. Other work that a stale holder might do still needs a token check or a conditional write at the receiver.

A lock has one of two purposes, and they need different tools. A lock for efficiency stops two machines from doing the same work, and a rare double run only costs time, so a single RedisRedisAn in-memory server that stores data structures under keys and runs commands on them. A small command takes far less time than the network round trip, so it often sits next to a database for caches, sessions, counters, and queues. Its copies are asynchronous, so a crash or failover can lose recent writes.Read more: Redis: Taking hot data off the database key with an expiry is enough (more on locks in Redis). A lock for correctness keeps data right, and a double holder corrupts it. It needs a consensus store and a fencing token checked by the receiver, as Martin Kleppmann explains.

Use a coordination service for small, shared metadata, such as locks, leaders, configuration, and shard maps. Keep the wallet’s balances in a database that spreads them over many consensus groups. Reads from a coordination service are not always linearizable: etcd’s are by default, but a ZooKeeper server answers reads from its own copy, which may be behind, and a sync before a read does not guarantee a fresh value in every failure. Clients that only need to find services can cache what they read and accept data a moment old.

Do not rely on a lock with an expiry alone where two holders would corrupt data. A paused holder can act after its lock has passed to another. Check a fencing token where the work lands, or, for a fixed piece of work, use an ID that the receiver processes only once.

The wallet outgrows one group and shards accounts by account ID, each shard a Raft group of its own. Anna’s account is on shard 1 and Ben’s on shard 7. So far the wallet runs transactions inside one shard only, so the transfer becomes two writes, and a process that stops between them loses $50 or creates it.

A transfer as two writes, interrupted between them A process debits Anna's $50 on shard 1, so her balance goes from $100 to $50. The process stops before it credits Ben on shard 7, so Ben stays at $0, and $50 is gone. SHARD 1 PROCESS SHARD 7 Anna: $100 Ben: $0 DEBIT ANNA $50 Anna: $50 stops CREDIT BEN $50 $50 is gone

Keeping the rows that change together on one shard is the first fix, but transfers go between any two users. The debit and the credit must both commit or both abort. This is atomic commit.

Two-phase commit provides it. One node is the coordinator, and the shards that hold the rows are participants:

  1. Prepare. The coordinator asks each participant to prepare. A participant saves its changes to disk with enough state to finish them after a crash, and votes yes. A yes vote is a promise to follow the final decision: the participant can no longer abort on its own. A participant that cannot make the change votes no.
  2. Commit. If every vote is yes, the coordinator writes “commit” to its own log, and from that moment the transaction is committed. Then it tells every participant, and each one commits and releases its locks. One no, or a participant that does not answer in time, makes the coordinator write “abort” and tell everyone.
Two-phase commit of a transfer across two shards The coordinator asks shard 1 and shard 7 to prepare transfer t-501. Shard 1 saves Anna −$50 and shard 7 saves Ben +$50; each keeps its change locked and votes yes. After both votes, the coordinator writes commit to its log, and from that moment the transaction is committed. It tells both shards to commit, and each commits and releases its locks. SHARD 1 COORDINATOR SHARD 7 PREPARE t-501 PREPARE t-501 Anna −$50: prepared, locked Ben +$50: prepared, locked YES YES log: commit COMMIT POINT COMMIT COMMIT committed, unlocked committed, unlocked
A missing vote makes the coordinator abort The coordinator asks shard 1 and shard 7 to prepare transfer t-501. Shard 1 prepares and votes yes. Shard 7 does not answer in time. The coordinator writes abort to its log and tells both shards; shard 1 undoes its prepared change and releases its locks. SHARD 1 COORDINATOR SHARD 7 PREPARE t-501 PREPARE t-501 Anna −$50: prepared, locked no answer YES TIMEOUT log: abort ABORT ABORT undone, unlocked

The weak spot is the coordinator. A participant that voted yes must wait for the decision and keeps its changes locked while it waits. If the coordinator fails after the votes and before the decision reaches everyone, the participants are in doubt until they learn the decision, usually from the coordinator once it recovers. A participant that aborts on its own after a timeout can break atomicity, since the coordinator may have decided to commit and told the others. Three-phase commit tries to remove the wait, but it is safe only when message delays are bounded: during a network partition, one side may commit while the other aborts. It is rarely used.

Participants in doubt after the coordinator fails Both shards have prepared transfer t-501 and voted yes. The coordinator writes commit to its log and fails before the decision reaches the shards. Both shards stay in doubt, holding their locks, until the coordinator recovers, reads commit from its log, and tells them to commit; only then do they commit and release the locks. SHARD 1 COORDINATOR SHARD 7 PREPARE t-501 PREPARE t-501 Anna −$50: prepared, locked Ben +$50: prepared, locked YES YES log: commit fails IN DOUBT, LOCKED IN DOUBT, LOCKED recovers: commit COMMIT COMMIT committed, unlocked committed, unlocked

Distributed databases such as Spanner and CockroachDB keep the decision and each participant’s prepared state in consensus groups, so the loss of one node loses neither, and another node carries on. A transaction can still wait when a group it needs has lost its majority.

Two-phase commit where every party is a consensus group The coordinator and the two shards are each a group of three nodes. The coordinator group keeps the decision in its log, and each shard group keeps its prepared state in its log. Node N1, the leader of the coordinator group, fails, and N2 becomes the leader and carries on with the decision. COORDINATOR N1 N2 N3 DECISION IN ITS LOG N2 TAKES OVER SHARD 1 N1 N2 N3 PREPARED STATE IN ITS LOG SHARD 7 N1 N2 N3 PREPARED STATE IN ITS LOG PREPARE, COMMIT

Basic two-phase commit still costs, though databases overlap or shorten some of its steps:

  • A round to prepare and a round to commit, with durable writes on every participant, and the slowest participant sets the pace.
  • Prepared changes stay locked across both rounds, so a busy row waits for the slowest node.
  • Every participant must support prepare. The XAeXtended ArchitectureA standard for running two-phase commit across different products, such as a database and a message broker. Each product must support prepare, and every product adds its own failure and recovery cases.Read more: Wikipedia: X/Open XA standard runs two-phase commit across different products, such as a database and a message broker, but recovery must then handle the failures of every product involved.

Inside the transaction, the debit is still a conditional write that keeps the balance at zero or above.

Two-phase lockingTwo-phase lockingA way to get serializable isolation by holding read and write locks until the transaction ends. Queries also lock the ranges they searched, to stop phantoms. Readers and writers block each other, and deadlocks are common. Not to be confused with two-phase commit.Read more: Transactions: Running as if one at a time keeps concurrent transactions in one database apart (more on two-phase locking). Two-phase commit makes one transaction commit on all of several nodes or on none of them. It gives atomicity, not isolation: the database adds concurrency control on top, such as locks or checks on snapshots.

Keep writes that must be atomic on one shard when the data allows it. Otherwise use the distributed transactions of a database that keeps their state in consensus groups. Avoid two-phase commit across services and different products.

Ordering transactions across nodes by time

Section titled “Ordering transactions across nodes by time”

An audit report reads all balances at one moment. Each shard keeps old row versionsRow versionOne state of a row, kept so that each transaction can read the state it should see. Versions are kept as separate copies of the row or rebuilt from an undo log. Readers pick the version they should see, and old versions are cleaned up later.Read more: Transactions: Undoing a half-done transaction with MVCCMulti-version concurrency controlKeeping several versions of a row so that each transaction reads the one it should see. Plain reads do not block writers, and writers do not block plain reads. Old versions stay until no transaction needs them, so long transactions make data grow.Read more: Transactions: Reading a consistent picture, so the report can read every shard “as of” one timestamp. The snapshot must include everything that each change it shows depended on, and each shard must have applied every commit up to that timestamp before it answers. Otherwise the report can show Ben spending money that has not reached him.

A read as of timestamp 100 on two shards Before: Anna $100 on shard 1, Ben $0 on shard 7. Transfer t-501 at timestamp 95 moves $50 from Anna to Ben. A report reads both shards as of 100. Shard 1 has applied commits up to 104. Shard 7 has applied only up to 90. Left: shard 7 answers at once with Ben $0, its version from before 95, so the report shows the debit without the credit. Right: shard 7 waits until it has applied up to 100 and answers with Ben $50, the version at 95. BEFORE: ANNA $100, BEN $0 · t-501 AT 95: ANNA −$50, BEN +$50 Shard 7 answers at once REPORT AS OF 100 Shard 1 applied to 104 Anna: $50 (at 95) Shard 7 applied to 90 Ben: $0 (at 80) $50 missing Shard 7 waits for 100 REPORT AS OF 100 Shard 1 applied to 104 Anna: $50 (at 95) Shard 7 applied to 100 Ben: $50 (at 95) Totals match

An auditor may also confirm a transfer by phone and then start the report. To include that transfer, the timestamps must follow real time: a transaction that finished before another started must get the smaller timestamp. Clocks on the shards disagree, so plain server timestamps cannot give this order.

Three ways to get such timestamps:

  • A timestamp oracle: one service hands out increasing numbers, and every transaction gets its timestamps from it, in Percolator, described by Google in 2010, once at the start and once at the commit. TiDB uses one too. It is simple and exact. Requests can be batched, but each transaction still waits for the oracle, so it should sit close to the transactions, and it must be replicated so that it does not become a single point of failureSingle point of failureA part whose failure stops the whole system. Remove it by running more than one copy and switching to a healthy one automatically.Read more: Wikipedia: Single point of failure.
  • TrueTime, in Spanner: the clock returns an interval [earliest, latest] that contains the real time under its clock assumptions. GPS receivers and atomic clocks in each data center keep the interval narrow. A transaction picks a commit timestamp no smaller than “latest” and than the timestamps its participants proposed, then waits until “earliest” has passed it before the commit becomes visible. This commit wait makes any transaction that starts later get a larger timestamp. In 2012, the error on each side of the reading was 1 to 7 ms, usually about 4 ms, and part of the wait overlapped with the replication of the commit. Cloud providers now offer clocks with error bounds too, such as AWS ClockBound.
  • A hybrid logical clock: a timestamp made of physical time plus a counter. The physical part follows the largest physical time known to the node, from its own clock and from timestamps it receives. When the physical parts are equal, the counter puts the new event after both the node’s previous event and any message it receives. Timestamps then respect cause, never go back, and stay close to real time, but they give no real-time order between unrelated events. In its 2022 design, CockroachDB uses such timestamps with a configured maximum clock skew. A read that finds a value with a timestamp just above its own, within that clock skew, restarts with a later timestamp, and a node shuts down when the cluster finds its clock off by more than the limit.
How three timestamp schemes order T1 and a later T2 In each panel, T1 finishes on node A, and later T2 starts on node B, with no message between them. Timestamp oracle: T1 gets 40 at its start and 41 at its commit from one counter, so T2 gets 42. TrueTime: T1 chooses the commit timestamp 10.007, the latest bound of its interval [10.000, 10.007], and waits until the earliest bound has passed 10.007; only then is T1 visible, so T2, which starts later, gets 10.015. Hybrid logical clock: T1 gets (10.000, 0) at real time 10.000 on node A; T2 starts at real time 10.003 on node B, whose clock is 5 ms behind, and gets (9.998, 0), a smaller timestamp. T1 FINISHES ON NODE A; LATER, T2 STARTS ON NODE B; NO MESSAGE BETWEEN THEM Timestamp oracle T1: start 40, commit 41 from one counter ONE COUNTER T2: start 42 41 < 42 TrueTime T1 timestamp: 10.007 interval [10.000, 10.007] COMMIT WAIT: EARLIEST > 10.007 T1 is visible only now T2: start 10.015 10.007 < 10.015 Hybrid logical clock T1: (10.000, 0) node A, real 10.000 B'S CLOCK −5 MS node B, real 10.003 T2: (9.998, 0) T2 sorts first
How a hybrid logical clock sets its timestamp when a message arrives Node B's last timestamp is (100, 4), and a message arrives from node A with (100, 7). Left: B's own clock reads 99, so the physical part stays 100, the largest known; the physical parts are equal, so the counter becomes the larger of 4 and 7, plus one: (100, 8). Right: B's clock reads 103, ahead of both, so the physical part becomes 103 and the counter starts at 0: (103, 0). Own clock behind NODE A NODE B last: (100, 4) clock: 99 MESSAGE (100, 7) now: (100, 8) SAME PHYSICAL 100: COUNTER max(4, 7) + 1 Own clock ahead NODE A NODE B last: (100, 4) clock: 103 MESSAGE (100, 7) now: (103, 0) PHYSICAL 103 IS LARGEST: COUNTER 0

With commit wait, Spanner gives strict serializability: transactions act as if they ran one at a time, in an order that matches real time. CockroachDB gives serializable transactions, but two transactions on unrelated rows can get timestamps in the opposite order of real time when clocks are off by less than the limit. In both, the timestamps order transactions and pick the versions to read, and the database’s concurrency control gives isolation.

A timestamp oracle costs a wait for its answers and fits transactions that run close to it. Commit wait costs a few milliseconds per write and needs clocks with a known error. Hybrid logical clocks need no special hardware and keep causal order, but alone they give no real-time order; CockroachDB adds read restarts and a clock skew limit.

Anna tops up her wallet with $100 from her card. The wallet asks a payment provider to charge the card, then credits her balance. The provider is another company’s system: it does not take part in two-phase commit, and a charge cannot be rolled backRollbackThe end of a transaction that discards all of its changes. The app rolls back when a check fails, and after a crash the database rolls back every transaction that did not commit.Read more: Transactions: Changing several rows at once, only refunded. The provider may also finish the payment later, long after its first answer.

A saga splits such a change into local transactions, each in one system, that commit one by one. A step that may need undoing gets a compensating transaction that reverses its business effect, as a refund reverses a charge. A refund does not erase the charge: both stay in the records. If the saga gives up before its pivot, it runs the compensations of the finished steps in reverse order. After the pivot, it retries the remaining steps.

The step after which the saga only moves forward is its pivot. Steps after it must not fail for business reasons. In the top-up, the wallet first records the top-up as pending and checks the limits, then charges the card, which is the pivot, then credits the balance. The account cannot be closed while a top-up is pending, so the credit can always happen. If the charge is declined, the compensation marks the top-up failed.

The steps of a card top-up saga and its pivot Three local steps, from top to bottom: record the top-up as pending and check the limits; charge the card, which is the pivot; credit the balance. If the charge is declined, the compensation marks the top-up failed. After the charge, the saga only moves forward: the credit is retried with the same ID until it is done. 1. Pending check limits 2. Charge card the pivot 3. Credit the balance CHARGED MAY BE UNDONE ONLY FORWARD Mark failed compensation DECLINED RETRY, SAME ID

A saga finishes only if its recovery works. Store the saga’s state and a stable ID for each step. When the result of a step is unknown, ask the other system or retry with the same ID. Write a local change and the message that starts the next step in one transaction. If retries cannot settle the outcome, reconcile the records of both systems, such as the wallet’s and the provider’s.

A saga has no isolation across its steps, though each local transaction keeps its own. Between steps, other requests see the middle state, such as a charge whose credit has not landed yet. The usual defense is a semantic lock: mark records as pending until the saga ends, and make other operations check the mark, so pending money cannot be spent. In try, confirm, cancel, the first step only holds the money, and a later step moves it or releases the hold (more on holds in several systems).

Pending money cannot be spent until the saga ends Left, while the top-up saga runs: the account shows $40 available and a pending top-up of $100; a payment of $120 is refused, because pending money cannot be spent. Right, after the credit: $140 is available, and the same payment goes through. While the saga runs Account available: $40 pending: +$100 PAY $120 Refused After the credit Account available: $140 no pending top-up PAY $120 Accepted

With orchestration, one service stores the saga’s progress and drives its steps, so one place shows where every saga stands. With choreography, each service reacts to events from the others, which suits small, simple flows.

A saga driven by an orchestrator and by events Left, orchestration: a top-up orchestrator stores the saga's progress and sends commands: the ledger records the pending top-up, the payments service charges the card, and the ledger credits the balance. Right, choreography: the ledger publishes the event "top-up pending", the payments service charges the card and publishes "card charged", and the ledger credits the balance when it gets that event. Orchestration commands from one place Orchestrator stores progress Ledger pending, credit Payments charge RECORD PENDING, CREDIT CHARGE Choreography events between services Ledger credits on event Payments charges on event TOP-UP PENDING CARD CHARGED
Two-phase commit Saga
Result all or nothing, enforced by the protocol all steps, or compensations of the finished ones, if recovery succeeds
Isolation from the database’s concurrency control none across the saga; each local transaction keeps its own
Who can take part nodes that support prepare systems whose steps can be retried safely and then compensated or completed
Typical duration milliseconds seconds to days
When a node fails participants wait, holding their changes the saga retries, compensates, or goes to reconciliation

Keep a change in one database transaction where you can. Use two-phase commit inside one distributed database, and a saga for a change of several steps across services or outside systems, when the middle states are acceptable and every step has defined recovery.

The wallet now runs in two regions, with sharded accounts, the majority of each group’s voting nodes in Europe, a coordination service, and sagas for card top-ups. Each operation gets the weakest guarantee that keeps it correct:

Operation Guarantee How During a network partition
Transfer between users atomic, with a conditional debit a distributed transaction: consensus per shard, two-phase commit across shards refused on the side without the majority
Taking a handle linearizable compare-and-set in a consensus store refused on the side without the majority
Balance after a notification includes the notified transfer the notification carries a token, such as the shard and log position of Ben’s account; the nearest copy waits until it has applied it waits until the copy has the transfer; a stale view, if offered, is labeled
Balance on its own may be stale the nearest copy, with the time of its last update served
History, requests, notifications causal the nearest region, writes shown after their dependencies served while the region holds the client’s past
Like counters eventual the nearest copy served
Card top-up saga a pending record, the charge as the pivot, the credit retried kept pending on the side without the majority
Nightly payout job each payout once, even if two runners overlap a lock with a lease and a fencing token from a coordination service; a payout ID that the bank processes once no new lock grants on the side without the service’s majority

For each operation in a design, name the guarantee it needs and why. Then say what it costs without failures, in latency, and during a network partition, in refused requests. “The system is strongly consistent” is not an answer: say which reads and writes are linearizable, which checks are atomic, and which reads may be stale.

  • Linearizability: every operation takes effect at one moment between its request and its answer, in one order that matches real time. Use it for reads that follow a signal; guard rules with atomic conditional writes or transactions.
  • CAP: during a network partition, a system cannot both stay linearizable and complete every request. Choose per operation; if both sides accept conflicting writes, define how to merge them and repair any rule they break.
  • PACELC: without a partition, keeping one order still makes nodes wait for each other. Pay that latency only where needed, and use weaker guarantees, such as bounded staleness or session, elsewhere.
  • Causal consistency: writes that depend on each other show up in order everywhere. It can keep working during a partition for clients that stay with a copy that holds their past; sequential and linearizable cannot.
  • Clocks: time-of-day clocks jump and differ between nodes; monotonic clocks measure durations on one node. Do not order events across nodes by time-of-day timestamps alone.
  • Logical clocks: a Lamport clock gives one sort order that respects cause; a vector clock also detects concurrent events. Neither can decide a winner while some events may be missing.
  • Consensus: crash-tolerant algorithms decide by majority with durable votes and logs; 2F + 1 voting nodes survive F crashes, and the side without a majority stops. Use a proven store on 3 or 5 nodes.
  • Raft elections: terms, random election timeouts, one durable vote per term, votes only for candidates whose log is not behind. Terms fence old leaders; writes pause for about an election timeout plus the election.
  • Raft log: an entry of the current term is committed once a majority has saved it, with every entry before it. Use idempotency keys against retries, and a read barrier, or a lease the implementation enforces, for linearizable reads.
  • Coordination services: etcd, ZooKeeper, or Consul for small metadata such as locks, leases, leaders, configuration, and shard maps. A lock for correctness needs a fencing token checked atomically where the work lands, or, for fixed work, an ID the receiver processes once.
  • Two-phase commit: participants prepare and promise, the coordinator decides. A failed coordinator leaves participants in doubt with changes locked; consensus groups survive node loss but still wait without a majority. Use it inside one distributed database.
  • Timestamps across nodes: a timestamp oracle, TrueTime with commit wait for strict serializability, or hybrid logical clocks, which keep causal order but alone give no real-time order.
  • Sagas: local steps, compensated in reverse if the saga gives up before its pivot and retried after it, with no isolation across the saga. Store the state and step IDs, make every step safe to retry, and mark middle states as pending.
  • Per operation: name the guarantee each operation needs and what it costs in latency and during a partition.

© 2026 Grig Alex · CC BY-SA 4.0