Filter concepts by levelShowing all levels.

MongoDB · Section 25

Replication

Level
advanced
Read
35 min
Concepts
8

What a replica set is and why it is MongoDB's baseline high-availability building block, the primary/secondary roles and how they can change over the set's lifetime, the oplog as the actual mechanism secondaries use to stay in sync, how elections pick a new primary automatically on failure, the five read preference modes and what each falls back to, replica lag as the concrete number that bounds staleness, and why reading from secondaries can introduce stale — including read-your-own-writes-violating — reads.

MongoDB overview

What is true here

  1. A replica set is several servers holding the same data, with one primary accepting writes at a time — MongoDB's baseline answer to server failure.
  2. Secondaries stay in sync by tailing and replaying the primary's idempotent, capped oplog — not by copying raw data files.
  3. An election picks a new primary by majority vote among reachable members, preferring the most up-to-date candidate; a brief write-unavailability window during it is expected.
  4. Read preference (primary, primaryPreferred, secondary, secondaryPreferred, nearest) decides which member a read can reach, independent of write and read concern.
  5. Replica lag — the delay between the primary and a specific secondary — is the concrete number that bounds how stale a secondary-routed read can be, and the reason read-your-own-writes needs the primary or a session for causal consistency.

What you will be able to do

  • Explain what a replica set protects against and how
  • Describe how secondaries actually stay in sync via the oplog
  • Explain what happens during an election, including the expected unavailability window
  • Choose the right read preference mode for a given read's tolerance for staleness
  • Diagnose a stale-read bug caused by routing a read-after-write to a lagging secondary

How replica sets work

The building block itself, the roles its members play, the mechanism keeping them in sync, and how failover happens automatically.

Replica sets

coreintermediate

A replica set is a group of MongoDB servers holding the same data, so the loss of any one of them does not mean data loss or downtime. One member is the primary and accepts writes; the others are secondaries that continuously copy the primary's changes.

Think of it as

Think of it as several identical copies of the same database kept in sync automatically, with one designated as the current point of truth for writes at any given moment — and if that one disappears, the group elects a different member to take over the role, without anyone reconfiguring anything by hand.

text
rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "m1" }, { _id: 1, host: "m2" }, { _id: 2, host: "m3" }] })

What we're doing: Show why a lone standalone server is a single point of failure a replica set exists to remove.

text
// Standalone: one server, one copy of the data
// -> that server fails, the application has zero availability and the data
//    is only as safe as that one machine's disk

// Replica set: 3 members hold the same data
// -> one member fails, the other two still hold the full dataset, and one
//    of them is elected primary automatically — the application keeps working
2
A standalone server has no automatic recovery path — its failure is the application's failure, and any unflushed data on that one disk is genuinely at risk.
6
A replica set survives the same failure because the data already existed on other members before it happened, and the group can continue serving both reads and, after a short election, writes.

Why this works: The core value of a replica set is redundancy plus automatic recovery — the same data exists in more than one place, and the group can reorganize itself around a lost member without needing a human to intervene before service resumes.

A three-member replica set
writesoplogoplog

Application

sends writes

Primary

accepts all writes

Secondary

replicates from primary

Secondary

replicates from primary

  • Application — sends writes
    • leads to Primary (writes)
  • Primary — accepts all writes
    • leads to Secondary (oplog)
    • leads to Secondary (oplog)
  • Secondary — replicates from primary
  • Secondary — replicates from primary

Running production MongoDB as a single standalone server "for now," planning to add replication later

Wrong

text
// One standalone mongod, serving production traffic, with a plan to convert it to a replica set eventually

Better

text
// Even a single-member replica set can later add members with zero application-code changes — start there instead

What you see: The standalone server crashes or its disk fails, and there is no other copy of the data anywhere — a routine hardware failure becomes permanent data loss.

Why: A standalone server has no automatic recovery path at all, and converting one to a replica set later is more disruptive than starting as one — a replica set (even a single member, initially) costs nothing extra in normal operation and can grow into full redundancy without an application-visible migration.

Remember: A replica set is several servers holding the same data, one primary accepting writes at a time, with automatic election of a new primary if the current one becomes unreachable — this is MongoDB's baseline answer to "what happens when one server dies".

See also: primary and secondary members · election and failover

Primary and secondary members

standardintermediate

The primary is the one member accepting writes at any moment; secondaries continuously apply the same changes the primary made, and can optionally serve reads. Which member is primary can change over time — it is a role, not a fixed identity of one particular server.

