Type something to search...
MongoDB Read Preferences and Write Concerns Explained

MongoDB Read Preferences and Write Concerns Explained

A replica set gives you several copies of your data, but it doesn't decide for you how to use them. When your application writes, how many copies must exist before it's told "success"? When it reads, should it go to the primary, which always has the latest data, or to a nearby secondary that might be a second behind? MongoDB leaves these decisions to you, and the defaults, while sensible, aren't right for every operation.

Write concern controls how durable a write must be before MongoDB acknowledges it. Read preference controls which replica set members a read may be sent to. And a third setting, read concern, controls how committed the data you read must be. Together they let you choose, per operation if you like, between speed, freshness, and durability.

This guide covers what each write concern level guarantees, how read preferences route queries, where read concern fits in, how to get read-your-own-writes consistency on secondaries, and which combinations work best for common scenarios.

Write Concern: When Is a Write "Done"?

When the primary receives a write, it applies it locally and records it in the oplog. Secondaries then copy it. Write concern tells the primary how long to wait before replying to the client.

It has three parts:

  • w: how many members must have the write. 0, 1, a specific number, "majority", or a custom tag set name.
  • j: whether the write must be in the on-disk journal before counting it.
  • wtimeout: how long to wait, in milliseconds, before returning an error if the w requirement isn't met.

The Levels in Practice

Write concernAcknowledged whenSurvives primary crash?Latency
w: 0Never (fire and forget)No, and you won't know it failedLowest
w: 1The primary has applied itNot guaranteed, can roll backLow
w: 1, j: trueThe primary has journaled itSurvives restart, can still roll back after failoverLow
w: "majority"A majority of voting members have itYesHigher
w: 3 (or a tag)That many or tagged members have itDepends on the numberHighest

The key idea is rollback. With w: 1, if the primary acknowledges your write and then crashes before any secondary copies it, a new primary is elected without that write. When the old primary rejoins, it rolls the write back. Your application was told the write succeeded, but it's gone. With w: "majority", the write is on a majority of members, and MongoDB's election rules guarantee the next primary will have it.

Since MongoDB 5.0, the default write concern is "majority" for most replica set configurations. The exception is some deployments with arbiters, where a majority of data-bearing members may not be achievable; there the implicit default falls back to w: 1. You can check the cluster-wide default:

db.adminCommand({ getDefaultRWConcern: 1 });
{
  defaultReadConcern: { level: "local" },
  defaultWriteConcern: { w: "majority", wtimeout: 0 },
  defaultWriteConcernSource: "implicit",
  defaultReadConcernSource: "implicit",
  ok: 1
}

Setting Write Concern

You can set write concern in the connection string, on a database or collection object, or per operation. More specific settings override broader ones.

import { MongoClient } from "mongodb";

// Client-wide default
const client = new MongoClient(
  "mongodb+srv://app:secret@cluster0.example.mongodb.net/?w=majority&wtimeoutMS=5000",
);
const db = client.db("shop");

// Collection-level: analytics events can tolerate w: 1
const events = db.collection("events", { writeConcern: { w: 1 } });

// Per operation: a payment record must be majority-acknowledged and journaled
await db
  .collection("payments")
  .insertOne(
    { orderId: "ord_9001", amount: 129.0, at: new Date() },
    { writeConcern: { w: "majority", j: true, wtimeoutMS: 5000 } },
  );

In PyMongo, write concern is set on the client or with with_options:

from pymongo import MongoClient, WriteConcern

client = MongoClient("mongodb://db-1,db-2,db-3/?replicaSet=rs0&w=majority")
db = client.shop

events = db.get_collection("events", write_concern=WriteConcern(w=1))
payments = db.payments.with_options(
    write_concern=WriteConcern(w="majority", j=True, wtimeout=5000)
)
payments.insert_one({"orderId": "ord_9001", "amount": 129.0})

What wtimeout Really Means

A wtimeout error does not mean the write failed. It means the primary couldn't confirm the requested number of copies within the time limit. The write has been applied on the primary and will most likely replicate once the slow secondary catches up. Treat a write concern timeout as "outcome uncertain," not "rolled back," and make sure retrying the operation is safe (for example, by using a unique key or an upsert).

