Skip to content

Replication, sharding and consistency

Scaling past one database, and what you give up when you do.

01

Read replicas and replication lag

intermediate

A read replica copies the primary and serves reads, which scales read capacity. Writes still go to one place, so this buys nothing for a write-heavy workload; it is the right move when reads outnumber writes by ten to one or more, which most applications do.

Replication is asynchronous by default, so a replica is always slightly behind. Usually single-digit milliseconds, and then a bulk update, a long transaction or a network hiccup pushes it to seconds or minutes, and the lag is worst exactly when traffic is highest.

That gap causes read-your-own-writes bugs. A user saves their profile, the next read lands on a replica that has not caught up, and they see the old value and conclude it did not save. They press save again. Now you have a duplicate, and a support ticket that says the site is broken, which it is.

The fix is not stronger consistency but a narrower rule: after a user writes, pin that user's reads to the primary for a few seconds. A cookie or a session flag carrying a timestamp is enough, and it costs almost nothing because only the writing user pays. The alternatives are worse: reading everything from the primary throws away the reason you added replicas, and waiting for synchronous replication makes every write as slow as your slowest replica.

Check yourself

Why do read replicas do nothing for a write-heavy workload?

writes, and reads justafter oneeverything elsereplication lagUserRouterread or writePrimaryall writesReplicasmilliseconds behind
ClientEdge / CDNData storeDashed = asynchronous
Reads scale out; the write path does not, and the gap is what users notice
Evidence · 1 source
  1. 01
    GitHub, October 21 post-incident analysis (2018)

    The incident described here: a network partition left replicas behind the primary, and the decision to serve stale data rather than fail.

Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

02

Sharding and choosing a key

advanced

Sharding splits data across independent databases so that writes scale, which is the one thing replicas cannot do. The shard key decides which shard holds a row, and it is the hardest decision in the system to reverse, because it is embedded in every query, every foreign key that no longer works, and every piece of application code that assumed a join was possible.

A poor key creates hotspots, and the two classic mistakes are geography and time. Sharding by country puts most of the traffic on whichever country you are largest in. Sharding by timestamp puts every current write on the newest shard while the others sit idle holding history, which is the worst possible arrangement: all the storage cost of many machines and the write capacity of one.

Hashing an identifier distributes evenly regardless of skew, and gives up range queries in exchange. Range partitioning keeps ordered scans cheap and invites hotspots. Directory-based sharding, a lookup table saying which shard holds which entity, keeps the flexibility to move one noisy tenant on its own and costs a lookup on every query plus a component that must never be unavailable. Each is a real answer for a different read pattern.

The key has to match how the data is read, not how it is naturally grouped. A query that can be answered from one shard is a normal query; one that spans shards becomes a scatter-gather, where the slowest shard sets the latency and the application does the joining and sorting that the database used to do. Notion sharded Postgres by workspace precisely because almost every query in that product is scoped to one workspace, which turns nearly all reads into single-shard reads.

Then there is everything you lose that nobody mentions in the design review. Cross-shard transactions are gone unless you build two-phase commit or sagas. Unique constraints across shards are gone, so identifiers have to be globally unique by construction. Auto-increment ids stop working, which is why Snowflake-style ids exist. Aggregate queries over the whole dataset need a separate analytics path. Each of these is solvable and each is work that did not exist the week before. Resharding later means moving live data while still serving traffic from it: double writes, backfill, verification, cutover, and a rollback plan for each. It is measured in months for a real system, which is why the honest order is a bigger machine, better indexes, caching and read replicas first. All four are reversible in an afternoon. Sharding is a change to the data model that you will live inside for years.

Check yourself

What does sharding by created_at do to a write-heavy table?

chosen keythe tempting keya thirda thirdeverything currentWritesarriving nowHash of tenant ideven spreadRange by created_atone hot shardShard 1even shareShard 2even shareShard 3all of it, or none
ServiceEdge / CDNData storeDashed = asynchronousConsidered, not chosen
Two keys, two very different write distributions
Evidence · 1 source
  1. 01
    Notion, Herding elephants: lessons learned from sharding Postgres at Notion (2021)

    That the shard key was chosen as the workspace because almost every query in the product is scoped to one, and that the migration was measured in months.

Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

03

CAP, stated usefully

intermediate

When a network partition splits your system, you either refuse requests in order to stay consistent, or answer them and risk divergence. That is the whole choice, and everything else written about CAP is commentary on those two sentences.

Partitions are not optional. Cables are cut, switches fail, a routing change isolates a rack, and a garbage collection pause long enough to miss every heartbeat looks exactly like a partition from outside. So sacrificing partition tolerance is not on the menu, and a system described as CA is a system whose behaviour during a partition has not been decided.