Think of it as

Primary is a role the replica set assigns to exactly one member at a time, not a permanent label on a specific machine — after an election, a former secondary can become primary, and the previous primary (once it recovers) rejoins as a secondary.

text
db.hello() (or rs.status()) — reports which member currently holds the primary role

What we're doing: Show a write correctly targeting the primary, and what happens if it is sent to a secondary instead.

javascript
// The driver automatically routes writes to whichever member currently holds
// the primary role — application code does not name a specific server
db.orders.insertOne({ ... })

// If a write somehow reaches a secondary directly (e.g. a misconfigured
// direct connection), it is rejected — a secondary is read-only for
// replicated data
// -> "not master" / "NotWritablePrimary" error
3
The driver, connected to the whole replica set (not one member), tracks which member is currently primary and sends writes there automatically.
7
A secondary refuses writes outright — this is by design, since accepting one would create data that diverges from what it is supposed to be replicating.

Why this works: Restricting writes to exactly one member at a time is what keeps the replica set's copies from diverging — if two members could both accept independent writes, there would be no single, consistent version of the data to replicate from.

Remember: Primary and secondary are roles, not permanent identities — exactly one member is primary at a time, all writes go there, and which member holds that role can change across an election.

See also: replica sets overview · election and failover

Replication via the oplog

standardadvanced

The oplog is a special capped collection on the primary recording every write in order. Secondaries continuously read the primary's oplog and apply the same operations themselves — that is the actual mechanism behind "secondaries stay in sync".

Think of it as

The oplog is a running transcript of "what changed and in what order" — secondaries do not copy the raw data files directly during normal operation, they replay the transcript, which is what lets them stay a consistent, independently-verifiable copy rather than a byte-for-byte mirror.

text
// local.oplog.rs — a capped collection on the primary; secondaries tail it continuously

What we're doing: Show conceptually what an oplog entry looks like and why idempotency matters for safe replay.

text
// Simplified oplog entry for an update
{ op: "u", ns: "shop.orders", o: { $set: { status: "shipped" } }, o2: { _id: "o1" } }

// Idempotent: applying this entry twice still leaves status as "shipped" —
// re-applying it after a resumed replication gap causes no harm
2
The entry records the operation itself, not just the resulting document — a secondary replays this exact operation rather than copying a snapshot.
5
Idempotency is what makes it safe for a secondary to resume from roughly where it left off after a brief disconnect, even if that means re-applying an entry it may have already processed.

Why this works: A secondary that briefly disconnects and reconnects needs to safely pick up wherever it left off without risking double-applying a change — idempotent oplog entries are exactly what makes that safe, rather than requiring an all-or-nothing full resync on every hiccup.

Remember: Secondaries replicate by tailing the primary's oplog — a capped, idempotent record of every write — and replaying it, not by copying raw data files. The oplog's fixed size sets the replication window for how long a disconnect can last before a full resync is needed.

See also: primary and secondary members · replica lag

Elections and failover

coreadvanced

When the primary becomes unreachable — a crash, a network partition, a planned step-down — the remaining voting members hold an election and pick a new primary automatically, typically within a few seconds. The application does not need to intervene, though it does experience a brief window where writes fail.

Think of it as

An election is a vote among the members that can still see each other: whoever has replicated the most data and gets a majority of votes becomes the new primary. This is exactly why an odd number of voting members matters — it guarantees a majority is always decidable, with no possibility of a tie deadlocking the vote.

text
rs.stepDown(<secs>) — deliberately trigger an election, e.g. before planned maintenance on the current primary

What we're doing: Show application code experiencing and recovering from a brief write-unavailability window during an election.

javascript
try {
  await orders.insertOne(order)
} catch (err) {
  if (err.name === "MongoServerSelectionError") {
    // No primary currently reachable — likely mid-election.
    // A short retry after a brief delay is the normal, correct response.
  }
  throw err
}
4
This error during the few-second election window is expected behavior, not a sign anything is broken — the driver will detect the new primary automatically once elected.

Why this works: An election has to briefly leave the replica set without a primary — accepting writes on more than one member during that window would risk the exact data divergence primary/secondary roles exist to prevent, so a short unavailability window is the deliberate, safer trade.

A member's role across a failover
primary becomesunreachablewins majorityvotesteps down / losescontact with majority

Secondary