Read Preference: Where Do Reads Go?

By default, all reads go to the primary. Read preference lets you send them elsewhere. There are five modes:

ModeReads go toUse it for
primaryThe primary only (default)Anything that must see the latest writes
primaryPreferredThe primary, or a secondary if no primary is availableReads that should keep working during elections
secondarySecondaries onlyIsolating heavy analytics from the primary
secondaryPreferredSecondaries, or the primary if none is availableOffloading read-mostly, staleness-tolerant work
nearestThe member with the lowest network latency, of any typeGeo-distributed apps where latency dominates

Setting it looks much like write concern:

import { ReadPreference } from "mongodb";

// Connection string
// ...?readPreference=secondaryPreferred&maxStalenessSeconds=120

// Per collection
const reports = db.collection("orders", {
  readPreference: ReadPreference.SECONDARY_PREFERRED,
});

// Per operation
const recent = await db
  .collection("orders")
  .find({ status: "shipped" })
  .withReadPreference("nearest")
  .limit(50)
  .toArray();

The Staleness Trade-Off

Secondaries apply writes asynchronously, so any read from a secondary might be missing recent writes. Usually the gap is milliseconds. During heavy load, network trouble, or maintenance, it can be minutes.

maxStalenessSeconds lets you set a limit. The driver estimates each secondary's lag and won't choose one that's further behind than the limit. The minimum allowed value is 90 seconds, because lag estimates are based on periodic heartbeats and aren't precise enough for tighter bounds.

const analytics = db.collection("orders", {
  readPreference: new ReadPreference("secondary", undefined, {
    maxStalenessSeconds: 120,
  }),
});

Tag Sets: Choosing Specific Members

You can tag replica set members with arbitrary labels and target them with read preference tag sets. A common pattern is a dedicated analytics member:

// In the replica set config (run by an admin)
cfg = rs.conf();
cfg.members[2].tags = { workload: "analytics", region: "us-east" };
rs.reconfig(cfg);
// In the reporting service
const reports = db.collection("orders", {
  readPreference: new ReadPreference("secondary", [{ workload: "analytics" }]),
});

Heavy aggregation queries now land on that member and don't compete with production traffic on the primary or other secondaries. On Atlas, analytics nodes implement exactly this pattern with a predefined nodeType: ANALYTICS tag.

Tag sets are also how you keep reads in-region for a geographically distributed replica set: [{ region: "eu-west" }, {}] means "prefer a member in eu-west, otherwise any eligible member." The empty document at the end is a fallback that matches everything.

Why Secondaries Don't Scale Reads as Much as You'd Think

It's tempting to see two secondaries as two extra servers' worth of read capacity. In practice, secondaries do all the same write work as the primary (they apply every change), so they have less spare capacity than it seems. And if your application depends on secondary reads for capacity, losing one member means the rest must absorb its load, at exactly the moment the set is already degraded.

Use secondary reads for isolation (keep reports off the primary) and latency (read from a nearby member), not as a primary scaling strategy. To scale reads, add indexes, add caching, or scale the instance up; to scale writes and data size, look at sharding.

Read Concern: How Committed Must the Data Be?

Read preference picks the member. Read concern picks which version of the data on that member you're allowed to see.

Read concernReturns
localThe member's most recent data, which may later be rolled back (default)
availableLike local; on sharded clusters it may include orphaned documents
majorityOnly data acknowledged by a majority, which can't be rolled back
linearizableMajority data that reflects all writes completed before the read began
snapshotMajority data from a single point in time (transactions and some reads)

The pairing matters. If you write with w: "majority" and read with readConcern: "majority", you only ever see data that's durable. If you read with local, you might read a write that later gets rolled back, which is rare but possible during failovers.

const confirmed = await db
  .collection("payments")
  .find({ orderId: "ord_9001" }, { readConcern: { level: "majority" } })
  .toArray();

linearizable is the strongest guarantee but only works for single-document reads on the primary and is noticeably slower. Reserve it for cases like reading a distributed lock where you must not see stale state.

Read Your Own Writes on Secondaries

Here's a classic bug. A user updates their profile, the app writes to the primary, then redirects to the profile page, which reads from a secondary. The secondary hasn't applied the update yet, and the user sees their old data.

