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 of those copies must confirm the write before you consider it done. When you read, you can decide how committed and how fresh the data you get back must be. The two settings are the write concern and the read concern, and they let you slide between fast-but-weaker and slow-but-stronger on every operation.
Write concern: how durable, how confirmed
Section titled “Write concern: how durable, how confirmed”A write concern has three parts. The w value says how many members must acknowledge the write: w: 1 means only the primary, while w: "majority" means a majority of the set has stored it — the level at which a write survives the loss of any single member. The j value, when true, requires the write to be flushed to the on-disk journal before it is acknowledged, so it survives a crash, not just a process restart. And wtimeout caps how long the driver waits for those acknowledgements before giving up with an error.
Here we insert a member and demand that a majority of the set confirms it, journalled, within five seconds:
db.members.insertOne( { name: "Grace", fines: 0 }, { writeConcern: { w: "majority", j: true, wtimeout: 5000 } })await db.collection("members").insertOne( { name: "Grace", fines: 0 }, { writeConcern: { w: "majority", j: true, wtimeout: 5000 } });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?;The acknowledgement comes back only once the majority has stored the write:
{ "acknowledged": true, "insertedId": "65f0b3d4e4b0a1c2e4b0a1e7" }Read concern and read preference: how fresh, from where
Section titled “Read concern and read preference: how fresh, from where”A read concern controls which version of the data a read may return. With local, you get whatever the member you queried has right now, even if that data could later be rolled back. With majority, you only see data that a majority of the set has stored, so what you read will not be undone. With linearizable, a read on the primary reflects every write that was acknowledged before it began — the strongest and slowest option. A separate setting, the read preference, chooses which member answers: primary always gives the freshest data, while secondary spreads read load across copies at the cost of possibly trailing slightly behind.
Here we read with a majority read concern from the primary, asking for data we know cannot be rolled back:
db.members .find({ fines: 0 }) .readConcern("majority") .readPref("primary")const docs = await db.collection("members") .find({ fines: 0 }, { readConcern: { level: "majority" }, readPreference: "primary" }) .toArray();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?;The trade-off, drawn out
Section titled “The trade-off, drawn out”Every step toward stronger guarantees costs latency. A w: 1 write returns the instant the primary has it; a w: "majority" write waits for the data to travel to and be stored by other members first. The flow below shows where that extra time goes:
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 you run 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 sweet spot for durability that survives a failover;w: 1is faster but a not-yet-replicated write can be lost if the primary fails right after acknowledging it.wtimeoutonly bounds the wait. If it fires, the write may still have been applied on the primary — it just was not confirmed by enough members in time, so treat the timeout as uncertain, not as a clean failure.- Reading from a secondary with read preference
secondarycan return slightly stale data, because secondaries trail the primary. Pair it with a read concern that matches how fresh your application actually needs the data to be.