Skip to content

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.

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.

mongosh/concern-seed.js
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:

mongosh/concern-write.js
const res = db.members.insertOne(
{ name: "Grace", fines: 0 },
{ writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
);
console.log(res);
assert.ok(res.acknowledged);
{
acknowledged: true,
insertedId: ObjectId('6abccc3967d5308401ddf857')
}
{
acknowledged: true,
insertedId: new ObjectId('6abccc39a50eaebf0946af88')
}

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:

mongosh/concern-defaults.js
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.

Ask for two data-bearing members on a one-node set and the server refuses immediately — but note the write was still applied:

app/concern-unsatisfiable.mjs
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 nodes
document present anyway: 1

A 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 true
w:majority with one secondary frozen -> true 3 ms
MongoWriteConcernError | 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 docs
healthy secondary sees 4

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:

mongosh/concern-read.js
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);
{ 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:

app/concern-own-writes.mjs
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 node

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"]
With majority write concern the client waits until a majority of members has stored the write before it is acknowledged

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.

  • w: "majority" is the default; ask for w: 1 only for data you can afford to lose, and know that it can be lost (proof below).
  • wtimeout bounds the wait, not the write: the timed-out w: 3 insert 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.
  • linearizable needs the primary and should carry maxTimeMS, otherwise it can block when no majority is reachable.

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 } }); // acknowledged
try { l.lost2.insertOne({ _id: "maj" }, { writeConcern: { w: "majority", wtimeout: 3000 } }); }
catch (e) { console.log("w:majority ->", e.codeName); }
ack on isolated primary: true
w: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.bson

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.

ChoiceBenefitCost
Write concern w: "majority" (default)An acknowledged write survives a failoverA 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: 1Fastest, acknowledges as soon as the primary acceptsProven above: an acknowledged write was rolled back after a partition
Read concern majority / snapshotNo reads of data that can be rolled backLags local (and your own w: 1 writes)
Read concern localFastest, freshest data on the queried memberMay read data that is later rolled back
Read concern linearizableReflects every completed majority writePrimary only; slow; needs maxTimeMS
  • “w: 'majority' needs j: true to be safe.” — with writeConcernMajorityJournalDefault: 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 is majority; getDefaultRWConcern shows it.
  • “A wtimeout error 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: 1 acknowledgement 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 majority read 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.

What is the implicit default write concern on MongoDB 8 (unset by the client)?
Do you need j: true on top of w: "majority" for the write to reach the journal?
A w: 3 insert with wtimeout: 2000 returns WriteConcernTimeout (64). Is the document on the primary?
You write with w: 1 and immediately read with readConcern "majority". What did 200 real trials show?
A primary is partitioned from the others, accepts a w: 1 insert (acknowledged), and the rest elect a new primary. What happens to that document when the old primary rejoins?
Which read concern is refused on a secondary?