Ch. 12 · MongoDB

MongoDB Read Concern and Consistency Expectations

What local, majority, linearizable and snapshot read concerns promise in MongoDB, and how sessions give read-your-own-writes on secondaries.

~5 min readintermediateupdated Oct 7, 2026

A MongoDB replica set has one primary and some secondaries, and data reaches the secondaries a little after it reaches the primary. So when you read, two questions arise: which member answers, and how sure do you want to be that what you read won’t disappear? The first is read preference. The second is read concern. Mixing them up is the most common mistake in interviews on this topic, so it’s worth keeping them apart from the start.

Examples are mongosh and driver-style snippets for MongoDB 7 and 8. They’re illustrative; no replica set was available while writing.

Why a read might “disappear”

Picture a three-member replica set. The app writes a payment with w: 1 (acknowledged by the primary only). The primary applies it and acknowledges. Before any secondary copies it, the primary loses power. The secondaries elect a new primary that never saw the payment. When the old primary comes back, it rolls back that write to match the new history.

Any client that read the payment from the old primary during that brief window saw data that, in the surviving history, never happened. That’s the problem read concern addresses. It says how much of the cluster must have the data before you’re allowed to see it.

The levels

local. Returns the member’s most recent data, regardless of whether it’s replicated. This is the default for reads against the primary and secondaries. It’s the fastest, and fine for most reads. It can return writes that are later rolled back.

available. Like local, with one difference on sharded clusters: it may return orphaned documents during chunk migrations because it skips the ownership check. It’s the lowest-latency option and rarely the right one; you’ll mostly see it in multiple-choice questions.

majority. Returns only data acknowledged by a majority of voting members, so it’s durable: it can’t be rolled back. It doesn’t wait for anything; the member returns its majority-committed snapshot, which may be slightly older than its latest data. That’s the subtle part: majority can return a value that’s a little stale, but never one that disappears.

linearizable. For reads on the primary only. The primary confirms with a majority that it’s still the primary and that the read reflects all majority-acknowledged writes completed before the read started. It’s slower (an extra round with the secondaries) and should be used with maxTimeMS because it can wait. It’s for single-document reads where you need “the latest committed value”, such as checking a lock or a leader record.

snapshot. Used in multi-document transactions and some reads, it returns data from a single point in time across all the documents read. With atClusterTime you can choose that point. In a transaction committed with w: "majority", it guarantees the snapshot is majority-committed.

Here’s how you set it:

db.payments.find({ accountId: 42 }).readConcern("majority")

db.runCommand({
  find: "leaders", filter: { _id: "billing-job" },
  readConcern: { level: "linearizable" }, maxTimeMS: 2000
})
JavaScript

In drivers you can set it per client, database, collection or operation.

Read preference is a separate dial

Read preference picks the member: primary (default), primaryPreferred, secondary, secondaryPreferred or nearest. Read concern decides what that member is allowed to return. The combinations matter:

  • secondary + local: fastest, may be stale, may include writes that will be rolled back.
  • secondary + majority: may be stale, never rolled back.
  • primary + majority: current as of the majority commit point, never rolled back.
  • primary + linearizable: the latest majority-committed value, confirmed.

A common wrong answer is “use read concern majority to avoid stale reads from secondaries”. Majority avoids rolled-back reads. A secondary that’s 5 seconds behind still returns 5-second-old data under majority.

Read your own writes

The everyday problem: a user updates their display name, the page reloads and shows the old name, because the reload read from a lagging secondary. Fixes, in order of preference:

  1. Read from the primary for that flow. Simple and correct.
  2. Use a causally consistent session. The driver tracks the cluster time of your write and sends afterClusterTime with later reads, so even a secondary waits until it has caught up to your write before answering.
const session = client.startSession({ causalConsistency: true })
const users = client.db("app").collection("users")
await users.updateOne({ _id: uid }, { $set: { name: "Ana L." } },
  { session, writeConcern: { w: "majority" } })
const doc = await users.findOne({ _id: uid },
  { session, readPreference: "secondary", readConcern: { level: "majority" } })
// sees "Ana L."
JavaScript

Causal guarantees are only fully reliable when both the writes and reads use majority concern. With weaker settings, a failover can break the chain. In a web app, the session lives inside one request handler; across HTTP requests you’d have to carry the cluster time yourself, which is why “read from primary” is the usual practical answer.

Costs and what interviewers listen for

  • majority reads are cheap on modern versions (the storage engine keeps a majority-committed snapshot), so don’t claim they’re slow. They do require majority read concern support, which is on by default.
  • linearizable adds latency and only works on the primary for single-document filters.
  • Read concern doesn’t help if your writes use w: 1 and you need durability guarantees end to end. Pair majority reads with majority writes for flows that matter (and w: "majority" is the default write concern since 5.0 in most deployments).
  • Strong answers name the anomaly first (“a rolled-back read”, “a stale read”, “not seeing my own write”), then pick the setting that prevents it.

Interview practice

A payments service reads account balances from secondaries with readConcern: "majority" to offload the primary. A customer pays, the payment service writes the debit with w: "majority", and the next request (a new HTTP call) shows the old balance for a couple of seconds. Someone suggests switching to readConcern: "linearizable" on the secondaries. What’s wrong with that, and what would you do?

Answer

First, linearizable only works on the primary, so it can’t be used on secondaries at all. Second, the problem isn’t durability: the old balance came from a secondary whose majority-committed snapshot hadn’t caught up yet. majority read concern prevents reading rolled-back data, not stale data.

The real requirement is read-your-own-writes for a customer right after they pay. Options: route balance reads after a write to the primary (simplest, and balances are a small, important read), or carry causal consistency across requests by saving the operation time from the write’s session and starting the next read’s session with advanceClusterTime/advanceOperationTime, so the secondary waits until it has applied the debit. That second option is more complex and needs care in a stateless API. For a payment balance I’d read from the primary with majority and keep secondary reads for things like statements and analytics, where a few seconds of staleness is acceptable.

Further reading

More in MongoDB

read ✓MongoDB · mid

MongoDB Replica Sets and Primary Elections

How a MongoDB replica set notices a dead primary, elects a new one in seconds, and what your app sees: errors, retries and rolled-back writes.

~5 min readread →
read ✓MongoDB · mid

MongoDB Write Concern and Acknowledgement

What w: 1, w: "majority" and j: true actually promise in MongoDB, what a wtimeout error means, and how to pick a write concern per operation.

~5 min readread →
read ✓MongoDB · mid

MongoDB Bulk Writes

Batch a nightly upsert job with bulkWrite, pick ordered or unordered mode, and read writeErrors so partial failures don't get lost or retried.

~6 min readread →
esc