Read Preference

A MongoDB replica set has one primary and one or more secondaries, and by default every read goes to the primary. Read preference is the client-side setting that lets you change that: it tells the driver or mongosh which member of the replica set (or, in a sharded cluster, which member of each shard’s replica set) is allowed to serve a given read. Getting it right matters because it directly trades off consistency, availability, and read latency — the wrong choice can mean stale reports, or an application that goes read-only during a failover when it didn’t need to.

Overview / How it works

Replication in MongoDB is asynchronous: the primary applies a write, records it in its oplog (a capped collection acting as a change log), and secondaries pull and replay oplog entries in the background. That means a secondary is always at least a little behind the primary — usually milliseconds, but potentially seconds or minutes under load, network partition, or heavy write traffic. Read preference is how you tell MongoDB whether that lag is acceptable for a given query.

There are five read preference modes:

Mode Reads from Typical use case
primary Primary only (default) Reads that must reflect the latest write (read-your-own-writes)
primaryPreferred Primary if available, else a secondary Mostly-consistent reads that can tolerate brief staleness during failover
secondary Secondaries only (errors if none available) Offloading analytics/reporting so it never competes with the primary
secondaryPreferred A secondary if available, else the primary Read scaling with a safety net
nearest Whichever member (primary or secondary) has the lowest network latency Latency-sensitive reads where mild staleness is fine

Under the hood, every driver (and mongosh) runs a background process called Server Discovery and Monitoring (SDAM): it opens monitoring connections to every known replica set member and sends periodic heartbeats (hello command, formerly isMaster) to learn each member’s role (primary/secondary), round-trip latency, and replication lag estimate. This builds a live topology description. When you issue a read with a given read preference, the driver filters that topology down to eligible servers, then picks one — among servers within a small latency window of each other (the localThresholdMS, 15ms by default) it round-robins for load balancing rather than always hitting the single fastest node.

Two extra dials refine this selection:

  • Tag sets — arbitrary key/value labels attached to replica set members in the replica set configuration (for example { region: "us-east", usage: "reporting" }). A read preference can require a matching tag set, so you can pin reporting queries to a member explicitly labeled for reporting load, or route reads to the secondary in the same data center as the application to cut network latency.
  • maxStalenessSeconds — a floor (minimum 90 seconds) on how far behind the primary a secondary is allowed to be before the driver excludes it from selection. Without it, secondary or nearest can happily route you to a member that is minutes behind if that’s the only one that matches your tags.

Read preference is a distinct concept from read concern (which controls what durability/visibility guarantee a read has — local, majority, or linearizable) and from write concern (how many members must acknowledge a write). Read preference only answers which node serves the read; it says nothing about whether that node’s data has been majority-committed. In a sharded cluster, mongos applies your read preference independently to each shard it must query, so a single aggregation can end up reading from a mix of primaries and secondaries across shards, each with its own lag.

Syntax

In mongosh, read preference can be set per cursor, per connection, or in the connection string:

// Per query (cursor-level)
db.collection.find(query).readPref(mode, tagSet);

// For the whole mongosh session (connection-level)
db.getMongo().setReadPref(mode, tagSet);

// Via connection string
// mongodb://host1,host2,host3/mydb?replicaSet=rs0
//   &readPreference=secondaryPreferred
//   &readPreferenceTags=region:us-east
//   &maxStalenessSeconds=90
  • mode — one of "primary", "primaryPreferred", "secondary", "secondaryPreferred", "nearest".
  • tagSet — an array of tag documents tried in order, e.g. [{ region: "us-east" }, {}]; the trailing empty document {} means “match any member” as a fallback.
  • readPreferenceTags (connection string) — comma-separated key:value pairs; repeat the parameter for multiple fallback tag sets.
  • maxStalenessSeconds — minimum 90; excludes secondaries estimated to be lagging more than this.

Examples

Example 1: Setting a read preference for the whole mongosh session

db.getMongo().getReadPrefMode();

db.getMongo().setReadPref("secondaryPreferred");

db.orders.find({ status: "shipped" });

Output:

'primary'
[
  { _id: ObjectId('66f1...'), orderId: 501, status: 'shipped' },
  { _id: ObjectId('66f2...'), orderId: 512, status: 'shipped' }
]

The first call confirms the default mode is primary. After setReadPref, every subsequent query on this connection prefers a secondary, only falling back to the primary if no secondary is reachable.

Example 2: Overriding read preference for a single query

db.orders
  .find({ status: "shipped" })
  .readPref("secondary")
  .toArray();

Output:

[
  { _id: ObjectId('66f1...'), orderId: 501, status: 'shipped' },
  { _id: ObjectId('66f2...'), orderId: 512, status: 'shipped' }
]

Unlike Example 1, readPref("secondary") on the cursor requires a secondary and will error if none is available — it does not fall back to the primary. This is useful when you want a hard guarantee that a reporting query never touches the primary.

Example 3: Tag sets to target a region

db.products.find({ category: "electronics" }).readPref("secondary", [
  { region: "us-east", usage: "reporting" },
  { region: "us-east" },
  {},
]);

Output:

[
  { _id: ObjectId('66f3...'), name: 'USB-C Hub', category: 'electronics' },
  { _id: ObjectId('66f4...'), name: 'Webcam', category: 'electronics' }
]

MongoDB tries each tag set in order: first a secondary tagged for reporting in us-east, then any us-east secondary, then finally any secondary at all. This lets you prefer a purpose-built replica while still having graceful fallbacks instead of an outright failure.

