Treat read preference as server selection: modes, tags, staleness filters, latency windows, and explicit stale-read consequences.
Read Preference Modes, Tag Sets, maxStalenessSeconds, and Secondary-Read Consequences
Configure tagged members and a deliberately stale secondary, then observe actual PyMongo routing and max-staleness filtering.
Learning objectives
Differentiate all five read-preference modes as server-selection policies, not consistency levels.
Configure member tag sets and prove which member actually served a PyMongo read.
Explain ordered tag-set matching, latency windows, and why
nearest can still be stale.
Demonstrate the 90-second minimum and coarse nature of
maxStalenessSeconds.
Identify invariants that secondary reads can violate even when the query itself is correct.
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 27106–27108.
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. Members are tagged with synthetic
region/workload labels. Member C is also made non-voting,
priority-0, and delayed so max-staleness behavior can be
observed after a controlled wait. 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. Read preference answers “where?”, not “what version is safe?”
Read preference is the client/server-selection
policy used for replica-set reads. It does not change data
visibility by itself. Every mode except primary can
route to a secondary, and asynchronous replication means that
secondary can be stale. Read concern must separately express the
version/isolation requirement.
| Mode | Selection intent | Failure/staleness consequence |
|---|---|---|
primary |
Current primary only. | Fails during no-primary windows; strongest default recency location. |
primaryPreferred |
Primary if available, otherwise eligible secondary. | Can become stale exactly during failover when it falls back. |
secondary |
Eligible secondaries only. | Fails when no secondary qualifies; stale reads allowed. |
secondaryPreferred |
Eligible secondary if possible, else primary. | Optimizes offload/availability, not currentness. |
nearest |
Random eligible member inside latency window, primary or secondary. | Latency preference is not a consistency guarantee. |
docker rm -f atlasmart-mongo-ch15-l3-a atlasmart-mongo-ch15-l3-b atlasmart-mongo-ch15-l3-c 2>/dev/null || truedocker network rm atlasmart-ch15-l3-net 2>/dev/null || truedocker volume rm atlasmart-mongo-ch15-l3-a-data atlasmart-mongo-ch15-l3-b-data atlasmart-mongo-ch15-l3-c-data 2>/dev/null || truedocker network create atlasmart-ch15-l3-netdocker run -d --name atlasmart-mongo-ch15-l3-a --network atlasmart-ch15-l3-net -p 127.0.0.1:27106:27017 -v atlasmart-mongo-ch15-l3-a-data:/data/db mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs15-l3 --bind_ip_alldocker run -d --name atlasmart-mongo-ch15-l3-b --network atlasmart-ch15-l3-net -p 127.0.0.1:27107:27017 -v atlasmart-mongo-ch15-l3-b-data:/data/db mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs15-l3 --bind_ip_alldocker run -d --name atlasmart-mongo-ch15-l3-c --network atlasmart-ch15-l3-net -p 127.0.0.1:27108:27017 -v atlasmart-mongo-ch15-l3-c-data:/data/db mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs15-l3 --bind_ip_alluntil mongosh "mongodb://127.0.0.1:27106/admin?directConnection=true" --quiet --eval 'quit(db.runCommand({ping:1}).ok===1?0:1)'; do sleep 1; donemongosh "mongodb://127.0.0.1:27106/admin?directConnection=true" --quiet --eval 'rs.initiate({_id:"atlasmart-rs15-l3",members:[ {_id:0,host:"atlasmart-mongo-ch15-l3-a:27017"}, {_id:1,host:"atlasmart-mongo-ch15-l3-b:27017"}, {_id:2,host:"atlasmart-mongo-ch15-l3-c:27017"}]})'until mongosh "mongodb://127.0.0.1:27106/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:27106/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:27106/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. Add synthetic placement tags and a delayed “west” secondary
let cfg = rs.conf();const a = cfg.members.find(m => m.host.startsWith("atlasmart-mongo-ch15-l3-a:"));const b = cfg.members.find(m => m.host.startsWith("atlasmart-mongo-ch15-l3-b:"));const c = cfg.members.find(m => m.host.startsWith("atlasmart-mongo-ch15-l3-c:"));a.tags = {region:"east",workload:"primary"};b.tags = {region:"east",workload:"analytics"};c.tags = {region:"west",workload:"analytics"};c.priority = 0; c.votes = 0; c.secondaryDelaySecs = 120;rs.reconfig(cfg);printjson(rs.conf().members.map(m => ({host:m.host,tags:m.tags,priority:m.priority,votes:m.votes,delay:m.secondaryDelaySecs||0})));const app=db.getSiblingDB("atlasmart");app.readpref_probe.drop();app.readpref_probe.insertOne({_id:"dashboard",version:1,value:"baseline"},{writeConcern:{w:"majority",wtimeout:5000}});
All three containers still run on one machine. Tags let you test selection rules, not cross-region round-trip time, correlated failure, bandwidth, or compliance boundaries.
3. Prove read source with PyMongo command monitoring
from pymongo import MongoClientfrom pymongo.monitoring import CommandListenerfrom pymongo.read_preferences import Primary, PrimaryPreferred, Secondary, SecondaryPreferred, NearestURI = "mongodb://127.0.0.1:27106,127.0.0.1:27107,127.0.0.1:27108/?replicaSet=atlasmart-rs15-l3"class Trace(CommandListener): def started(self, event): if event.command_name == "find": print("find routed to", event.connection_id) def succeeded(self, event): pass def failed(self, event): passclient = MongoClient(URI, event_listeners=[Trace()], serverSelectionTimeoutMS=5000)base = client["atlasmart"]["readpref_probe"]policies = [ ("primary", Primary()), ("primaryPreferred", PrimaryPreferred()), ("secondary-east", Secondary([{"region":"east"}])), ("secondaryPreferred-west", SecondaryPreferred([{"region":"west"}])), ("nearest-east", Nearest([{"region":"east"}])),]for name, pref in policies: coll = base.with_options(read_preference=pref) try: print(name, coll.find_one({"_id":"dashboard"})) except Exception as exc: print(name, type(exc).__name__, str(exc))client.close()
The event’s connection_id is the evidence. Run
multiple reads: modes that have several eligible members can
choose among them per operation. Do not infer routing from one
sample.
4. Tag lists are ordered fallback sets
from pymongo import MongoClientfrom pymongo.read_preferences import Secondaryclient = MongoClient("mongodb://127.0.0.1:27106,127.0.0.1:27107,127.0.0.1:27108/?replicaSet=atlasmart-rs15-l3")# First try a nonexistent region, then east, then any eligible secondary.pref = Secondary([{"region":"north"}, {"region":"east"}, {}])coll = client["atlasmart"]["readpref_probe"].with_options(read_preference=pref)print(coll.find_one({"_id":"dashboard"}))client.close()
MongoDB evaluates tag-set documents in order until one matches.
The empty document is an explicit “any eligible member”
fallback. Tags do not override mode: for example, tags are
incompatible with primary, while
nearest can apply tags to either a primary or
secondary candidate.
5. maxStalenessSeconds is a coarse guardrail, not an SLA
from pymongo.read_preferences import Secondarytry: Secondary(max_staleness=30)except Exception as exc: print(type(exc).__name__, str(exc))# Current drivers require maxStalenessSeconds >= 90 seconds.
# C is configured with secondaryDelaySecs=120. Allow real wall time to exceed the# protocol's 90-second minimum before testing staleness filtering.sleep 95python - <<'PY'from pymongo import MongoClientfrom pymongo.read_preferences import SecondaryURI="mongodb://127.0.0.1:27106,127.0.0.1:27107,127.0.0.1:27108/?replicaSet=atlasmart-rs15-l3"client=MongoClient(URI,serverSelectionTimeoutMS=5000)west = client["atlasmart"]["readpref_probe"].with_options( read_preference=Secondary([{"region":"west"}], max_staleness=90))try: print(west.find_one({"_id":"dashboard"}))except Exception as exc: print(type(exc).__name__, str(exc))client.close()PY
After C’s estimated staleness exceeds 90 seconds, a west-only
secondary preference with max_staleness=90 should
have no eligible server and fail selection. The estimate is
intentionally coarse because clients derive it from periodic
last-write observations; do not market 90 seconds as an exact
freshness SLA.
Production judgment. Use non-primary reads only for workloads whose invariants tolerate staleness or that are paired with an appropriate session/read concern. Tags are policy metadata that must be maintained during topology changes. Monitor per-member lag, server-selection failures, RTT distributions, and fallback frequency. Geographic routing can reduce read latency but increases topology/operational complexity and can surprise you during failover.
Bridge. Lesson 4 shows how a session carries causal time so a secondary read can wait for the write it depends on.
docker rm -f atlasmart-mongo-ch15-l3-a atlasmart-mongo-ch15-l3-b atlasmart-mongo-ch15-l3-c 2>/dev/null || truedocker volume rm atlasmart-mongo-ch15-l3-a-data atlasmart-mongo-ch15-l3-b-data atlasmart-mongo-ch15-l3-c-data 2>/dev/null || truedocker network rm atlasmart-ch15-l3-net 2>/dev/null || true
Check your understanding
- Does read preference change data visibility guarantees?
- Can nearest choose a secondary?
- What is the minimum maxStalenessSeconds?
- Are tag sets evaluated as an unordered OR?
- Why can primaryPreferred violate a freshness-sensitive invariant during failover?
Review the answers
1. No. It selects eligible members; read concern controls the permissible data view.
2. Yes. It treats eligible primary and secondaries equivalently within the latency window.
3. 90 seconds.
4. No. The tag-set list is tried in order until one set matches eligible members.
5. When no primary is available it can fall back to a stale secondary.
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.