The idea: which member answers, and which version it may return
A MongoDB replica set keeps the same data on several servers, but not at the same moment. The primary writes first. Each secondary copies the primary's oplog a little later, and some secondaries are further behind than others. So a read has to answer two questions. The read preference says which member gets the read. The read concern says which version of the data that member may return: its newest data, or only data that a majority of members already has.
The canvas shows a 3-member replica set with one document from the
How MongoDB Runs a Command page: test.users {_id: 5, name: "Eve", age: 30},
written at t10. Each write is updateOne({_id: 5}, {$inc: {age: 1}}). Each member row
shows its role and term, its lastApplied (newest oplog entry), the commit point
it knows, the age it would return right now, and its oplog with one column per timestamp. In the oplog, a
green entry is majority-committed as far as that member knows, a
blue one is applied but not yet committed, grey is a no-op, and red is being rolled back.
The green vertical line is the primary's majority commit point.
B is the fast secondary. C lags: it copies only on Replicate, when a read has to wait for it,
or after an election.
Read preference: which member gets the read
The driver keeps a view of the replica set (it sends hello to each member every
heartbeatFrequencyMS, 10 s) and picks a member for each read:
| readPreference | Reads go to |
|---|---|
primary (default) | The primary only. Error if there is none. |
primaryPreferred | The primary; a secondary if there is no primary. |
secondary | A secondary only. |
secondaryPreferred | A secondary; the primary if there is none. |
nearest | Any member within 15 ms (localThresholdMS) of the fastest one. |
Among the eligible members, the driver picks one with a low round-trip time. You can also filter by
tags (for example one data centre) and set maxStalenessSeconds (at least 90) to skip
secondaries that are too far behind. On the canvas, secondary always picks C, the nearest and also
the laggiest one.
Read concern levels
| readConcern | Returns | Can it be rolled back? |
|---|---|---|
"local" (default) | The newest data on the member that answers. | Yes |
"available" | Like local. In a sharded cluster it may also return orphan documents. Same as local on a replica set. | Yes |
"majority" | Data as of the member's majority commit point. | No |
"snapshot" | A majority-committed snapshot at one cluster time (atClusterTime); in a transaction, the same snapshot for every read. | No |
"linearizable" | The newest majority-committed data, after the primary confirms it is still primary. Primary only. | No |
local reads and rolled-back reads
With "local" the member returns whatever it has applied, majority-committed or not. In
Demo: local read, then a rollback, the app writes age 31 with w: 1. A acknowledges at once,
and a local read on A returns 31. Then A is cut off from B and C. After electionTimeoutMillis (10 s)
B is elected in term 2. B never got t11. When the network heals, A sees term 2, steps down, finds that its
entry t11 is not in B's oplog, and rolls it back: the old document goes to a
rollback/ file. The next read returns 30. The app saw an age 31 that, for the cluster, never
happened. This is the replica-set version of a dirty read.
The same can happen with a w: "majority" write. A local read on the primary sees the write as soon
as the primary applies it, before the majority has it. Only the writer waits for the majority.
majority reads and the commit point
The primary tracks how far every member has got (replSetUpdatePosition). The
majority commit point (lastCommittedOpTime) is the newest entry that a majority
has. With 3 members, that is the second-newest lastApplied. An entry at or before it can never be rolled back:
a candidate only wins an election if its oplog is at least as new as a majority's, so every future primary has it.
Secondaries learn the commit point from the metadata of each getMore and heartbeat reply, so their
copy is a little older.
With "majority" the member reads from the snapshot at its commit point (WiredTiger keeps that
snapshot, so the read does not wait). In Demo: majority read sees committed data only, A has t11 but the
commit point is t10, so a majority read on A returns 30. After Replicate, B and C have t11, the commit
point moves to t11, and the same read returns 31. A majority read is safe from rollback, but it can be older than
the newest write, even on the primary.
Your own write, missing on a secondary
Reading from secondaries spreads the load, but a secondary can be behind. In
Demo: your own write missing on a secondary, the app writes age 31 with w: "majority". A and B
have it and the write is acknowledged. Then the app reads with readPreference: "secondary". The driver
picks C, which is still at t10, and the app gets 30: it does not see its own acknowledged write. The read log
marks it stale. Even "majority" would not help here, because C's commit
point is also t10.
Causally consistent sessions
A causally consistent session (client.startSession({causalConsistency: true}),
the default for explicit sessions) fixes this. Every reply carries an operationTime: the time of
the operation in the oplog. The session remembers the newest one. Each read in the session then carries
readConcern: {level: "majority", afterClusterTime: <operationTime>}, and the member must not
answer until it has data at least that new.
In Demo: causal session waits for afterClusterTime, the write's operationTime is t11. The read goes to C
with afterClusterTime t11. C's commit point is t10, so it waits. Its next getMore on A's
oplog brings t11 and A's commit point t11. Now C can answer, and it returns 31. The session guarantees
read your own writes, monotonic reads, monotonic writes and
writes follow reads, but only with "majority" read concern and
"majority" write concern. With w: 1 the write itself could be rolled back.
Cluster time is a hybrid logical clock ($clusterTime, signed by the server). Every message
carries the newest cluster time its sender has seen, so time only moves forward across the whole cluster, even
when you pass the session's times to another client with advanceClusterTime and
advanceOperationTime.
Linearizable reads and a stale primary
During a partition there can briefly be two primaries. The new one has the majority. The old
one has not noticed yet: it steps down only after it has missed a majority for
electionTimeoutMillis, and the driver keeps sending to it until its next check. A
"local" or "majority" read on the old primary still answers, with data that may already
be out of date.
"linearizable" closes that gap. The primary reads the document, then writes a no-op
to its oplog and waits until a majority has it. If the no-op is majority-committed, the member was still the primary
when it read, so the value is the newest committed one. The cost is one more majority round trip per read. Use it
with a filter on a unique field (one document) and with maxTimeMS, because on a cut-off primary it
waits.
In Demo: linearizable read and a stale primary, the first linearizable read on A works: B copies the
no-op t11 and A answers 30. Then A is cut off. B is elected in term 2, and another app already connected to B
writes age 31 with w: "majority". The driver still sends the next read to A, which returns 30 with
"local": stale. The linearizable read on A writes the no-op t12, which nobody copies. After 10 s A steps
down, and the read fails with NotWritablePrimary instead of returning 30. A linearizable read sent to a
secondary fails at once.
snapshot
"snapshot" reads a majority-committed snapshot at one point in cluster time. In a multi-document
transaction, every read sees that same snapshot. Since MongoDB 5.0 you can also use it outside a transaction
(on find, aggregate and distinct) with an atClusterTime, to read
several collections as of the same moment. The server keeps old versions only for
minSnapshotHistoryWindowInSeconds (300 s), so an older time gives SnapshotTooOld. On the
canvas, snapshot returns the same version as majority and reports it as atClusterTime.
Choosing a combination
| Read | Write | You get |
|---|---|---|
| local, primary | w: 1 | Fastest. You may read data that a failover undoes. |
| majority, primary | w: majority | No rolled-back reads, read your writes; may be slightly behind a w: 1 write. |
| majority, secondary, causal session | w: majority | Reads spread over secondaries, and still read your writes (the read may wait). |
| linearizable, primary | w: majority | Every read sees every write acknowledged before it started. One extra round trip. |
| local, secondary | any | Cheapest reads for analytics; data can be minutes old if a secondary lags. |
These are the ideas of Consistency Patterns (eventual, causal, strong) in a real database, and the CAP theorem in small: during a partition a linearizable read gives up availability on the minority side. See also MongoDB Replica Set Elections and Rollback for heartbeats, votes and rollback in detail, Primary–Replica Replication for lag and failover in SQL databases, and Raft, on which MongoDB's elections are based.
What the page leaves out
- Real timestamps: an optime is
{ts: Timestamp(seconds, increment), t: term}, not a single counter. lastAppliedvslastDurable(journaled): here applying an entry also journals it.- The no-op a new primary writes when it is elected, and catch-up mode before it accepts writes.
- Member priorities, priority takeover, arbiters, hidden and delayed members, chained replication.
primaryPreferred,secondaryPreferred,nearest, tag sets andmaxStalenessSecondson the canvas.- Sharded clusters:
mongos, orphan documents (the real difference of"available"), and cluster time across shards. - Multi-document transactions with
"snapshot", andmaxTimeMSvalues. - The driver's real server selection (round-trip times,
localThresholdMS); here "secondary" always picks C.