Carry causal time through an AtlasMart workflow so a dependent secondary read waits for the write it must observe.

Causally Consistent Sessions, Read-Your-Writes, Monotonic Reads, and Cluster Time

Contrast a plain stale secondary read with a causally consistent session that carries operation/cluster time across the dependency.

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

Learning objectives

01

Define causal consistency and its four guarantees: read-your-writes, monotonic reads, monotonic writes, and writes-follow-reads.

02

Observe operationTime and $clusterTime in a PyMongo client session.

03

Prove that a causal secondary read can wait for a deliberately delayed member to reach the operation it depends on.

04

Contrast a plain secondary read with a causally consistent majority read/write session.

05

Explain session scope/threading requirements and why causal ordering is not global linearizability.

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 27109–27111. 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 tagged and delayed by 20 seconds while made non-voting/priority-0. The delay creates a visible dependency gap that a causal session can close with afterClusterTime. 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 dependency: a user confirms stock, then immediately opens the order page

Causal consistency preserves order among operations that have a causal dependency. MongoDB describes four guarantees: read your writes, monotonic reads, monotonic writes, and writes follow reads. It is a session-scoped ordering model—not a claim that every client observes one global real-time order.

Guarantee AtlasMart meaning
Read your writes After this session confirms an order, its later read must not show the pre-confirmation version.
Monotonic reads A later session read must not go backward to a state older than one it already observed.
Monotonic writes Dependent writes from the session preserve their causal sequence.
Writes follow reads A write that depends on a read is ordered after the state that read observed.
Required concerns for durable causal guarantees

MongoDB guarantees the full causal-consistency set under failures when associated reads use majority read concern and writes use majority write concern. Weaker combinations can appear to work while the topology is healthy but do not preserve the complete guarantees in all failure scenarios.

isolated three-member replica-set setup (l4)
docker rm -f atlasmart-mongo-ch15-l4-a atlasmart-mongo-ch15-l4-b atlasmart-mongo-ch15-l4-c 2>/dev/null || truedocker network rm atlasmart-ch15-l4-net 2>/dev/null || truedocker volume rm atlasmart-mongo-ch15-l4-a-data atlasmart-mongo-ch15-l4-b-data atlasmart-mongo-ch15-l4-c-data 2>/dev/null || truedocker network create atlasmart-ch15-l4-netdocker run -d --name atlasmart-mongo-ch15-l4-a --network atlasmart-ch15-l4-net -p 127.0.0.1:27109:27017 -v atlasmart-mongo-ch15-l4-a-data:/data/db mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs15-l4 --bind_ip_alldocker run -d --name atlasmart-mongo-ch15-l4-b --network atlasmart-ch15-l4-net -p 127.0.0.1:27110:27017 -v atlasmart-mongo-ch15-l4-b-data:/data/db mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs15-l4 --bind_ip_alldocker run -d --name atlasmart-mongo-ch15-l4-c --network atlasmart-ch15-l4-net -p 127.0.0.1:27111:27017 -v atlasmart-mongo-ch15-l4-c-data:/data/db mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs15-l4 --bind_ip_alluntil mongosh "mongodb://127.0.0.1:27109/admin?directConnection=true" --quiet --eval 'quit(db.runCommand({ping:1}).ok===1?0:1)'; do sleep 1; donemongosh "mongodb://127.0.0.1:27109/admin?directConnection=true" --quiet --eval 'rs.initiate({_id:"atlasmart-rs15-l4",members:[  {_id:0,host:"atlasmart-mongo-ch15-l4-a:27017"},  {_id:1,host:"atlasmart-mongo-ch15-l4-b:27017"},  {_id:2,host:"atlasmart-mongo-ch15-l4-c:27017"}]})'until mongosh "mongodb://127.0.0.1:27109/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:27109/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:27109/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 20-second lagging causal-read target

tag and delay member C
let cfg=rs.conf();const c=cfg.members.find(m=>m.host.startsWith("atlasmart-mongo-ch15-l4-c:"));c.tags={region:"west",purpose:"causal"};c.priority=0; c.votes=0; c.secondaryDelaySecs=20;const b=cfg.members.find(m=>m.host.startsWith("atlasmart-mongo-ch15-l4-b:"));b.tags={region:"east",purpose:"majority"};rs.reconfig(cfg);const app=db.getSiblingDB("atlasmart");app.causal_inventory.drop();app.causal_inventory.insertOne({_id:"sku-drone",state:"available",version:1},{writeConcern:{w:"majority",wtimeout:5000}});printjson(rs.conf().members.map(m=>({host:m.host,tags:m.tags,delay:m.secondaryDelaySecs||0,votes:m.votes})));

3. First prove the stale-read problem without a causal session