Candidate (during election)

Primary

  • Secondary
    • → Candidate (during election) when primary becomes unreachable
  • Candidate (during election)
    • → Primary when wins majority vote
  • Primary
    • → Secondary when steps down / loses contact with majority

Treating a MongoServerSelectionError during an election as a fatal failure instead of a brief, expected window

Wrong

text
// Catching the error, logging it as a critical failure, and alerting on-call immediately for every occurrence

Better

text
// Retry briefly (the driver often does this automatically for a bounded window) before treating it as an actual outage

What you see: A routine, few-second election — a normal part of replica set operation — pages an engineer as if production were down.

Why: A short write-unavailability window during an election is expected behavior, not an outage — alerting on every occurrence produces alert fatigue that makes a genuine, longer outage easier to miss among the noise.

Remember: An election picks a new primary by majority vote among reachable members, preferring the one with the most up-to-date data — a few-second write-unavailability window during this process is expected behavior, not a failure.

See also: primary and secondary members · voting members and elections

Advertisement

Reading from a replica set

The read preference modes available, and the concrete staleness cost of routing reads to secondaries.

What read preference controls

standardadvanced

Read preference decides which member(s) of a replica set a read is allowed to go to — the primary only, secondaries, or a mix — independent of write concern, which is a completely separate setting about writes.

Think of it as

Every replica set has spare read capacity sitting on its secondaries, doing nothing but replicating by default. Read preference is the setting that decides whether the application is allowed to spend some of that capacity, and what it accepts giving up (recency, in exchange for read capacity and locality) to do so.

text
db.collection.find({...}).readPref("secondaryPreferred")

What we're doing: Route an analytics query to secondaries to keep it off the primary's write path, while application writes and critical reads stay on the primary by default.

javascript
// Default: primary — most reads, especially anything read-your-write sensitive
await orders.findOne({ _id: orderId })

// Explicitly routed to secondaries: a heavy analytics scan that can tolerate
// slightly stale data, kept off the primary's write-serving capacity
await orders.find({ placedAt: { $gte: monthStart } }).readPref("secondary")
2
The default primary read preference guarantees this order lookup sees the most recent write, which matters right after a user places an order.
6
Explicitly opting into "secondary" here trades a small amount of recency for keeping a heavy scan off the primary, which is otherwise busy serving the application's write-sensitive traffic.

Why this works: The right read preference is a per-query decision, not a single global setting — a read that must reflect the latest write belongs on the primary, while a read that can tolerate slightly stale data is a good candidate to move off it.

Remember: Read preference decides which member a read is allowed to reach — primary for guaranteed-latest data, secondaries to spread load or reduce latency at the cost of some recency. It is independent of write concern and read concern.

See also: read preference modes · stale reads from secondaries

The five read preference modes

standardadvanced

Five named modes cover every point on the primary-vs-secondary, guaranteed-vs-best-effort spectrum: primary (only), primaryPreferred (primary if reachable, else secondary), secondary (only), secondaryPreferred (secondary if reachable, else primary), and nearest (whichever member has the lowest network latency, primary or secondary).

Think of it as

Read each mode name as a literal instruction about where reads may go and what happens when the preferred choice is unavailable — the "Preferred" suffix is always "try this first, fall back if it is not reachable", never "only if convenient".

text
.readPref("primary" | "primaryPreferred" | "secondary" | "secondaryPreferred" | "nearest")

What we're doing: Choose the right mode for two workloads with different tolerance for staleness and unavailability.

javascript
// Must never see stale data, and can tolerate a brief failure during an election
db.accounts.findOne({ _id }).readPref("primary")

// A dashboard that should keep working even if it occasionally shows slightly
// older numbers, and should never be blocked by a primary-only requirement
db.dashboardMetrics.find({...}).readPref("secondaryPreferred")
2
primary here is a deliberate choice to fail rather than silently read stale data — appropriate when staleness is worse than a brief unavailability.
6
secondaryPreferred here accepts some staleness in exchange for reads that keep working even during a primary-only outage, appropriate when availability matters more than absolute recency for this data.

Why this works: Each mode encodes a specific answer to "what do I do when my preferred target is unreachable" — picking the wrong one either fails a read that could have tolerated staleness, or silently serves stale data to a read that could not.

The five modes