Causally consistent sessions fix this. Within a session, the driver tracks the cluster time of each operation and tells the server "don't answer until you've caught up to at least this point." With majority read and write concerns, reads in the session always reflect the session's earlier writes, even from a secondary.

const session = client.startSession({ causalConsistency: true });
try {
  const profiles = db.collection("profiles", {
    readConcern: { level: "majority" },
    writeConcern: { w: "majority" },
  });

  await profiles.updateOne(
    { _id: userId },
    { $set: { displayName: "Maria K." } },
    { session },
  );

  const doc = await profiles.findOne(
    { _id: userId },
    { session, readPreference: ReadPreference.SECONDARY },
  );
  console.log(doc.displayName); // "Maria K.", guaranteed
} finally {
  await session.endSession();
}

The secondary may wait briefly to catch up, but it will never return data older than the write. In a web app, you'd typically only need this for the request immediately after a write, or you'd simply read that page from the primary.

Choosing the Right Combination

Here are sensible starting points for common workloads:

WorkloadWrite concernRead preferenceRead concern
Orders, payments, account changesmajority, jprimarymajority
General CRUD for a web appmajorityprimarylocal
Logs, metrics, clickstream1secondaryPreferredlocal
Dashboards and reportsn/asecondary with analytics tagmajority
Global app, read-heavy contentmajoritynearest with region tagslocal
Status page that must stay upmajorityprimaryPreferredlocal

A few notes on these:

  • For the general web app, local read concern on the primary is fine: the primary's data is almost always majority-committed within milliseconds.
  • For logs, w: 1 trades a tiny risk of losing a few events during a failover for lower write latency. That's often a good deal for telemetry but never for money.
  • w: 0 doesn't appear anywhere. You can't even detect errors like duplicate keys with it. There's almost never a good reason to use it.

Common Mistakes

Lowering write concern to hide latency. If majority writes are slow, the cause is usually a lagging or distant secondary. Switching to w: 1 hides the symptom and exposes you to rollbacks. Find and fix the slow member instead.

Using secondary without a fallback. If all secondaries are down or too stale, reads with mode secondary fail. For anything user-facing, use secondaryPreferred or primaryPreferred so reads still succeed.

Assuming secondary reads are consistent. Two consecutive reads from different secondaries can go backwards in time, showing a newer value and then an older one. If order matters, use a causally consistent session or read from the primary.

Treating wtimeout as failure. The write may well have been applied. Retrying a non-idempotent operation after a timeout can create duplicates. Design writes so retries are safe.

Setting maxStalenessSeconds below 90. The driver rejects values below 90 seconds. If you need fresher data than that, read from the primary.

Forgetting per-operation overrides exist. You don't have to pick one setting for the whole app. Keep safe defaults on the client and loosen them only for the specific collections or queries that benefit.

Conclusion

Write concern decides how many members must hold a write before it's acknowledged, and w: "majority" is what makes writes survive failovers. Read preference decides which members serve reads, trading freshness for isolation or latency. Read concern decides whether you can see data that might still be rolled back. Keep strong defaults (majority writes, primary reads), then relax them deliberately for workloads like logging and analytics, and use causally consistent sessions when you need read-your-writes on secondaries.

As a next step, run db.adminCommand({ getDefaultRWConcern: 1 }) on your cluster and grep your codebase for readPreference, writeConcern, and w= in connection strings. Make a short list of every place the defaults are overridden, and check that each override still has a good reason to exist.

Tags :
Share :

Related Posts

A Complete Guide to MongoDB Query Operators

A Complete Guide to MongoDB Query Operators

Your first MongoDB queries are usually simple equality filters: find the user with this email, find orders with this status. That covers a surprising

Continue Reading
Async MongoDB in Python with Motor and FastAPI

Async MongoDB in Python with Motor and FastAPI

FastAPI runs your endpoints on an event loop. That's what lets a single worker juggle hundreds of concurrent requests: while one request waits on the

Continue Reading
Atlas Online Archive: Tiering Cold Data to Cut Costs

Atlas Online Archive: Tiering Cold Data to Cut Costs

Look at almost any production database and you'll find the same shape. A small slice of recent data gets nearly all the reads and writes: this week's

Continue Reading