plain west-secondary read can observe the old version
from pymongo import MongoClientfrom pymongo.read_preferences import Secondaryfrom pymongo.read_concern import ReadConcernfrom pymongo.write_concern import WriteConcernURI="mongodb://127.0.0.1:27109,127.0.0.1:27110,127.0.0.1:27111/?replicaSet=atlasmart-rs15-l4"client=MongoClient(URI,serverSelectionTimeoutMS=5000)base=client["atlasmart"]["causal_inventory"]writer=base.with_options(write_concern=WriteConcern("majority",wtimeout=5000))west=base.with_options(read_preference=Secondary([{"region":"west"}]),read_concern=ReadConcern("majority"))writer.update_one({"_id":"sku-drone","version":1},{"$set":{"state":"reserved","version":2}})print("plain secondary read:", west.find_one({"_id":"sku-drone"}))client.close()
Expected observation

Because west member C applies writes 20 seconds late, the immediate plain majority read can still return version 1. That is not a violation of majority read concern; version 1 is majority-committed. The missing guarantee is the dependency between this caller’s write and later secondary read.

4. Repeat inside one causally consistent session

majority write followed by causal west-secondary read
import timefrom pymongo import MongoClientfrom pymongo.read_preferences import Secondaryfrom pymongo.read_concern import ReadConcernfrom pymongo.write_concern import WriteConcernURI="mongodb://127.0.0.1:27109,127.0.0.1:27110,127.0.0.1:27111/?replicaSet=atlasmart-rs15-l4"client=MongoClient(URI,serverSelectionTimeoutMS=30000)base=client["atlasmart"]["causal_inventory"]coll=base.with_options(    read_preference=Secondary([{"region":"west"}]),    read_concern=ReadConcern("majority"),    write_concern=WriteConcern("majority",wtimeout=5000),)with client.start_session(causal_consistency=True) as session:    print("before", session.operation_time, session.cluster_time)    coll.update_one({"_id":"sku-drone","version":2},{"$set":{"state":"picked","version":3}},session=session)    print("after write operation_time", session.operation_time)    started=time.perf_counter()    doc=coll.find_one({"_id":"sku-drone"},session=session)    elapsed=time.perf_counter()-started    print("causal read",doc,"elapsed_seconds",round(elapsed,3))    print("after read", session.operation_time, session.cluster_time)client.close()

The driver carries the session’s operation time and cluster time. For the dependent read, it can send afterClusterTime; the delayed secondary must advance to at least that causal point before satisfying the read. Measure the wait rather than asserting exactly 20 seconds—server selection, heartbeat timing, and application delay all contribute.

5. Session boundaries and misuse

deliberately weak combination: causal flag does not upgrade local/w:1 semantics
from pymongo import MongoClientfrom pymongo.read_concern import ReadConcernfrom pymongo.write_concern import WriteConcernfrom pymongo.read_preferences import SecondaryPreferredclient=MongoClient("mongodb://127.0.0.1:27109,127.0.0.1:27110,127.0.0.1:27111/?replicaSet=atlasmart-rs15-l4")coll=client["atlasmart"]["causal_inventory"].with_options(    read_preference=SecondaryPreferred(),    read_concern=ReadConcern("local"),    write_concern=WriteConcern(w=1),)with client.start_session(causal_consistency=True) as session:    print("causal flag:", session.options.causal_consistency)    print("This combination is not the documented majority/majority recipe for full causal guarantees under failover.")client.close()
Wrong assumption

Setting causal_consistency=True does not magically strengthen arbitrary read/write concerns. Under failover/partition scenarios, local + w:1 cannot guarantee the full causal-consistency set. Also, use a client session from only the MongoClient that created it and serialize operations on that session rather than sharing it concurrently across threads.

Production judgment. Causal sessions are useful when a workflow reads from secondaries but must preserve its own dependency chain. The price is that a read may wait for the selected member to catch up, making replication lag part of tail latency. Observe session/operation time, replication lag, server selection, and p95/p99 causal read latency. Do not use causal consistency as a substitute for a transaction when multiple writes must be atomic together.

Bridge. Lesson 5 turns these primitives into operation-specific policy instead of a single global “strong consistency” setting.

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

Check your understanding

  1. What are the four causal guarantees?
  2. Why can a causal secondary read wait?
  3. Does causal consistency mean every client observes one global real-time order?
  4. Which concern pairing gives the documented durable causal guarantees?
  5. Can one ClientSession be used with a different MongoClient?
Review the answers

1. Read-your-writes, monotonic reads, monotonic writes, and writes-follow-reads.

2. The selected secondary may need to advance to the session’s afterClusterTime before returning the dependent read.

3. No. It preserves causal dependencies for the session; linearizability is a different stronger real-time property.

4. Majority read concern and majority write concern.

5. No. Use it only with the client that created it, and do not run concurrent session operations across threads.

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.