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.

Three oplogs side by side. A has t10 age 30, t11 age 31 and t12 age 32. B has t10 and t11. C has only t10. The majority commit point is at t11, because two of three members have it. A read with readConcern local on A returns age 32; majority on A returns age 31; local on C returns age 30.
Each member is at its own point in the oplog; "local" reads the newest entry on the member, "majority" stops at the commit point.

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:

readPreferenceReads go to
primary (default)The primary only. Error if there is none.
primaryPreferredThe primary; a secondary if there is no primary.
secondaryA secondary only.
secondaryPreferredA secondary; the primary if there is none.
nearestAny 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

readConcernReturnsCan 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.

A time-down sequence with lanes app, A (primary) and C (secondary). The app sends updateOne with w majority to A; A applies t11 and acknowledges with operationTime t11. The app sends find to C with afterClusterTime t11. C is at t10, so it waits; C's getMore on A returns t11 and the commit point t11. C applies t11 and answers age 31.
afterClusterTime makes a lagging secondary wait until it has your write, instead of returning an older version.

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.

A partition splits the replica set. On the left, A still believes it is primary in term 1 and has age 30. On the right, B is primary in term 2 with C, and app 2 has written age 31 there. A local read on A returns the stale age 30. A linearizable read on A writes a no-op that B and C never copy, so A steps down and the read fails instead of returning stale data.
A stale primary still serves local reads with old data; a linearizable read must reach a majority, so it fails instead.

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

ReadWriteYou get
local, primaryw: 1Fastest. You may read data that a failover undoes.
majority, primaryw: majorityNo rolled-back reads, read your writes; may be slightly behind a w: 1 write.
majority, secondary, causal sessionw: majorityReads spread over secondaries, and still read your writes (the read may wait).
linearizable, primaryw: majorityEvery read sees every write acknowledged before it started. One extra round trip.
local, secondaryanyCheapest 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.
  • lastApplied vs lastDurable (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 and maxStalenessSeconds on the canvas.
  • Sharded clusters: mongos, orphan documents (the real difference of "available"), and cluster time across shards.
  • Multi-document transactions with "snapshot", and maxTimeMS values.
  • The driver's real server selection (round-trip times, localThresholdMS); here "secondary" always picks C.