Separate the version a read may observe from the member that serves it, then prove durable does not necessarily mean newest.

Read Concern local, available, majority, linearizable, snapshot, and Staleness Tradeoffs

Use a delayed secondary to compare all read-concern levels and expose majority/linearizable/snapshot boundaries.

Intermediate110–175 minutesReplica-set consistency/latency labMongoDB 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning objectives

01

Distinguish local, available, majority, linearizable, and snapshot read concerns by permitted visibility and topology.

02

Demonstrate that majority durability is not the same thing as recency on a deliberately delayed secondary.

03

Show why linearizable is primary-only and should be bounded with maxTimeMS.

04

Use snapshot reads outside transactions without confusing point-in-time consistency with “latest data”.

05

Explain the sharded-cluster distinction that makes available materially different from local.

Reproducible lab baseline

This lesson pins MongoDB Community Server 8.3.8 with mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim, mongosh 2.10.0, and PyMongo 4.17.0 where the driver is used. The mandatory topology is a disposable three-member replica set on one Docker host, with loopback-published diagnostic ports 27103–27105. Members communicate over a lesson-specific Docker bridge network by container DNS name. Authentication and TLS are disabled only for this isolated local learning topology. Feature Compatibility Version (FCV) is observed and never changed. Unless the experiment states otherwise, reads target the primary and writes use an explicitly named concern rather than assuming a global default. Run only one Chapter 15 topology at a time and budget roughly 3–4 GB of free RAM plus disk headroom. Local member tags such as east/west demonstrate routing semantics only; they do not simulate real WAN latency or failure domains. Atlas, Search, Vector Search, KMS, and Enterprise Advanced are not mandatory. Member C is converted to a non-voting, priority-0 delayed secondary so stale-but-valid reads can be observed deterministically without network/firewall manipulation. Product commands were not executed in this generation environment because Docker, mongod, mongosh, and PyMongo are unavailable here; expected invariants are documentation-derived and measured values must be recorded on the learner’s machine.

1. AtlasMart invariant: “confirmed” must mean the reader is allowed to trust that version

Read concern controls which consistency/isolation view a read is allowed to return. It does not select the server—that is read preference—and it does not decide how many members acknowledged the preceding write—that is write concern. A read can be perfectly valid under its declared read concern and still be stale if it runs on a lagging secondary.

Read concern Core permission Important boundary
local Most recent data available on that member; may be rolled back. Default for ordinary primary/secondary reads.
available Most recent data available on that member; may be rolled back. Same as local on primary/non-sharded secondary; in sharded clusters may expose orphaned documents.
majority Only data in the member’s majority-committed view. Durable does not mean globally newest; a lagging member may return an older committed value.
linearizable Reflects successful majority writes completed before read start in real-time order. Primary only; can wait on majority availability; use a deadline.
snapshot A majority-committed point-in-time snapshot. Point-in-time consistency is not a freshness guarantee on a delayed secondary.
isolated three-member replica-set setup (l2)
docker rm -f atlasmart-mongo-ch15-l2-a atlasmart-mongo-ch15-l2-b atlasmart-mongo-ch15-l2-c 2>/dev/null || truedocker network rm atlasmart-ch15-l2-net 2>/dev/null || truedocker volume rm atlasmart-mongo-ch15-l2-a-data atlasmart-mongo-ch15-l2-b-data atlasmart-mongo-ch15-l2-c-data 2>/dev/null || truedocker network create atlasmart-ch15-l2-netdocker run -d --name atlasmart-mongo-ch15-l2-a --network atlasmart-ch15-l2-net -p 127.0.0.1:27103:27017 -v atlasmart-mongo-ch15-l2-a-data:/data/db mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs15-l2 --bind_ip_alldocker run -d --name atlasmart-mongo-ch15-l2-b --network atlasmart-ch15-l2-net -p 127.0.0.1:27104:27017 -v atlasmart-mongo-ch15-l2-b-data:/data/db mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs15-l2 --bind_ip_alldocker run -d --name atlasmart-mongo-ch15-l2-c --network atlasmart-ch15-l2-net -p 127.0.0.1:27105:27017 -v atlasmart-mongo-ch15-l2-c-data:/data/db mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs15-l2 --bind_ip_alluntil mongosh "mongodb://127.0.0.1:27103/admin?directConnection=true" --quiet --eval 'quit(db.runCommand({ping:1}).ok===1?0:1)'; do sleep 1; donemongosh "mongodb://127.0.0.1:27103/admin?directConnection=true" --quiet --eval 'rs.initiate({_id:"atlasmart-rs15-l2",members:[  {_id:0,host:"atlasmart-mongo-ch15-l2-a:27017"},  {_id:1,host:"atlasmart-mongo-ch15-l2-b:27017"},  {_id:2,host:"atlasmart-mongo-ch15-l2-c:27017"}]})'until mongosh "mongodb://127.0.0.1:27103/admin?directConnection=true" --quiet --eval 'quit(db.hello().isWritablePrimary?0:1)'; do sleep 1; donefor i in $(seq 1 60); do  STATES=$(mongosh "mongodb://127.0.0.1:27103/admin?directConnection=true" --quiet --eval 'const s=rs.status(); print(s.members.map(m=>m.stateStr).sort().join(","))' || true)  [ "$STATES" = "PRIMARY,SECONDARY,SECONDARY" ] && break  sleep 1donemongosh "mongodb://127.0.0.1:27103/admin?directConnection=true" --quiet --eval 'printjson({server:db.version(),hello:db.hello(),fcv:db.runCommand({getParameter:1,featureCompatibilityVersion:1}).featureCompatibilityVersion,status:rs.status().members.map(m=>({name:m.name,stateStr:m.stateStr}))})' 