The choice is rarely uniform within one company, and this is where the theory becomes useful rather than academic. A core ledger chooses consistency and refuses the write. The cash machine in the lobby chooses availability, dispenses the money anyway, and reconciles afterwards with an overdraft fee, which is Brewer's own illustration and a reminder that the resolution rule can be a commercial decision rather than a technical one.

The extension worth knowing is PACELC: if there is a partition, choose availability or consistency, else, in normal operation, choose latency or consistency. That second half describes the trade you make every day, unlike the first half which describes a bad afternoon once a year. Every synchronous replication decision, every quorum size, every strongly consistent read is the else branch being answered.

Consistency in CAP is also narrower than the word suggests: it means linearizability, one register behaving as though there is a single copy. It is not the C in ACID, which is about invariants holding within a transaction. Conflating the two is the most common way a CAP discussion goes wrong, and noticing the difference is a reliable signal that someone has read past the triangle diagram.

So the answer that carries weight is never a letter. It is: during a partition this system refuses writes to these entities and accepts them for those, conflicts on the second group resolve by this rule, and here is what a user sees while it happens. Anything shorter has left the interesting part out.

Check yourself

Consistency in CAP means something narrower than the word usually implies. What?

ledgercash machinereconcile afterwardsand the rest of the yearand the rest of the yearNetwork partitionor a GC pause thatlooks like oneRefuse the writeconsistent,unavailableAccept the writeavailable, maydivergeThe merge rulethe interestingengineeringNo partitionlatency againstconsistency
ExternalServiceData storeDashed = asynchronous
A partition forces one answer, and the else branch is every other request
Evidence · 2 sources
  1. 01
    Brewer, CAP twelve years later: how the rules have changed (IEEE, 2012)

    That the choice is not a one-off classification, the cash machine illustration, and that consistency in CAP means linearizability rather than the C in ACID.

  2. 02
    Amazon DynamoDB: read consistency

    That the trade is exposed per read as a parameter, eventually consistent by default and strongly consistent on request at twice the capacity cost.

Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

04

Eventual consistency in the interface

advanced

Eventual consistency means replicas converge given no new writes. Read the definition carefully: it promises convergence and says nothing whatsoever about when, and in a system that is always being written to, the condition it depends on never actually occurs. The guarantee is real and it is weaker than the phrase makes it sound. There is a ladder of stronger session guarantees, and each is far cheaper than linearizability. Read-your-own-writes: you see your own changes. Monotonic reads: you never see time run backwards, which is what happens when consecutive reads hit replicas at different lag. Consistent prefix: you see writes in the order they were made, so a reply never appears before the message it answers. Most complaints filed as database inconsistency are one of these three, and each can be bought with routing rather than with consensus.

The engineering is mostly in the interface. Show the pending state, use an optimistic update so the change appears immediately, and do not pretend an action is complete when it is merely accepted. A spinner that says processing is honest; a green tick for work that is queued is a lie that generates a support ticket when the work later fails.

Convergence needs a merge rule, and choosing it is a product decision wearing technical clothes. Last write wins is the default in many stores and is the lossiest: it discards a write silently and depends on clocks you do not control. Amazon's shopping basket takes the union of conflicting versions, which can resurrect a removed item but can never lose an added one, because losing a purchase costs more than an unexpected item at checkout.

CRDTs are the version where the merge is a property of the data type rather than a decision at read time. A grow-only counter, an add-wins set, a sequence for collaborative text: each is defined so that merging in any order gives the same answer, which removes conflict resolution from the application entirely. They are not free, since the metadata to make that work can outgrow the data, and they are the right tool for exactly the cases they fit.

The practical test for a design is to ask what a user sees during the gap and what happens if two people act in it. If the answer to the first is nothing at all and to the second is one of them silently loses, the consistency model has not been designed, it has been inherited from a default.

Check yourself

A user refreshes twice and the second page shows older data than the first. Which guarantee is missing?

requestjust wrotesame sessionno rule at allOne userwrites then readsRouterreads the sessionPrimaryread-your-own-writesOne sticky replicamonotonic readsAny replicatime can go backwards
ClientEdge / CDNData storeDashed = asynchronousConsidered, not chosen
Session guarantees are bought with routing, not consensus
Evidence · 1 source
  1. 01
    DeCandia et al., Dynamo: Amazon's highly available key-value store (SOSP 2007)

    That conflicting shopping basket versions are merged by taking the union, so a partition can lose a removal but never a purchase, and that this is stated as a business decision expressed as a merge rule.

Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.