Read and Write Concerns

Every write to MongoDB and every read from it involves an implicit question: how sure do you want to be? Write concern tells MongoDB how many replica set members must acknowledge a write before your application is told it succeeded. Read concern tells MongoDB how up-to-date and durable the data returned by a query has to be. Together they let you dial the tradeoff between speed and safety independently for every operation, instead of living with one fixed guarantee for the whole database.

Overview: What Read and Write Concerns Actually Control

In a MongoDB replica set, one member is the primary and accepts all writes; secondaries continuously replicate the primary’s operation log (the oplog) and apply the same changes. A write “succeeding” on the primary and a write being safe are not the same thing — if the primary crashes half a second after accepting a write but before any secondary has replicated it, that write can be lost during failover. Write concern is how you tell MongoDB how much replication to wait for before it reports success back to you.

Write concern has three parts: w (how many members must acknowledge), j (whether the acknowledging member(s) must have written the operation to the on-disk journal), and wtimeout (how long to wait before giving up and returning an error). The most common values are w: 1 (only the primary needs to acknowledge — the default) and w: "majority" (a majority of voting members must acknowledge, which guarantees the write will survive a primary failover).

Read concern is the read-side counterpart. It controls what a query is allowed to return: data that merely exists on the primary right now (which could later be rolled back if that primary loses an election before replicating), or only data that has already been replicated to a majority of the set and is therefore guaranteed durable. Read concern does not filter on age or timestamp — it filters on durability guarantee.

These two settings are independent. You can write with w: "majority" and read with readConcern: "local", or vice versa. Choosing the right combination for each operation, rather than blindly maximizing both everywhere, is the core skill this lesson teaches.

Syntax

Write concern is passed as an option object to any write method:

db.collection.insertOne(document, { writeConcern: { w: <value>, j: <boolean>, wtimeout: <milliseconds> } });
Option Values Meaning
w 0, 1, a number, or "majority" Number of voting members (or "majority" of them) that must acknowledge the write. 0 means fire-and-forget, no acknowledgment at all.
j true / false Require the acknowledging member(s) to have written the operation to their on-disk journal, so it survives a process crash, not just a network blip.
wtimeout milliseconds How long to wait for the requested acknowledgment before returning an error. The write itself is not undone if this expires — it may still succeed and propagate.

Read concern is passed as an option to a query, or set for a whole session/transaction:

db.collection.find(query).readConcern("majority");
Level Guarantee
local (default) Returns the primary’s most recent data, even if it hasn’t replicated yet and could theoretically be rolled back later.
available Like local but used on sharded clusters to avoid an extra check for orphaned documents; slightly faster, slightly less strict.
majority Only returns data that has been replicated to and acknowledged by a majority of voting members — guaranteed durable.
linearizable Guarantees the absolute latest majority-committed value for a single document read, even accounting for a concurrent write in flight. Slower; single-document reads only.
snapshot Used inside multi-document transactions to give every read in the transaction a consistent point-in-time view.

Examples

Example 1: A majority-acknowledged write