2. Create a deliberate 20-second secondary delay

make member C delayed and non-electable
let cfg = rs.conf();const c = cfg.members.find(m => m.host.startsWith("atlasmart-mongo-ch15-l2-c:"));c.priority = 0;c.votes = 0;c.secondaryDelaySecs = 20;rs.reconfig(cfg);printjson(rs.conf().members.map(m => ({host:m.host,priority:m.priority,votes:m.votes,secondaryDelaySecs:m.secondaryDelaySecs||0})));const app = db.getSiblingDB("atlasmart");app.read_concerns.drop();app.read_concerns.insertOne({_id:"order-1501",status:"created",version:1},{writeConcern:{w:"majority",wtimeout:5000}});printjson(app.read_concerns.findOne({_id:"order-1501"}));

C remains a useful observation target, but because it has zero votes it is not required for majority acknowledgement. That lets A+B commit a new version while C intentionally applies it later.

3. Majority-acknowledge a new version, then compare views

primary writes version 2 with majority acknowledgement
const app = db.getSiblingDB("atlasmart");printjson(app.read_concerns.updateOne(  {_id:"order-1501",version:1},  {$set:{status:"confirmed",version:2,confirmedAt:new Date()}},  {writeConcern:{w:"majority",wtimeout:5000}}));printjson(rs.status().optimes);
read local / available / majority / snapshot from delayed member C
URI='mongodb://127.0.0.1:27105/atlasmart?directConnection=true&readPreference=secondary'for LEVEL in local available majority snapshot; do  echo "--- $LEVEL on delayed secondary ---"  mongosh "$URI" --quiet --eval "printjson(db.runCommand({find:'read_concerns',filter:{_id:'order-1501'},readConcern:{level:'$LEVEL'}}))"done
What this proves

Immediately after the majority-acknowledged update, delayed C can still return version 1. A majority or snapshot read there is durable/consistent relative to the snapshot that C can serve, but not necessarily the newest system version. Record the observed document and C’s optime rather than assuming a particular delay.

4. Linearizable is a different contract

primary-only linearizable read with a deadline
const app = db.getSiblingDB("atlasmart");printjson(app.runCommand({  find:"read_concerns",  filter:{_id:"order-1501"},  readConcern:{level:"linearizable"},  maxTimeMS:5000}));
controlled misuse: attempt linearizable on the delayed secondary
mongosh 'mongodb://127.0.0.1:27105/atlasmart?directConnection=true&readPreference=secondary' --quiet --eval 'try {  printjson(db.runCommand({find:"read_concerns",filter:{_id:"order-1501"},readConcern:{level:"linearizable"},maxTimeMS:3000}));} catch (e) { printjson({expectedPrimaryOnlyFailure:e.codeName||e.name,message:e.message}); }' 
Majority is not linearizable

A majority read excludes data that can roll back, but it does not perform the primary validation required for real-time linearizable order. Conversely, linearizable reads trade availability/latency and are appropriate only when the business invariant actually needs that stronger contract.

5. available vs local: why the distinction matters later

On this non-sharded replica-set lab, available behaves like local. In a sharded cluster, however, available can return orphaned documents because it avoids the shard-version filtering that local uses. That makes available a specialized latency/availability choice, not a generally “better local”.

optional: wait for delayed member and re-read every level
sleep 25URI='mongodb://127.0.0.1:27105/atlasmart?directConnection=true&readPreference=secondary'for LEVEL in local available majority snapshot; do  mongosh "$URI" --quiet --eval "print('$LEVEL'); printjson(db.runCommand({find:'read_concerns',filter:{_id:'order-1501'},readConcern:{level:'$LEVEL'}}))"done

Production judgment. Pick read concern from anomaly tolerance, not from a “strongest is safest” reflex. Linearizable reads can block when a majority is unavailable; snapshot reads consume snapshot-history resources and can terminate if history ages out; majority reads protect against rollback but may still be stale on lagging members. Bound strong reads with operation deadlines and measure tail latency plus member lag.

Bridge. Read concern defines the permissible view. Lesson 3 decides which member gets asked for that view.

cleanup / full reset
docker rm -f atlasmart-mongo-ch15-l2-a atlasmart-mongo-ch15-l2-b atlasmart-mongo-ch15-l2-c 2>/dev/null || truedocker volume rm atlasmart-mongo-ch15-l2-a-data atlasmart-mongo-ch15-l2-b-data atlasmart-mongo-ch15-l2-c-data 2>/dev/null || truedocker network rm atlasmart-ch15-l2-net 2>/dev/null || true

Check your understanding

  1. Can majority read concern return stale data?
  2. Where can linearizable reads run?
  3. Why use maxTimeMS with linearizable?
  4. What is snapshot read concern outside transactions?
  5. Why does available matter more in sharded clusters?
Review the answers

1. Yes. It guarantees majority-committed data, not that the selected member has applied the newest system write.

2. On the primary only.

3. If a majority cannot be confirmed, a bounded deadline prevents an indefinite wait.

4. For supported operations, a point-in-time majority-committed snapshot; it is not automatically the newest snapshot on a delayed member.

5. It can return orphaned documents and prioritizes availability/latency over the filtering semantics of local.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.