The five modes
ModePrefersFalls back toTypical use
primaryprimary onlyfails (no fallback)anything needing guaranteed-latest data
primaryPreferredprimarysecondarymostly-latest reads that can tolerate brief fallback
secondarysecondary onlyfails (no fallback)reads that must never touch the primary
secondaryPreferredsecondaryprimaryread-heavy workloads that can tolerate some staleness
nearestlowest-latency memberlatency-sensitive reads, geographically distributed clients

Together

javascript
db.reports.find({...}).readPref("secondaryPreferred")
db.userSessions.find({...}).readPref("nearest")

Remember: primary/secondary have no fallback and fail if their target is unreachable; primaryPreferred/secondaryPreferred fall back to the other; nearest picks by latency regardless of role — match the mode to what the read can actually tolerate.

See also: read preference overview · stale reads from secondaries

Replica lag

standardadvanced

Replica lag is how far behind a secondary is in applying the primary's oplog — measured in time, not operation count. Under normal conditions it is milliseconds; under load, network issues, or a slow secondary, it can grow to seconds or more, and that gap is exactly how stale a read from that secondary can be.

Think of it as

Lag is the delay between "the primary knows this" and "this specific secondary knows this too" — reading from a secondary always means reading through that delay, whatever it currently happens to be, which is why lag is the number that turns "reads from secondaries" from a theoretical trade-off into a concrete one.

text
rs.printSecondaryReplicationInfo() — reports each secondary's lag behind the primary

What we're doing: Show why monitoring lag matters before routing reads to secondaries.

text
rs.printSecondaryReplicationInfo()
// source: secondary-1.example.net:27017
//   syncedTo: Wed Sep 10 2026 10:15:02 GMT+0000
//   0 secs (0 hrs) behind the primary
// source: secondary-2.example.net:27017
//   syncedTo: Wed Sep 10 2026 10:14:38 GMT+0000
//   24 secs (0.01 hrs) behind the primary
7
A read routed to secondary-2 right now could return data up to 24 seconds stale — a very different risk than secondary-1's near-zero lag.

Why this works: Lag is a live, changing number, not a fixed property of a secondary — checking it before assuming "reading from secondaries is fine" is what turns a read-preference decision from a guess into an informed trade-off.

Remember: Replica lag is the time delay between the primary and a specific secondary applying a change — normally tiny, but the exact number that bounds how stale a secondary-routed read can be, and worth monitoring rather than assuming.

See also: oplog replication · stale reads from secondaries

Why secondary reads can be stale

standardadvanced

A secondary applies the primary's writes with a delay — replica lag — so a read routed there can return data that does not yet include the most recent write, even one made moments earlier by the same client. This is the concrete cost behind every read-preference decision that allows secondaries.

Think of it as

This ties the two previous concepts together directly: replica lag is the mechanism, and stale reads are the visible consequence of reading during that lag window — "reading from a secondary can be stale" is not a separate risk, it is exactly what nonzero lag means from a reader's point of view.

text
// Read-your-writes needed? Use readPref("primary"), or a session for causal consistency — secondaryPreferred alone does not guarantee it

What we're doing: Show the classic stale-read symptom and the fix using a session for causal consistency.

javascript
// Bug: a write followed by an immediate read that can land on a lagging secondary
await profile.updateOne({ _id: userId }, { $set: { bio: newBio } })
const p = await profile.findOne({ _id: userId }).readPref("secondaryPreferred")
// p.bio can still be the OLD value if this read reached a secondary that has
// not yet applied the update above

// Fix: share a session so the driver enforces causal consistency between them
const session = client.startSession()
await profile.updateOne({ _id: userId }, { $set: { bio: newBio } }, { session })
const fixed = await profile.findOne({ _id: userId }, { session }) // guaranteed to see the write
3
Nothing here is wrong syntactically — this genuinely can return stale data, because secondaryPreferred allows the read to land on a member that has not yet caught up.
10
Sharing a session between the write and the read is what lets the driver guarantee the read reflects the write, regardless of which member actually serves it.

Why this works: The bug is invisible until a secondary happens to be lagging at the exact moment a read-after-write occurs — the fix (a shared session, or reading from the primary) addresses the actual mechanism rather than hoping lag stays low enough to never matter.

Remember: A secondary read can return data that does not yet include a recent write, because it reflects wherever that secondary's replication currently is — for read-your-own-writes, use readPref("primary") or a shared session for causal consistency, not secondaryPreferred alone.

See also: replica lag · read preference modes · logical sessions

Advertisement