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.
Learning objectives
Distinguish local, available,
majority, linearizable, and
snapshot read concerns by permitted visibility
and topology.
Demonstrate that majority durability is not the same thing as recency on a deliberately delayed secondary.
Show why linearizable is primary-only and
should be bounded with maxTimeMS.
Use snapshot reads outside transactions without confusing point-in-time consistency with “latest data”.
Explain the sharded-cluster distinction that makes
available materially different from
local.
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. |
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
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
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);
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
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
const app = db.getSiblingDB("atlasmart");printjson(app.runCommand({ find:"read_concerns", filter:{_id:"order-1501"}, readConcern:{level:"linearizable"}, maxTimeMS:5000}));
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}); }'
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”.
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.
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
- Can majority read concern return stale data?
- Where can linearizable reads run?
- Why use maxTimeMS with linearizable?
- What is snapshot read concern outside transactions?
- 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
- MongoDB 8.3 release notes — Current server release line and patch-sensitive behavior.
-
Write Concern
—
w,j,wtimeout, majority acknowledgment, and journaling behavior. - Read Concern — Supported read-concern levels, operations, and transaction/session compatibility.
- Read Concern majority — Majority-commit visibility and rollback guarantees.
- Read Concern linearizable — Primary-only real-time ordering semantics and latency implications.
- Read Concern snapshot — Point-in-time majority-committed reads and snapshot-history limits.
- Read Preference — Primary/secondary routing modes and stale-read consequences.
- Server Selection Algorithm — Eligibility, latency windows, and per-operation member selection.
- Read Preference Tag Sets — Ordered tag-set matching for replica-set reads.
- maxStalenessSeconds — Coarse secondary-staleness filtering and the 90-second minimum.
- Causal Consistency and Concerns — Read-your-writes, monotonic reads/writes, and writes-follow-reads.
- Read Isolation, Consistency, and Recency — Session guarantees, visibility, and operation-time behavior.
- PyMongo CRUD configuration — Driver read preference, read concern, write concern, and tags.
- PyMongo sessions and causal consistency — ClientSession behavior and causal consistency.
- PyMongo release notes — Current 4.17 driver baseline.
- mongosh release notes — Current 2.10.0 shell baseline.