The Error
You run an insert, update, or delete against your MongoDB cluster and hit this:
MongoServerError: not primary
Writes are suddenly rejected. Nine times out of ten, your app is talking to a secondary node instead of the primary. The other case: the primary just stepped down and election hasn't finished yet.
Why This Happens
In a replica set, only the primary accepts writes. Secondaries are read-only โ full stop. The not primary error surfaces in a few specific situations:
- Your connection string points directly to a secondary's IP/port instead of using the replica set URI.
- A failover just happened โ the old primary stepped down, and no new one has been elected yet.
- Read preference is set to
secondaryorsecondaryPreferred, but write operations go through the same connection. - A hidden or delayed secondary was temporarily promoted during maintenance.
- DNS or a load balancer is routing traffic to the wrong node.
Step 1 โ Check the Replica Set Status
Connect to any node and run:
rs.status()
Scan the members array for the stateStr field on each node:
{
"members" : [
{ "name" : "mongo1:27017", "stateStr" : "PRIMARY" },
{ "name" : "mongo2:27017", "stateStr" : "SECONDARY" },
{ "name" : "mongo3:27017", "stateStr" : "SECONDARY" }
]
}
All nodes showing SECONDARY? Or one stuck in RECOVERING? The replica set has no primary right now. That's normal during an election โ wait 10โ30 seconds and check again.
To confirm which node you're actually connected to:
db.isMaster()
// or the newer equivalent:
db.hello()
If ismaster (or isWritablePrimary) comes back false, you're on a secondary. That's your problem.
Fix 1 โ Use a Replica Set Connection String
The most common culprit. Connecting to a single node gives the driver zero awareness of the replica set topology:
# Wrong โ single node, no replica set awareness
mongodb://mongo1:27017/mydb
Switch to the full URI listing all members with the replicaSet parameter:
# Correct โ driver discovers the primary automatically
mongodb://mongo1:27017,mongo2:27017,mongo3:27017/mydb?replicaSet=rs0
Now the driver handles failover on its own. Primary changes? It re-discovers the new one. You don't touch anything.
On Atlas, use the SRV format โ replica set info is already baked in:
mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/mydb
Fix 2 โ Node.js / Mongoose Connection Update
// Before (broken)
const uri = 'mongodb://192.168.1.10:27017/mydb';
// After (replica set aware)
const uri = 'mongodb://192.168.1.10:27017,192.168.1.11:27017,192.168.1.12:27017/mydb?replicaSet=rs0';
await mongoose.connect(uri, {
serverSelectionTimeoutMS: 5000,
heartbeatFrequencyMS: 10000,
});
Fix 3 โ Python / PyMongo Connection Update
from pymongo import MongoClient
# Before (broken)
client = MongoClient('mongodb://192.168.1.10:27017/')
# After (replica set aware)
client = MongoClient(
'mongodb://192.168.1.10:27017,192.168.1.11:27017,192.168.1.12:27017/',
replicaSet='rs0',
serverSelectionTimeoutMS=5000
)
# Verify you're on the primary
print(client.primary) # Should print ('192.168.1.10', 27017)
Fix 4 โ During Failover (Temporary State)
Sometimes the cluster just lost its primary โ crash, network partition, planned maintenance. Election takes time. Watch it live:
# From mongosh, run this in a loop
while true; do
mongosh --quiet --eval "rs.status().members.forEach(m => print(m.name, m.stateStr))"
sleep 3
done
Under normal conditions, election wraps up in under 10 seconds. If it's dragging on, check these three things:
- Do you have at least 3 nodes? You need a majority vote to elect a primary.
- Is network connectivity between all nodes healthy?
- Any nodes stuck in
RECOVERINGstate?
Need to manually kick off a new election? Connect to the current primary and run:
rs.stepDown() // Steps down current primary, triggers new election
Fix 5 โ Check writeConcern Settings
Easy one to miss: a readPreference of secondary on a write operation makes no sense โ and can cause this error. Double-check your write calls:
// Wrong โ secondary read preference on a write operation
db.collection('orders').insertOne(
{ item: 'widget' },
{ readPreference: 'secondary' } // This doesn't apply to writes
);
// Correct โ write concern separate from read preference
db.collection('orders').insertOne(
{ item: 'widget' },
{ writeConcern: { w: 'majority', wtimeout: 5000 } }
);
Verify the Fix
After updating your connection string, run a quick smoke test from mongosh:
use mydb
db.test.insertOne({ ping: new Date() })
// Should return: { acknowledged: true, insertedId: ... }
The MongoServerError: not primary errors should stop appearing. To confirm the driver landed on the primary:
# Node.js โ log the topology
mongoose.connection.on('connected', () => {
console.log('Connected to:', mongoose.connection.host);
});
Prevention
- Always use replica set connection strings in production. Hardcoding a single node IP is a ticking clock โ failover will happen eventually, usually at 3 AM.
- Set aggressive timeouts. Configure
serverSelectionTimeoutMSandconnectTimeoutMSso your app fails fast and retries instead of hanging during elections. - Enable retryable writes. Add
retryWrites=trueto your connection string (the default in most modern drivers). Transient primary changes become invisible to your application. - Monitor replica set health proactively. Alert on members entering
RECOVERINGorUNKNOWNstate โ don't wait for users to report write errors. - Split read and write clients. Reading from secondaries for load distribution? Use a dedicated client with
readPreference: 'secondaryPreferred'. Keep your write client targeting primary only.
# Full production-ready connection string
mongodb://mongo1:27017,mongo2:27017,mongo3:27017/mydb
?replicaSet=rs0
&retryWrites=true
&w=majority
&readPreference=primaryPreferred
&serverSelectionTimeoutMS=5000

