Read and write concerns
A replica set keeps several copies of your data: one primary that takes all writes, and one or more secondaries that copy those writes over. This redundancy is what makes the durability and visibility knobs meaningful. When you write, you can decide how many copies must confirm the write before you consider it done. When you read, you can decide how committed and how fresh the data must be. The two settings are the write concern and the read concern.
Outputs come from two places, both on this Mac with mongo 8.3.11, mongosh 2.11.1 and Node driver 7.7.0: fences with a path comment ran against the course’s one-node replica set; fences marked 3-node ran on a throwaway three-container replica set on a Docker network (rs-1, rs-2, rs-3, started with --replSet rsb3), removed afterwards. Python/Go/Rust tabs mirror the verified shell/Node call and are not executed by the course’s automated checker.
Write concern: how durable, how confirmed
Section titled “Write concern: how durable, how confirmed”A write concern has three parts. w says how many members must acknowledge: w: 1 means only the primary, w: "majority" means a majority of the voting members has the write. j, when true, requires the write to be in the on-disk journal on the members that count. wtimeout (driver option wtimeoutMS) caps how long the server waits for those acknowledgements.
db.members.insertMany([ { name: "Ada", fines: 0 }, { name: "Linus", fines: 2 },]);assert.strictEqual(db.members.countDocuments(), 2);Insert a member and demand a majority, journalled, within five seconds:
const res = db.members.insertOne( { name: "Grace", fines: 0 }, { writeConcern: { w: "majority", j: true, wtimeout: 5000 } });console.log(res);assert.ok(res.acknowledged);const res = await db.collection("members").insertOne( { name: "Grace", fines: 0 }, { writeConcern: { w: "majority", j: true, wtimeoutMS: 5000 } });console.log(res);if (!res.acknowledged) throw new Error("expected an acknowledged write");from pymongo import WriteConcern
db.members.with_options( write_concern=WriteConcern(w="majority", j=True, wtimeout=5000)).insert_one({"name": "Grace", "fines": 0})wc := writeconcern.Majority()coll := client.Database("library").Collection( "members", options.Collection().SetWriteConcern(wc),)_, err := coll.InsertOne(ctx, bson.M{"name": "Grace", "fines": 0})if err != nil { return err}let wc = WriteConcern::builder() .w(Acknowledgment::Majority) .journal(true) .build();let coll = client .database("library") .collection::<Document>("members") .clone_with_options(CollectionOptions::builder().write_concern(wc).build());coll.insert_one(doc! { "name": "Grace", "fines": 0 }).await?;{ acknowledged: true, insertedId: ObjectId('6abccc3967d5308401ddf857')}{ acknowledged: true, insertedId: new ObjectId('6abccc39a50eaebf0946af88')}You already have w: "majority"
Section titled “You already have w: "majority"”Since MongoDB 5.0 the implicit default write concern is w: "majority". The server reports it, and reports that a majority write already waits for the journal on the members that count:
const d = db.adminCommand({ getDefaultRWConcern: 1 });console.log(d.defaultWriteConcern, d.defaultReadConcern);const conf = rs.conf();console.log({ writeConcernMajorityJournalDefault: conf.writeConcernMajorityJournalDefault });assert.strictEqual(d.defaultWriteConcern.w, "majority");assert.strictEqual(conf.writeConcernMajorityJournalDefault, true);{ w: 'majority', wtimeout: 0 } { level: 'local' }{ writeConcernMajorityJournalDefault: true }The manual agrees (Write Concern, consulted 2026-09-30): the implicit default is w: majority, which waits for the on-disk journal per writeConcernMajorityJournalDefault — with one edge case: if the data-bearing voting members are not more than the voting majority (a set with arbiters), the default falls back to { w: 1 }. So an unadorned insertOne is already a majority write, and j: true on top of w: "majority" adds nothing while writeConcernMajorityJournalDefault is true (the default). It matters for w: 1, where j decides whether the primary waits for its own journal flush.
When the concern cannot be satisfied
Section titled “When the concern cannot be satisfied”Ask for two data-bearing members on a one-node set and the server refuses immediately — but note the write was still applied:
const members = db.collection("members");let err = null;try { await members.insertOne({ name: "X" }, { writeConcern: { w: 2, wtimeoutMS: 1000 } }); }catch (e) { err = e; }console.log(err.name, err.code, err.codeName, "-", err.message);console.log("document present anyway:", await members.countDocuments({ name: "X" }));if (err.code !== 100) throw new Error("expected UnsatisfiableWriteConcern (100)");MongoWriteConcernError 100 UnsatisfiableWriteConcern - Not enough data-bearing nodesdocument present anyway: 1A write-concern error is not a rollback (the manual: wtimeout “does not undo successful data modifications”): it says “I could not confirm enough copies”, not “I did not write”. This is also why wtimeout is uncertain, not a clean failure. The 3-node run shows the timeout flavour — one secondary frozen with fsyncLock, a w: 3 write with wtimeout: 2000 (other writes in the same run: w: 1 acknowledged, w: "majority" acknowledged in 3 ms because the other secondary still counts):
// mongosh/concern-timeout.js @skip-verify needs the 3-node replica set (real output below)const lib = db.getSiblingDB("lib");lib.members.insertOne({ name: "Grace" }, { writeConcern: { w: 1 } });lib.members.insertOne({ name: "Linus" }, { writeConcern: { w: "majority", wtimeout: 5000 } });try { lib.members.insertOne({ name: "Edsger" }, { writeConcern: { w: 3, wtimeout: 2000 } }); }catch (e) { console.log(e.name, e.code, e.message); console.log(e.errInfo); }w:1 ack truew:majority with one secondary frozen -> true 3 msMongoWriteConcernError | 64 | waiting for replication timed out{ wtimeout: true, writeConcern: { w: 3, wtimeout: 2000, provenance: 'clientSupplied' } }primary has 4 docs (the w:3 write is applied even though it errored)frozen secondary sees 1 docshealthy secondary sees 4Read concern: how committed
Section titled “Read concern: how committed”A read concern controls which version of the data a read may return. local (the default for reads outside transactions) returns what the queried member has now, even data that could later be rolled back. available is the same on a replica set. majority returns only data acknowledged by a majority. snapshot is majority-based and is meant for transactions and multi-statement reads. linearizable reflects every majority-acknowledged write that completed before the read began, and works only on the primary. All five run on the one-node set:
const counts = {};for (const level of ["local", "available", "majority", "snapshot"]) { counts[level] = db.members.find({ fines: 0 }).readConcern(level).toArray().length;}counts.linearizable = db.members.find({ fines: 0 }).readConcern("linearizable").maxTimeMS(3000).toArray().length;console.log(counts);assert.strictEqual(counts.majority, 2);const members = db.collection("members");await members.deleteMany({}); // fresh startawait members.insertMany([{ name: "Ada", fines: 0 }, { name: "Linus", fines: 2 }]);const counts = {};for (const level of ["local", "available", "majority", "snapshot"]) { counts[level] = (await members.find({ fines: 0 }, { readConcern: { level }, readPreference: "primary" }).toArray()).length;}counts.linearizable = (await members.find({ fines: 0 }, { readConcern: { level: "linearizable" }, maxTimeMS: 3000 }).toArray()).length;console.log(counts);if (counts.majority !== 1) throw new Error("expected one member with fines 0");from pymongo import ReadPreferencefrom pymongo.read_concern import ReadConcern
docs = list( db.members.with_options( read_concern=ReadConcern("majority"), read_preference=ReadPreference.PRIMARY, ).find({"fines": 0}))coll := client.Database("library").Collection( "members", options.Collection(). SetReadConcern(readconcern.Majority()). SetReadPreference(readpref.Primary()),)cur, err := coll.Find(ctx, bson.M{"fines": 0})if err != nil { return err}defer cur.Close(ctx)let coll = client .database("library") .collection::<Document>("members") .clone_with_options( CollectionOptions::builder() .read_concern(ReadConcern::majority()) .selection_criteria(SelectionCriteria::ReadPreference(ReadPreference::Primary)) .build(), );let mut cursor = coll.find(doc! { "fines": 0 }).await?;{ local: 2, available: 2, majority: 2, snapshot: 2, linearizable: 2 }{ local: 1, available: 1, majority: 1, snapshot: 1, linearizable: 1 }The levels differ in what they wait for, and the difference is visible even on one node. Below, 200 times: write, then immediately count with local and with majority — once for w: 1 writes, once for w: "majority" writes:
const rc = db.collection("rc");await rc.deleteMany({});async function behindCount(w) { let behind = 0; for (let i = 0; i < 200; i++) { await rc.insertOne({ i, w }, { writeConcern: { w } }); const local = await rc.countDocuments({}, { readConcern: { level: "local" } }); const maj = await rc.countDocuments({}, { readConcern: { level: "majority" } }); if (maj < local) behind++; } return behind;}const withW1 = await behindCount(1);await rc.deleteMany({});const withMajority = await behindCount("majority");console.log({ "w:1 then majority-read behind": withW1, "w:majority then majority-read behind": withMajority });if (withMajority !== 0 || withW1 === 0) throw new Error("expected w:1 to lag and w:majority not to");{ 'w:1 then majority-read behind': 200, 'w:majority then majority-read behind': 0}A w: 1 write is acknowledged before it is majority-committed, so a majority read right after it does not see your own write. A w: "majority" write followed by a majority read always does. (On one node the gap is milliseconds; across a network it is the replication lag.)
linearizable is refused on anything but the primary. 3-node run, connected directly to a secondary:
MongoServerError | 10107 NotWritablePrimary | cannot satisfy linearizable read concern on non-primary nodeRead preference: from where
Section titled “Read preference: from where”Read preference chooses which member answers. primary (default) always reads the freshest data; secondary/secondaryPreferred spread load at the cost of staleness. 3-node run of the Node driver, 20 explained reads with secondaryPreferred — the primary was rs-1:
// app/concern-readpref.mjs @skip-verify needs the 3-node replica set (real output below)const client = new MongoClient("mongodb://rs-1:27017,rs-2:27017,rs-3:27017/?replicaSet=rsb3");await client.connect();const members = client.db("lib").collection("members", { readPreference: "secondaryPreferred" });const served = new Set();for (let i = 0; i < 20; i++) served.add((await members.find({}).explain()).serverInfo.host);console.log("reads served by:", [...served].sort());reads served by: [ 'rs-2', 'rs-3' ]And what a secondary can show: with rs-2 frozen (db.fsyncLock()) while four documents were written on the primary, a direct read on rs-2 returned 1 document and the healthy rs-3 returned 4 (output above). Read concern and read preference are independent: majority on a lagging secondary is safe from rollback, but still behind.
flowchart TD
C["Client sends a write"] --> P["Primary stores it"]
P --> R1["Secondary one replicates"]
P --> R2["Secondary two replicates"]
R1 --> M{"Majority has stored it?"}
R2 --> M
P --> M
M -->|"Yes"| ACK["Acknowledge to client — write is durable"] In Compass: the connection settings dialog lets you set a default read preference for the whole session, so reads in the Documents tab can be routed to secondaries without changing each query.
Tips and gotchas
Section titled “Tips and gotchas”w: "majority"is the default; ask forw: 1only for data you can afford to lose, and know that it can be lost (proof below).wtimeoutbounds the wait, not the write: the timed-outw: 3insert was applied on the primary.- Reading from a secondary can return stale data, because secondaries trail the primary — a frozen one returned 1 of 4 documents.
linearizableneeds the primary and should carrymaxTimeMS, otherwise it can block when no majority is reachable.
Reading AI-generated code
Section titled “Reading AI-generated code”An assistant “optimising” an order write turns the concern down and writes a confident comment:
🐛 Spot the bug
// AI comment: "w: 1 = acknowledged by the primary, which is durable; faster than majority"await orders.insertOne(order, { writeConcern: { w: 1 } });Show the answer
An acknowledgement from the primary is not durability across a failover. 3-node proof: the primary rs-2 was cut off from the network, a w: 1 insert was still acknowledged, the other two members elected rs-1, and after rs-2 was reconnected the acknowledged document was rolled back and is gone (its BSON was moved to /data/db/rollback/). The fix is the default, w: "majority" — during the same partition it did not falsely acknowledge, it timed out:
// mongosh/concern-lost-write.js @skip-verify needs the 3-node replica set and a network partition (real output below)const l = db.getSiblingDB("lib");l.lost2.insertOne({ _id: "only-on-old-primary" }, { writeConcern: { w: 1 } }); // acknowledgedtry { l.lost2.insertOne({ _id: "maj" }, { writeConcern: { w: "majority", wtimeout: 3000 } }); }catch (e) { console.log("w:majority ->", e.codeName); }ack on isolated primary: truew:majority -> WriteConcernTimeout(after the partition heals, on the set)rs-1:27017 PRIMARY | rs-2:27017 SECONDARY | rs-3:27017 SECONDARY[ { _id: 'on-new-primary' } ]/data/db/rollback/200fb679-8f77-415c-bafe-7bf7b4b846b3/removed.2026-09-30T08-43-30.1.bsonMental Model
Section titled “Mental Model”Two independent dials. Write concern says how many copies must have the write before you are told “done” — it changes what the writer knows. Read concern says how settled the data must be before a reader may see it — it changes what the reader sees. Read preference only chooses the server. A write that was acknowledged by fewer than a majority can vanish in a failover; a read below majority can see it before it vanishes.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
Write concern w: "majority" (default) | An acknowledged write survives a failover | A network round trip to secondaries (3 ms with a frozen member on the lab); blocks or times out if a majority is unreachable |
Write concern w: 1 | Fastest, acknowledges as soon as the primary accepts | Proven above: an acknowledged write was rolled back after a partition |
Read concern majority / snapshot | No reads of data that can be rolled back | Lags local (and your own w: 1 writes) |
Read concern local | Fastest, freshest data on the queried member | May read data that is later rolled back |
Read concern linearizable | Reflects every completed majority write | Primary only; slow; needs maxTimeMS |
Common Misconceptions
Section titled “Common Misconceptions”- “
w: 'majority'needsj: trueto be safe.” — withwriteConcernMajorityJournalDefault: true(the default, shown above) a majority write already waits for the journal on the members that count. - “The default write concern is
w: 1.” — since 5.0 it ismajority;getDefaultRWConcernshows it. - “A
wtimeouterror means the write failed.” — the write was applied on the primary (primary has 4 docs); only the confirmation is missing. - “An acknowledged write is safe.” — only if the concern covers a majority; a
w: 1acknowledgement was rolled back. - “Read concern and read preference are the same knob.” — one picks how settled the data is, the other picks which member; a
majorityread on a lagging secondary is safe but still stale.
Python/Go/Rust tabs mirror the verified shell/Node behaviour; they are not executed by the course’s automated checker.