Skip to main content
MongoDB · lesson 8 of 10
MongoDB advanced Lesson 8 of 10

Also part of: System Design Databases

Killing the Primary and Measuring the Real Failover Time

docker kill the primary node of a real 3-node replica set and time exactly how long the cluster takes to elect a new one from container logs and MongoDB's own timestamps — 9.01 seconds, closely matching the documented default election timeout.

By SkillByExample · Published

mongodb replication failover high-availability replica-set

Not an assertion that failover works — a real 3-node cluster, a real docker kill, and a real stopwatch.

Confirming the starting state

docker exec mongo-rs1 mongosh --quiet --eval 'rs.status().members.map(m => ({name: m.name, state: m.stateStr}))'
[
  { name: 'mongo-rs1:27017', state: 'PRIMARY' },
  { name: 'mongo-rs2:27017', state: 'SECONDARY' },
  { name: 'mongo-rs3:27017', state: 'SECONDARY' }
]

mongo-rs1 is the current primary, from the replica set built in the change-streams lesson.

Killing the primary, precisely

docker inspect mongo-rs2 --format "{{.State.FinishedAt}}"
2026-09-13T16:01:25.992145189Z

docker kill mongo-rs2 (run just before this) sends SIGKILL immediately — no graceful shutdown window to blur the timing, unlike docker stop. Docker’s own recorded stop timestamp is the real starting point for the measurement.

docker exec mongo-rs1 mongosh --quiet --eval '
const status = rs.status();
const primary = status.members.find(m => m.stateStr === "PRIMARY");
printjson({ name: primary.name, electionDate: primary.electionDate });
'
{ name: 'mongo-rs1:27017', electionDate: ISODate('2026-09-13T16:01:35.000Z') }

16:01:35.000 minus 16:01:25.992 is 9.01 seconds — real, measured, from MongoDB’s own recorded election timestamp against Docker’s own recorded kill timestamp. That’s close to MongoDB’s documented default electionTimeoutMillis of 10 seconds: how long a replica set waits without contact from the primary before starting an election. This isn’t a coincidence to assert — it’s a number that lines up with documentation because both are measuring the same real timeout.

The bug: writes fail during the gap

docker exec mongo-rs3 mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
try {
  db.accounts.insertOne({ name: "eve", balance: 5 });
} catch (e) {
  print("ERROR: " + e.message);
}
'
ERROR: not primary

Run against a secondary during the roughly 9-second gap where no primary exists yet, a write fails outright. This is the real cost of automatic failover: not zero downtime, but a bounded, short one — bounded by electionTimeoutMillis, not by how long it takes a human to notice and intervene.

After the election: the old primary rejoins as a secondary

docker exec mongo-rs3 mongosh --quiet --eval 'rs.status().members.map(m => ({name: m.name, state: m.stateStr}))'
[
  { name: 'mongo-rs1:27017', state: 'SECONDARY' },
  { name: 'mongo-rs2:27017', state: 'PRIMARY' },
  { name: 'mongo-rs3:27017', state: 'SECONDARY' }
]

mongo-rs2 won the election and stays primary — restarting mongo-rs1 doesn’t hand primary status back. Nothing about the original primary is special once someone else has taken over.

Practice

1. Confirm a write survives correctly once a new primary is in place.
docker exec mongo-rs2 mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
printjson(db.accounts.insertOne({ name: "eve", balance: 5 }));
'
{ acknowledged: true, insertedId: ObjectId('...') }

Writing directly to the new primary (mongo-rs2, confirmed primary in the previous step) succeeds normally — the “not primary” error was specific to the roughly 9-second gap, not a lasting problem with the cluster.

2. Check which node the current primary considers to be lagging, if any.
docker exec mongo-rs2 mongosh --quiet --eval '
rs.status().members.map(m => ({ name: m.name, state: m.stateStr, lastHeartbeat: m.lastHeartbeat }));
'
[
  { name: 'mongo-rs1:27017', state: 'SECONDARY', lastHeartbeat: ISODate('2026-09-13T16:04:09.272Z') },
  { name: 'mongo-rs2:27017', state: 'PRIMARY', lastHeartbeat: undefined },
  { name: 'mongo-rs3:27017', state: 'SECONDARY', lastHeartbeat: ISODate('2026-09-13T16:04:09.855Z') }
]

Note which entry has lastHeartbeat: undefined — it isn’t the node that just recovered from being killed (mongo-rs1), it’s mongo-rs2, the node this query was actually run against. A member never heartbeats itself; it only tracks heartbeats received from the others. This is the field that would reveal a lagging or unreachable secondary in a cluster with a real ongoing problem, but reading it requires knowing which entry is “self” before drawing any conclusion from a missing value.

3. Read rs.conf() to see the actual configured election timeout, rather than assuming the documented default applies.
docker exec mongo-rs2 mongosh --quiet --eval 'rs.conf().settings.electionTimeoutMillis'
10000

10000 milliseconds confirms this cluster is running the documented default of 10 seconds — the measured 9.01-second failover lines up with it directly, not with an assumption about what the setting “probably” is.

Next: sharding — partitioning data across multiple shards by key.

Frequently Asked Questions

Why measure failover with docker kill instead of docker stop?
docker stop sends SIGTERM and waits for a graceful shutdown, which lets mongod close connections cleanly before it actually goes offline — the moment other nodes detect it as unreachable doesn't line up cleanly with the container's recorded stop time. docker kill sends SIGKILL immediately, which is a closer simulation of a real hardware failure or OOM kill, and gives a clean, correlatable timestamp to measure from.
Why did it take about 9 seconds, not instantly?
Replica set members don't declare the primary dead the instant a heartbeat is missed — electionTimeoutMillis (10 seconds by default) is how long they wait without contact before starting an election. The measured 9.01 seconds here is real, not asserted, and it lines up closely with that documented default, which is exactly the kind of claim worth confirming rather than trusting from memory.
What actually happens to writes during those 9 seconds?
They fail outright with a 'not primary' error — this lesson's example shows exactly that, attempting an insert against a secondary while no primary exists yet. There is a real window where the cluster accepts no writes at all, which is why electionTimeoutMillis is a genuine production tuning knob: lower it for faster failover at the cost of more elections triggered by transient network blips.
Does the old primary automatically become primary again once it rejoins?
No — once a new primary is elected and the cluster is stable, a rejoining former primary comes back as a secondary and stays that way unless a new election happens to choose it again. There's no automatic 'give it back' behavior; the replica set doesn't treat the original primary as special once someone else has taken over.
Is a 3-node replica set actually representative of a production deployment?
Yes, for a lot of real deployments — 3 nodes is MongoDB's own minimum recommendation for automatic failover (an even number, or fewer than 3, either can't reach a majority to elect a new primary or has no redundancy at all). Production clusters often add more nodes for read scaling or geographic distribution, but the failover mechanism measured here is the same one running underneath.