MongoDB Course — 10 · Replica Sets — Primary, Secondary, Oplog, Elections
A single mongod is a single point of failure. A replica set is a group of mongod processes that maintain copies of the same data and automatically elect a new primary when one fails. Three or more members get you durability, automatic failover, optional read scaling on secondaries, and zero-downtime maintenance. This post covers the architecture, the oplog, election protocol, write-concern interaction with replication, secondary read trade-offs, and the canonical 3-member + arbiter topologies.
Series map: 00 Foundations → … → 09 Transactions → 10 Replica Sets (this post) → 11 Sharding → 12 Change Streams → … → 17 Drivers & Case Studies.
Learning Outcome
- Explain PRIMARY / SECONDARY / ARBITER roles.
- Initiate a replica set and inspect
rs.status(). - Read the oplog and understand replication lag.
- Force an election + observe automatic failover.
- Configure priorities, hidden members, delayed members.
- Pick the right topology for your durability + cost target.
Chapter 1 — Why replication
Three things at once:
- Durability — N copies of the data; loss of one server doesn't lose data.
- High availability — automatic failover; clients reconnect to a new primary in seconds.
- Read scaling (optional) — secondaries serve eventual-consistent reads.
Replication doesn't scale writes — every write still goes through one primary. For write scaling, you shard (Post 11) on top of replica sets.
Chapter 2 — Member roles
| Role | Receives writes | Serves reads | Votes in election | Holds data |
|---|---|---|---|---|
| PRIMARY | yes (only one at a time) | yes | yes | yes |
| SECONDARY | replicates from primary's oplog | yes (with secondaryPreferred etc.) |
yes | yes |
| ARBITER | no | no | yes | no — voting only |
| HIDDEN | no | no (invisible to drivers) | yes | yes (backups, analytics) |
| DELAYED | no | no | yes | yes (lags primary by N seconds — disaster recovery) |
A typical minimal production set: 3 data-bearing members (1 primary + 2 secondaries). Loss of any one keeps a majority alive.
Avoid:
- Even number of voting members without an arbiter → election can split. Always odd.
- Arbiter-only failover safety in 2026 → arbiters can't vote on majority writes; prefer a 3-data-member setup unless cost-constrained. (See "election + write concern" pitfall in Chapter 8.)
Chapter 3 — Initiate a replica set (local)
1
2
3
4
5
6
# start three mongod instances on different ports
mkdir -p data/rs0-{0,1,2}
mongod --replSet rs0 --port 27017 --dbpath data/rs0-0 --bind_ip 127.0.0.1 --fork --logpath data/rs0-0/log
mongod --replSet rs0 --port 27018 --dbpath data/rs0-1 --bind_ip 127.0.0.1 --fork --logpath data/rs0-1/log
mongod --replSet rs0 --port 27019 --dbpath data/rs0-2 --bind_ip 127.0.0.1 --fork --logpath data/rs0-2/log
Connect to one + initiate:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mongosh --port 27017
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "127.0.0.1:27017" },
{ _id: 1, host: "127.0.0.1:27018" },
{ _id: 2, host: "127.0.0.1:27019" }
]
})
rs.status()
rs.conf()
Within a second or two, one member is PRIMARY, the other two are SECONDARY.
In Atlas, every cluster is already a replica set — you don't initiate manually.
Chapter 4 — Connection string for a replica set
1
2
3
mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=rs0
mongodb+srv://cluster.abcd.mongodb.net/?retryWrites=true # SRV form, Atlas
The driver:
- Reads the seed list (your hosts).
- Polls each to discover the full topology and the current primary.
- Re-discovers automatically on failover.
You only need to list a few seed hosts — the driver finds the rest. The SRV form (Atlas) returns the full member list via DNS, so the URI stays short.
Chapter 5 — The oplog
The oplog is a capped collection (local.oplog.rs) on every replica-set member that records every write applied. Secondaries tail it and replay it to stay in sync.
1
2
3
use local
db.oplog.rs.find().sort({ ts: -1 }).limit(5)
Each entry: timestamp, operation type (insert/update/delete), namespace, document or update spec.
Oplog size matters
Default size: 5 % of free disk (with a 50 GB upper cap on most platforms). If a secondary falls behind by more oplog entries than fit, it goes STALE and must do a full resync — slow.
Tune via --oplogSizeMB at start, or replSetResizeOplog admin command. Atlas auto-sizes.
Trick: if you regularly run multi-hour ingest jobs while secondaries catch up, increase oplog size. The "1 day of writes fits" rule of thumb prevents most stale-member surprises.
Chapter 6 — Replication mechanics + lag
- Client writes to PRIMARY.
- PRIMARY records the write to its own oplog and applies it.
- Each SECONDARY tails the primary's oplog and applies in order.
- Once a majority of voters have applied the write, it's "majority committed".
Replication lag = how far behind the primary a secondary is in oplog time.
Inspect:
1
2
3
rs.printSecondaryReplicationInfo()
// returns each secondary's lag in seconds
Causes of lag:
- Big write spike (secondaries can't keep up).
- Slow disk on a secondary.
- Network latency (especially across regions).
- Long oplog entries (huge updates).
Persistent lag > a few seconds = warning. Lag growing unbounded = crisis (eventually goes STALE).
Chapter 7 — Elections
If the PRIMARY becomes unreachable, voting members hold an election (under the hood: Raft-like protocol Version 1, "PV1").
Steps:
- A secondary that hasn't heard from the primary for
electionTimeoutMillis(default 10 s) calls an election. - Other voting members vote yes if the candidate's oplog is up-to-date enough.
- Majority of votes → new PRIMARY.
- Drivers detect the change via heartbeats; subsequent writes go to the new primary.
Typical failover: 10-20 seconds for clients to reconnect; retryable writes (Post 08) hide most of the disruption.
Force a manual failover (test)
1
2
rs.stepDown(60) // current primary steps down for 60 s
A useful production drill — practise failover under controlled conditions.
Chapter 8 — Write concern + replica sets
Recap from Post 08 — write concern interacts with replication:
w |
Meaning |
|---|---|
| 0 | fire-and-forget |
| 1 | primary acked |
"majority" |
majority of voters acked |
| N | exactly N members acked |
"majority" is the production-correct default — guarantees the write survives the primary failing (because at least one surviving voter has it).
Arbiter pitfall
If you have a 2-data + 1-arbiter set (total 3 voters, majority = 2), arbiters can vote in elections but don't store data. A write reaching only the primary + the arbiter (waiting for the second data member) can be lost in failover. Modern guidance: avoid arbiters in 2026 for any system where data loss isn't acceptable. Use 3 data-bearing members.
Chapter 9 — Read preference + secondary reads
Recap from Post 08:
| Preference | Effect |
|---|---|
primary (default) |
always primary, freshest |
primaryPreferred |
primary if up, else secondary |
secondary |
secondary only |
secondaryPreferred |
secondary first, fall back to primary |
nearest |
lowest-latency member |
Use secondary reads for:
- Reporting / dashboards tolerating seconds of lag.
- Scaling read-heavy workloads horizontally.
- Region-local reads via
tagSets.
Don't use for:
- Read-your-own-writes (RYOW) without causal-consistency session.
- Anything where stale-by-seconds matters (orders, balances, leaderboards).
Chapter 10 — Priorities, hidden, delayed members
Each member has a configuration in rs.conf():
1
2
3
4
5
6
const cfg = rs.conf();
cfg.members[1].priority = 0; // never become primary
cfg.members[1].hidden = true; // invisible to drivers (analytics replica)
cfg.members[1].slaveDelay = 3600; // older Mongo; modern uses secondaryDelaySecs
rs.reconfig(cfg);
Common variations:
- Analytics replica — hidden, priority 0 → drivers never route reads here; you connect directly for heavy analytics queries.
- Backup target — hidden, priority 0 → take snapshots without affecting production reads.
- Delayed member —
secondaryDelaySecs: 3600→ lags primary by 1 hour. Disaster recovery against fat-finger drops ("oops, restore from the delayed member's state of an hour ago").
Topology recipe — 5-member production
1
2
3
4
5
6
m1: data, primary candidate (priority 1)
m2: data, primary candidate (priority 1) — different AZ
m3: data, primary candidate (priority 1) — third AZ
m4: data, hidden, priority 0 — analytics replica
m5: data, hidden, priority 0, secondaryDelaySecs: 3600 — DR replica
5 voters (majority 3). Tolerates 2 simultaneous failures of voting members.
Chapter 11 — Sizing + topology choices
| Topology | Voters | Majority | Tolerated failures | When |
|---|---|---|---|---|
| 3 data | 3 | 2 | 1 | minimum production |
| 2 data + 1 arbiter | 3 | 2 | 1 | cheap; avoid in 2026 |
| 5 data | 5 | 3 | 2 | larger production, mixed roles |
| 7 data | 7 | 4 | 3 | very high availability; rare |
| 3 data, multi-region | 3 | 2 | 1 (region) | DR; read latency increases; needs tagSets |
Voters max is 7 in a replica set (additional members can be non-voting).
Multi-region replica set: one primary serves global writes, secondaries in other regions serve local reads. Trade-off: writes to a "far" primary have high latency. Sharding by region (Post 11) often beats multi-region replica sets for writes.
Chapter 12 — Operational toolkit
1
2
3
4
5
6
7
8
9
rs.status() // overall set health, who's primary
rs.conf() // current configuration
rs.reconfig(newCfg, { force: true }) // change configuration
rs.printSecondaryReplicationInfo() // lag per secondary
rs.printReplicationInfo() // oplog stats
rs.stepDown(seconds, electionTimeoutMs) // force this primary down
rs.freeze(seconds) // prevent this member from being elected
rs.syncFrom(host:port) // force secondary to sync from a specific source
For Atlas, the UI surfaces the equivalents; you rarely run these by hand.
Atlas dashboards to watch
- Replication Lag chart.
- Oplog Window (how many hours of oplog you have).
- Connections / IOPS / CPU per member.
- "Failover" event log.
Alert on: lag > 60 s, oplog window < 4 h, any voting member down.
Chapter 13 — Rolling maintenance pattern
To upgrade or restart members without downtime:
- Pick a secondary. Drain its connections.
- Stop + upgrade + restart it. Wait for SECONDARY state.
- Repeat for the next secondary.
- Step down the primary (
rs.stepDown()); it becomes secondary; another member becomes primary. - Stop + upgrade + restart the old primary. Wait for SECONDARY.
Zero downtime as long as a majority is always up. This is the "rolling upgrade" pattern for any major version bump or OS patch.
Atlas does this automatically when you click "Upgrade".
Chapter 14 — Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Failover takes 30+ seconds | electionTimeoutMillis too high; or driver serverSelectionTimeoutMS too high |
lower both modestly; test |
| Secondaries falling behind | undersized oplog or slow disk on secondary | enlarge oplog; faster disk |
| Stale secondary | lag exceeded oplog window | full resync; or enlarge oplog |
| Data lost after failover | used w: 1 instead of w: "majority" |
switch to majority |
| Even number of voters | possible split-brain | add an arbiter or another data member |
| Arbiter loses data (2+1 set) | known antipattern | use 3 data members |
| Driver doesn't see new primary | replicaSet= missing or wrong; DNS slow |
check connection string; SRV form preferred |
| Cross-region writes slow | primary is far from app | route writes to local primary or shard by region |
| Secondary read returns stale doc | replication lag | use causal consistency session or primary |
Practice Exercises
Exercise 1 — Three-member local set
Initiate the 3-node set from Chapter 3. Insert data. Confirm replication: read on secondary (allow db.getMongo().setReadPref('secondaryPreferred')).
Exercise 2 — Forced failover
Run rs.stepDown(30). Watch a Node script that does inserts every second — note the brief failure + resume (retryable writes save the day).
Exercise 3 — Oplog inspection
use local; db.oplog.rs.find().sort({ ts: -1 }).limit(10). Identify your last few writes.
Exercise 4 — Replication lag
In one window, run a tight insert loop. In another, stop one secondary (mongod SIGSTOP / pause). Restart it after a minute. Watch rs.printSecondaryReplicationInfo() show the catch-up.
Exercise 5 — Hidden analytics replica
Set member 2 to priority: 0, hidden: true. Confirm drivers no longer route reads there. Connect directly to that member for heavy aggregations.
Exercise 6 — Delayed member
Configure member 2 with secondaryDelaySecs: 60. Insert a doc. Immediately read from member 2 (direct connection) — should not see it. Wait 90 s, read again — should see it.
Cheat-sheet
| What | How |
|---|---|
| Initiate | rs.initiate({ _id, members }) |
| Status | rs.status() |
| Config | rs.conf() |
| Reconfig | rs.reconfig(newCfg) |
| Lag | rs.printSecondaryReplicationInfo() |
| Oplog stats | rs.printReplicationInfo() |
| Step down | rs.stepDown(60) |
| Connection string | mongodb://h1,h2,h3/?replicaSet=name (or SRV for Atlas) |
| Production defaults | 3 data members, w: majority, retryable writes/reads |
| Avoid | arbiters in serious workloads (2026) |
| Reads scale | secondaryPreferred for stale-OK; causal consistency for RYOW |
| Hidden | analytics / backup target |
| Delayed | DR against accidental drops (secondaryDelaySecs) |
| Failover time | ~10-20 s typical |
| Election protocol | PV1 (Raft-like) |
What's next
Post 11 covers sharding — mongos router, config servers, shard keys (cardinality, frequency, monotonic), ranged vs hashed sharding, chunks + balancer, zone sharding, resharding (5.0+), and query targeting vs scatter-gather.
References
Note: this post is in progress. Planned additions: chained replication trade-offs, multi-region latency tables, ELECTING_PRIMARY state transitions visualised.










