Primary, Secondary, and Elections
A MongoDB replica set is a group of mongod instances that all hold the same data and automatically coordinate who is in charge. At any given moment exactly one member is the primary, which accepts every write, while the other members are secondaries, continuously copying the primary’s changes so they can take over if it disappears. When the primary becomes unreachable, the surviving members hold an election and pick a new primary, usually within a few seconds, so your application keeps working without a human flipping a switch. Understanding exactly how that election works — and how to configure a replica set so elections are safe rather than a source of split-brain bugs — is the difference between a replica set that protects you and one that quietly loses writes.
Overview: How Replica Sets Work
A replica set is a set of mongod processes, each started with the same --replSet name, that together maintain one logical copy of your data. The set has a configuration document (created with rs.initiate() and changed with rs.reconfig()) listing every member, its host, and settings like priority and votes. Internally, MongoDB assigns each member one of a small number of roles:
- Primary — the only member that accepts writes. Reads default to the primary too, unless you explicitly set a different read preference.
- Secondary — replicates the primary’s data and can serve reads if the client opts in with a read preference such as
secondaryorsecondaryPreferred. - Arbiter — votes in elections but holds no data and can never become primary. Arbiters exist purely to help a set reach a voting majority cheaply.
- Hidden / delayed / priority-0 members — secondaries with special settings: hidden members don’t appear to client read-preference routing, delayed members lag behind on purpose (useful for recovering from an accidental mass delete), and priority-0 members can never be elected primary even though they hold data.
Replication itself is driven by the oplog (operations log), a special capped collection at local.oplog.rs on every member. Every write on the primary is recorded there as an idempotent, replayable operation. Secondaries continuously tail the primary’s oplog — essentially running a long-lived query against it — and apply each entry to their own data in the same order. This is why replication is asynchronous by default: a secondary might be a few milliseconds (or, under load, much longer) behind the primary. That gap is called replication lag, and it’s exactly why reading from secondaries can return stale data.
All members exchange heartbeats with each other roughly every two seconds. Each member uses these heartbeats to track whether the others are reachable and how caught-up their oplogs are. When a majority of voting members can no longer reach the primary, or the primary itself decides it can no longer reach a majority, an election starts.
Elections use a Raft-derived consensus protocol
MongoDB’s election protocol is based on Raft. Time is divided into monotonically increasing terms. When an eligible secondary decides an election is needed, it increments the term, votes for itself, and requests votes from every other voting member. A member grants its vote to a candidate only if all of the following hold: it hasn’t already voted in this term, the candidate has priority greater than 0, and the candidate’s oplog is at least as up to date as its own (MongoDB will never elect a member whose data is behind, to avoid losing acknowledged writes). A candidate becomes primary as soon as it collects votes from a majority of all voting members — not just a majority of the members that happen to be reachable. This majority requirement is the whole reason replica-set sizing matters: it’s what prevents two members from both believing they’re primary at once (split brain).
Syntax
You don’t “call an election” directly; instead you configure the replica set and MongoDB triggers elections automatically. The commands you’ll use to set up, inspect, and influence this behavior:
| Command | Purpose |
|---|---|
rs.initiate(config) |
Creates a brand-new replica set from a configuration document (run once, on the first member). |
rs.status() |
Shows the live state of every member: role (PRIMARY/SECONDARY/ARBITER), health, and how current its oplog is. |
rs.conf() |
Returns the current replica set configuration document (members, priorities, votes). |
rs.reconfig(config) |
Applies a modified configuration document (e.g. to change a member’s priority or votes). |
rs.stepDown(secs) |
Forces the current primary to step down for at least secs seconds, triggering a new election — useful before planned maintenance. |
db.hello() |
The modern replacement for the deprecated rs.isMaster(); returns whether the member you’re connected to is currently primary or secondary. |
Key per-member configuration fields you’ll set inside members[] when calling rs.initiate() or rs.reconfig():
priority(default1) — higher-priority members are preferred as primary;priority: 0means “can never become primary.”votes(default1) — whether this member counts toward the election majority; a member can only havevotes: 0if it also haspriority: 0.hidden(defaultfalse) — hides the member from driver read-preference routing.arbiterOnly(defaultfalse) — marks the member as a vote-only arbiter with no data.secondaryDelaySecs— keeps this secondary’s data intentionally behind by N seconds.
Examples
Example 1: Creating a three-node replica set
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongo1.example.net:27017" },
{ _id: 1, host: "mongo2.example.net:27017" },
{ _id: 2, host: "mongo3.example.net:27017" }
]
});
Output:
{
ok: 1
}
This is run once, connected to the first node. MongoDB writes the configuration document, and within a few seconds the three members elect one of themselves — here, typically mongo1.example.net, since it has the lowest _id and equal priority — as the initial primary.
Example 2: Checking who’s primary right now
rs.status();
Output:
{
set: "rs0",
members: [
{ _id: 0, name: "mongo1.example.net:27017", stateStr: "PRIMARY", health: 1 },
{ _id: 1, name: "mongo2.example.net:27017", stateStr: "SECONDARY", health: 1 },
{ _id: 2, name: "mongo3.example.net:27017", stateStr: "SECONDARY", health: 1 }
],
ok: 1
}
The (trimmed) members array is the field to watch: stateStr tells you each member’s current role, and health: 1 means the member is reachable via heartbeats. A quicker one-liner from the current connection is db.hello(), which returns { isWritablePrimary: true, ... } when you’re talking to the primary.
Example 3: Forcing a controlled failover for maintenance
// Run on the current primary before patching/rebooting it
rs.stepDown(60);
Output:
{
ok: 1
}
This tells the current primary to stop accepting writes and refuse to be re-elected for 60 seconds, then immediately triggers a new election among the remaining members. Re-running rs.status() a moment later shows a different member now marked PRIMARY. This is the standard way to safely take a primary down for planned maintenance instead of just killing the process and waiting for a timeout-driven election.
How Elections Work Step by Step
- Every member sends heartbeats to every other member roughly every two seconds.
- If a secondary stops receiving heartbeat responses from the primary for longer than
electionTimeoutMillis(10 seconds by default), it assumes the primary is down and calls for an election. The primary can also trigger its own step-down if it notices it can no longer reach a majority of the set. - The calling member increments the replica set’s term, marks itself a candidate, votes for itself, and sends a vote request to every other voting member, including its own oplog position.
- Each member grants its vote to at most one candidate per term, and only if the candidate’s
priorityis above 0 and its oplog is at least as up to date as the voter’s own. - Once a candidate collects votes from a strict majority of all voting members (not just the reachable ones), it becomes primary, and every other member updates its local view of the set accordingly.
- If no candidate reaches a majority (a split vote, or a network partition that leaves no side with a majority), the election times out and a new one is triggered — the set simply has no primary until a majority can agree.
Note the last point carefully: while an election is in progress, the replica set has no primary at all, so writes fail during that window. This is usually a couple of seconds, but application code still needs to handle it gracefully rather than assuming failover is instantaneous.
Common Mistakes
Mistake 1: an even number of voting data-bearing members, no arbiter
// Four data-bearing members, all with the default votes: 1 — dangerous
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongo1.example.net:27017" },
{ _id: 1, host: "mongo2.example.net:27017" },
{ _id: 2, host: "mongo3.example.net:27017" },
{ _id: 3, host: "mongo4.example.net:27017" }
]
});
With four voting members, a network partition can split the set 2-2, and neither side can reach the 3-vote majority needed to elect a primary — the whole set becomes read-only until the partition heals. Always keep the number of voting members odd, either by using an odd member count or by adding a vote-only arbiter:
cfg = rs.conf();
cfg.members.push({ _id: 4, host: "arbiter.example.net:27017", arbiterOnly: true });
rs.reconfig(cfg);
Mistake 2: assuming a hidden or priority-0 member can rescue you
Teams sometimes add a priority: 0 analytics replica in a different region and assume it will automatically take over if the main region goes down. It won’t — a member with priority: 0 will never be elected primary, by design, no matter how caught up its data is. If a region needs to be able to take over, its members need priority greater than 0 (and enough votes to actually win an election).
Mistake 3: treating failover as instant and reads from secondaries as fresh
// Assumes this always returns the very latest data — it doesn't
db.orders.find({ status: "pending" }).readPref("secondary");
Reading with readPref("secondary") can return data that’s seconds (or, under replication lag, much longer) behind the primary, because secondaries apply the oplog asynchronously. If your read absolutely needs the latest write, read from the primary (the default) or use readPref("primaryPreferred"). Likewise, don’t assume an election is invisible to your application — writes issued during the no-primary window will fail, so use a modern driver with retryWrites=true (the default in current drivers) so a failed write is automatically retried against the newly elected primary instead of surfacing an error to the user.
Best Practices
- Keep the number of voting members odd (3, 5, or 3 data-bearing plus 1 arbiter) so a majority can always be determined.
- Prefer real data-bearing members over arbiters when you can afford them — arbiters add a vote but no redundant copy of your data.
- Set
priority: 0on members that should never become primary, such as a reporting or backup replica, so they don’t win an election at a bad moment. - Spread members across availability zones or data centers so a single zone outage can’t take out your voting majority.
- Use
w: "majority"write concern for important writes so they’re guaranteed to survive a failover instead of being rolled back if the old primary rejoins. - Leave
electionTimeoutMillisat its default unless you have a specific, measured reason to change it — too low a value causes spurious elections on brief network blips. - Monitor
rs.status()regularly (or via your monitoring tool) for memberhealthand oplog lag, not just whether a primary currently exists. - Make sure client applications use a modern driver with retryable writes/reads enabled so short primary-less windows during elections don’t surface as user-facing errors.
Practice Exercises
- Set up a local 3-node replica set (or reason through it on paper), run
rs.status()to find the primary, then runrs.stepDown(60)on it and runrs.status()again. Confirm a different member is nowPRIMARYand note roughly how long the transition took. - Given a 3-member replica set returned by
rs.conf(), write thers.reconfig()call needed to turn member index2into a hidden,priority: 0backup replica that still votes in elections. - Explain in your own words: with exactly 2 data-bearing voting members and no arbiter, why can neither member become primary if the network partitions between them? What’s the minimum change to the deployment that fixes this?
Summary
- A replica set has exactly one primary, which accepts all writes; secondaries replicate the primary’s changes by tailing its oplog.
- Elections use a Raft-derived consensus protocol: a candidate needs votes from a strict majority of all voting members to become primary, and voters won’t back a candidate whose oplog is behind their own.
- Keep the number of voting members odd (via member count or an arbiter) so the set can always determine a majority and avoid split votes.
prioritycontrols who is preferred as primary (priority: 0means never);votesandarbiterOnlycontrol who participates in elections.- Failover isn’t instant — there’s a brief window with no primary, so use
w: "majority"and a driver with retryable writes to ride through it safely. rs.status(),rs.conf(), anddb.hello()are your main tools for inspecting replica set and election state.
