MongoDBDatabases

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.

TT
Sarah Mitchell
5 min read
MongoDB Replication & High Availability: Replica Sets Explained

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
text
                    ┌─────────────┐
  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)

yaml
# 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:
bash
docker compose up -d

# Connect to the first node and initiate the replica set
mongosh --port 27017
javascript
rs.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).

javascript
// 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:

  1. A secondary detects the primary is gone (after electionTimeoutMillis, default 10 seconds)
  2. It becomes a candidate and requests votes from other members
  3. A candidate needs a majority of votes (e.g. 2 of 3 members) to become primary
  4. The new primary begins accepting writes — the connection string with replicaSet=rs0 automatically reconnects clients
javascript
// 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:

javascript
// 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:

ModeReads Go ToUse Case
primary (default)Primary onlyStrong consistency required
primaryPreferredPrimary; secondary if primary unavailableNear-primary with fallback
secondarySecondaries onlyAnalytics, reporting, reads that tolerate slight lag
secondaryPreferredSecondaries; primary if no secondary availableOffload reads
nearestLowest network latency memberGeographically distributed reads
javascript
// 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 primary or primaryPreferred.


Monitoring Replication

javascript
// 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

StateMeaning
PRIMARYAccepts writes
SECONDARYReplicating; can serve reads
RECOVERINGCatching up from oplog or initial sync in progress
ARBITERVoting member with no data
DOWNUnreachable
ROLLBACKRolling back writes that did not reach a majority

Adding and Removing Members

javascript
// 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)