MongoDB Replication & High Availability: Replica Sets Explained
Set up a MongoDB replica set, understand elections and failover, configure read preferences, and monitor replication lag. Lesson 9 of the MongoDB & NoSQL Mastery course.

A standalone MongoDB instance is a single point of failure. A replica set is a group of MongoDB processes that maintain the same dataset — providing automatic failover, data redundancy, and the ability to serve reads from secondary members. All production MongoDB deployments should run as replica sets.
Previous: Lesson 8 — Security: Authentication, RBAC & TLS
Replica Set Architecture
A replica set consists of:
- Primary: the only member that accepts write operations
- Secondaries: replicate the primary's oplog and maintain identical copies of the data
- Arbiter (optional): participates in elections but holds no data — used to break ties in even-numbered member sets
┌─────────────┐
Writes ──────────►│ PRIMARY │──── oplog ────► SECONDARY 1
Reads (default) ──►│ (active) │──── oplog ────► SECONDARY 2
└─────────────┘
│
Heartbeat every 2s
│
┌─────┴─────┐
│ ARBITER │ (no data)
└───────────┘A replica set requires an odd number of voting members (typically 3) to ensure a majority can be reached for elections.
Setting Up a 3-Node Replica Set (Docker Compose)
# docker-compose.yml
services:
mongo1:
image: mongo:7.0
command: mongod --replSet rs0 --bind_ip_all --port 27017
ports: ["27017:27017"]
volumes: ["mongo1_data:/data/db"]
mongo2:
image: mongo:7.0
command: mongod --replSet rs0 --bind_ip_all --port 27017
ports: ["27018:27017"]
volumes: ["mongo2_data:/data/db"]
mongo3:
image: mongo:7.0
command: mongod --replSet rs0 --bind_ip_all --port 27017
ports: ["27019:27017"]
volumes: ["mongo3_data:/data/db"]
volumes:
mongo1_data:
mongo2_data:
mongo3_data:docker compose up -d
# Connect to the first node and initiate the replica set
mongosh --port 27017rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongo1:27017" },
{ _id: 1, host: "mongo2:27017" },
{ _id: 2, host: "mongo3:27017" }
]
})
// Check the replica set status
rs.status()The Oplog: How Replication Works
MongoDB replication is based on the oplog (operations log) — a special capped collection in the local database that records every write operation as an idempotent statement.
Secondaries tail the primary's oplog and replay each operation in order. If a secondary falls behind, it catches up by replaying the oplog entries it missed — as long as those entries have not been overwritten (the oplog has a configurable size).
// View the oplog
use local
db.oplog.rs.find().sort({ $natural: -1 }).limit(5)
// Check oplog size and utilisation
db.getReplicationInfo()
// { logSizeMB: 1536, usedMB: 87.3, timeDiff: 86400, ... }Elections and Automatic Failover
If the primary becomes unreachable, the remaining members hold an election:
- A secondary detects the primary is gone (after
electionTimeoutMillis, default 10 seconds) - It becomes a candidate and requests votes from other members
- A candidate needs a majority of votes (e.g. 2 of 3 members) to become primary
- The new primary begins accepting writes — the connection string with
replicaSet=rs0automatically reconnects clients
// Force a step-down of the current primary (useful for maintenance)
rs.stepDown()
// Check which member is currently primary
rs.isMaster() // or rs.hello() in MongoDB 5+
rs.status()Write Concern — Controlling Durability
Write concern defines how many members must acknowledge a write before it is considered successful:
// Default: acknowledged by primary only (w: 1)
db.orders.insertOne(doc)
// Write acknowledged by majority of members
db.orders.insertOne(doc, { writeConcern: { w: "majority", wtimeout: 5000 } })
// Write persisted to journal on primary
db.orders.insertOne(doc, { writeConcern: { w: 1, j: true } })
// Set default write concern at the replica set level
db.adminCommand({
setDefaultRWConcern: 1,
defaultWriteConcern: { w: "majority" }
})
w: "majority"is the recommended production write concern. It ensures that a write survives a single member failure before being acknowledged.
Read Preferences — Distributing Reads
By default, all reads go to the primary. Read preferences allow reads to be routed to secondaries for read-heavy workloads:
| Mode | Reads Go To | Use Case |
|---|---|---|
primary (default) | Primary only | Strong consistency required |
primaryPreferred | Primary; secondary if primary unavailable | Near-primary with fallback |
secondary | Secondaries only | Analytics, reporting, reads that tolerate slight lag |
secondaryPreferred | Secondaries; primary if no secondary available | Offload reads |
nearest | Lowest network latency member | Geographically distributed reads |
// Set read preference in the connection string
mongosh "mongodb://mongo1:27017,mongo2:27017,mongo3:27017/myapp?replicaSet=rs0&readPreference=secondaryPreferred"
// Set read preference per operation
db.products.find({ category: "electronics" }).readPref("secondary")Caution with secondary reads: secondaries may lag behind the primary. If your application requires reading your own writes immediately, use
primaryorprimaryPreferred.
Monitoring Replication
// Full replica set status including member states and lag
rs.status()
// Replication lag per secondary (in seconds)
rs.printSecondaryReplicationInfo()
// View the current primary
rs.isMaster().primary
// Check oplog coverage (how long secondaries can fall behind before needing resync)
db.getReplicationInfo()Member States
| State | Meaning |
|---|---|
PRIMARY | Accepts writes |
SECONDARY | Replicating; can serve reads |
RECOVERING | Catching up from oplog or initial sync in progress |
ARBITER | Voting member with no data |
DOWN | Unreachable |
ROLLBACK | Rolling back writes that did not reach a majority |
Adding and Removing Members
// Add a new secondary
rs.add("mongo4:27017")
// Add an arbiter
rs.addArb("mongo-arbiter:27017")
// Remove a member
rs.remove("mongo3:27017")
// Reconfigure the replica set (e.g. change priority)
const config = rs.conf()
config.members[1].priority = 0 // never become primary
config.members[1].hidden = true // invisible to drivers (for backups)
rs.reconfig(config)