Example 4: Node.js driver with a custom staleness limit

import { MongoClient } from "mongodb";

const client = new MongoClient(
  "mongodb://:@/mydb?replicaSet=rs0"
);
await client.connect();

const db = client.db("mydb");
const orders = db.collection("orders");

const results = await orders
  .find({ status: "shipped" })
  .withReadPreference("secondaryPreferred")
  .toArray();

console.log(results.length);

Output:

2

withReadPreference mirrors mongosh’s readPref for the official driver, letting you set the mode per query while the MongoClient can carry a different default.

How it works step by step

  1. The driver’s SDAM background monitor sends periodic hello heartbeats to every seed and discovered member, recording each one’s role, round-trip time, and (from the primary) each secondary’s last-applied oplog timestamp.
  2. When your query executes, the driver reads its read preference mode, optional tag sets, and optional maxStalenessSeconds.
  3. It filters the topology: first by role (primary vs. secondary, per the mode), then by tag set match (trying each tag document in order until one matches at least one member), then by staleness (excluding any secondary whose estimated lag exceeds the limit).
  4. Among the servers remaining, it groups those within localThresholdMS (15ms by default) of the fastest one, and picks randomly from that group — spreading load rather than pinning every query to a single “nearest” node.
  5. The query executes against the chosen member. If that member becomes unreachable mid-operation, the driver retries server selection (subject to retryable reads) against the next eligible candidate.

Common Mistakes

Mistake 1: Reading your own write from a secondary

// Wrong: insert, then immediately read back from a secondary
await orders.insertOne({ orderId: 501, status: "placed" });

const order = await orders
  .find({ orderId: 501 })
  .withReadPreference("secondary")
  .toArray();

console.log(order); // could be [] -- the write may not have replicated yet

Because replication is asynchronous, the secondary you land on may not have applied the insert yet, so the read can come back empty or stale. This is the single most common read preference bug: a page that shows “order not found” right after the user placed it.

// Fixed: read from the primary when you need to see your own write
await orders.insertOne({ orderId: 501, status: "placed" });

const order = await orders
  .find({ orderId: 501 })
  .withReadPreference("primary")
  .toArray();

console.log(order); // [{ orderId: 501, status: 'placed', _id: ObjectId('...') }]

Mistake 2: Using nearest across regions without a staleness limit

// Wrong: nearest picks lowest latency, not freshest data
const results = await db
  .collection("events")
  .find({ type: "click" })
  .withReadPreference("nearest")
  .toArray();

In a geographically distributed replica set, the lowest-latency member is often a secondary in a different region than the primary — and it may be replicating over a slower link, lagging by minutes. nearest alone optimizes only for round-trip time, not freshness.

import { ReadPreference } from "mongodb";

// Fixed: cap acceptable staleness explicitly
const readPref = new ReadPreference("nearest", [], {
  maxStalenessSeconds: 90,
});

const results = await db
  .collection("events")
  .find({ type: "click" })
  .withReadPreference(readPref)
  .toArray();

Now any secondary estimated to be more than 90 seconds behind is excluded from consideration before latency is even compared.

Best Practices

  • Keep the default (primary) unless you have a concrete reason to change it — it’s the only mode that guarantees you see your own recent writes.
  • Use secondary or secondaryPreferred for reporting, analytics, and backups so those workloads never compete with your primary’s write throughput.
  • Always set maxStalenessSeconds (90 or higher) when using secondary, secondaryPreferred, or nearest in a multi-region deployment.
  • Use tag sets to route latency-sensitive reads to a same-region secondary instead of relying on nearest alone.
  • Pair secondary reads with readConcern: "majority" when you need a guarantee the data won’t be rolled back after a failover.
  • Check real replication lag with rs.printSecondaryReplicationInfo() before assuming secondary reads are “basically real-time.”
  • Remember that in a sharded cluster, mongos applies your read preference per shard, so different shards in the same query can return data of different staleness.
  • Never use secondary-only modes for a query whose result feeds back into a decision requiring the very latest write (inventory checks before checkout, balance checks before a transfer).

Practice Exercises

  • Write a mongosh query on db.orders that prefers a secondary tagged { region: "eu-west" }, falls back to any secondary, and finally falls back to the primary if no secondary is reachable. (Hint: which mode falls back to the primary automatically?)
  • A nightly reporting job must never read from the primary, even during a failover. Which read preference mode enforces that, and what happens to the job if every secondary is temporarily down?
  • Explain, in your own words, why a checkout flow that does insertOne followed immediately by a find with readPreference: "secondaryPreferred" can intermittently show the order as missing, and describe two different fixes.

Summary

  • Read preference decides which replica set member serves a read; it does not affect writes.
  • The five modes are primary, primaryPreferred, secondary, secondaryPreferred, and nearest, trading off consistency for availability and read scaling.
  • Secondaries lag asynchronously; reading from one can mean missing or stale data compared to the primary.
  • Tag sets target specific members (by region, purpose, etc.); maxStalenessSeconds excludes secondaries that are too far behind.
  • Drivers select servers via ongoing SDAM heartbeat monitoring, filtering by role, tags, and staleness, then load-balancing among the closest matches.
  • In sharded clusters, mongos applies your read preference independently per shard.
  • Default to primary for anything requiring read-your-own-writes; reserve secondary reads for reporting, analytics, and load offloading with an explicit staleness cap.