db.orders.insertOne(
  { customerId: 1042, item: "Wireless Mouse", qty: 2, status: "pending" },
  { writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
);
{
  acknowledged: true,
  insertedId: ObjectId("64f1a2b3c4d5e6f7a8b9c0d1")
}

MongoDB does not return until a majority of the replica set’s voting members have replicated and journaled this insert. If the primary crashed the instant after returning this result, the order is guaranteed to still exist after failover, because a majority already has it.

Example 2: Reading only durable data

db.orders.find({ status: "pending" }).readConcern("majority");
[
  {
    _id: ObjectId("64f1a2b3c4d5e6f7a8b9c0d1"),
    customerId: 1042,
    item: "Wireless Mouse",
    qty: 2,
    status: "pending"
  }
]

This query only considers data that has reached the replica set’s “majority commit point.” A write accepted by the primary a moment ago but not yet replicated will not show up here yet — it will appear once it becomes majority-committed. This is exactly what you want for a report that a finance team will act on.

Example 3: readConcern and writeConcern together in a transaction

const session = db.getMongo().startSession();
session.startTransaction({
  readConcern: { level: "snapshot" },
  writeConcern: { w: "majority" }
});

const accounts = session.getDatabase("bank").accounts;
accounts.updateOne({ _id: "acct_1" }, { $inc: { balance: -100 } });
accounts.updateOne({ _id: "acct_2" }, { $inc: { balance: 100 } });

session.commitTransaction();
session.endSession();
{ acknowledged: true }
{ acknowledged: true }
// commitTransaction resolves once both updates are majority-acknowledged

Inside a transaction, readConcern: "snapshot" gives every read a single consistent point-in-time view of the data across both documents, and writeConcern: "majority" on the commit ensures the whole multi-document change is durable before the transaction reports success. Setting these at the transaction level, not per-operation, is the recommended pattern.

How It Works Step by Step

When a client sends a write with w: "majority", the primary applies the operation locally and records it in its oplog immediately — it does not wait before doing this part. What it does wait for is secondaries pulling that oplog entry and applying it themselves, then reporting their progress back to the primary. Once a majority of voting members (including the primary) have replicated the operation, the replica set’s majority commit point advances past it, and only then does the primary respond to the client.

Read concern "majority" works by having the storage engine keep multiple historical snapshots of the data. When a majority-read query runs, it is served from the snapshot that corresponds to the current majority commit point, not from the absolute latest (possibly not-yet-durable) data on disk. That is why a majority read can momentarily lag slightly behind a local read — it is intentionally showing you only what is safe.

With w: 0 (unacknowledged), the driver does not even wait for the primary to finish; you get no confirmation and no error, not even a duplicate-key error. This is rarely appropriate outside of high-volume telemetry you can afford to lose.

Common Mistakes

Mistake 1: Trusting the default write concern for critical data

The default write concern is w: 1 — only the primary needs to acknowledge. For a payment or inventory-reservation write, this is dangerous: if the primary fails over before secondaries replicate, the write can vanish even though your application already told the user it succeeded.

// Wrong: relies on the default w:1, no durability guarantee across failover
db.payments.insertOne({ orderId: 501, amount: 49.99, status: "captured" });
// Correct: require majority acknowledgment for anything you can't afford to lose
db.payments.insertOne(
  { orderId: 501, amount: 49.99, status: "captured" },
  { writeConcern: { w: "majority", wtimeout: 5000 } }
);

Mistake 2: Blindly retrying after a write concern timeout

wtimeout only bounds how long the driver waits for acknowledgment — it does not undo the write. The operation may already have succeeded on the primary and will still propagate. Retrying an insertOne unconditionally after a timeout error can silently create a duplicate document.

// Wrong: assumes a timeout means the write never happened
try {
  db.orders.insertOne(newOrder, { writeConcern: { w: "majority", wtimeout: 2000 } });
} catch (err) {
  db.orders.insertOne(newOrder, { writeConcern: { w: "majority", wtimeout: 2000 } }); // may duplicate!
}
// Correct: use a client-generated idempotency key and upsert instead of blind retry
db.orders.updateOne(
  { clientOrderId: newOrder.clientOrderId },
  { $setOnInsert: newOrder },
  { upsert: true, writeConcern: { w: "majority", wtimeout: 2000 } }
);

Best Practices

  • Default to w: "majority" for any write you would be upset to lose; reserve w: 1 or w: 0 for high-volume, disposable data like raw metrics or logs.
  • Use readConcern: "majority" for reads that drive business decisions or reports; use the default "local" for high-throughput reads where marginal staleness is acceptable.
  • Reserve "linearizable" for the rare case where a single document read must reflect the absolute latest committed write — it costs an extra round trip and only applies to single-document reads.
  • In multi-document transactions, set readConcern and writeConcern once at the transaction level rather than per operation.
  • Treat a wtimeout error as “unknown outcome,” not “write failed” — design writes to be idempotent (unique keys, upserts) so a safe retry is possible.
  • Watch replication lag on your secondaries; majority reads and writes both get slower if secondaries fall behind, since they wait on real replication progress.
  • Avoid w: 0 except for genuinely disposable data — you lose all error feedback, including duplicate key errors.

Practice Exercises

  1. Write an insertOne for a new user signup in db.users that requires majority acknowledgment and journal confirmation. Explain in a sentence what happens if only 1 of 3 replica set members is currently reachable.
  2. Given db.orders, write a find query for orders with status: "shipped" using readConcern("majority"), and explain how its result could differ from the same query with the default read concern immediately after a burst of new writes.
  3. Write a transaction skeleton (using startSession/startTransaction/commitTransaction) that transfers 50 units of quantity from one document in db.inventory to another, using readConcern: "snapshot" and writeConcern: "majority".

Summary

  • Write concern (w, j, wtimeout) controls how many replica set members must acknowledge a write, and how, before MongoDB reports success.
  • w: "majority" guarantees a write survives a primary failover; the default w: 1 does not.
  • Read concern controls what data a query is allowed to see: local (possibly not-yet-durable), majority (guaranteed durable), linearizable (absolute latest, single document), or snapshot (consistent point-in-time view inside a transaction).
  • A wtimeout error means “acknowledgment unknown,” not “write failed” — design writes to be idempotent rather than blindly retrying.
  • Match the concern level to the operation’s importance: majority for money and inventory, local for disposable high-volume data.