Fix: MongoDB "not primary" Write Error (Replica Set)
Part of: Database Errors
Quick Answer
How to fix MongoDB 'not primary' errors when writing to a replica set — read preference misconfiguration, connecting to a secondary, replica set elections, and write concern settings.
The Error
A write operation to MongoDB fails with:
MongoServerError: not primaryOr in older MongoDB versions:
MongoError: not masterOr after a failover:
MongoServerError: not primary and secondaryOk=false
MongoNotPrimaryError: Command find requires authenticationOr reads that worked suddenly fail:
MongoServerError: not primary or secondary; cannot currently read from this replSetMember.STATE=RECOVERINGWhy This Happens
MongoDB replica sets consist of one primary and one or more secondaries. Only the primary accepts write operations. This error occurs when:
- Connecting to a secondary — the connection string points to a secondary node directly, or the driver selected a secondary for a write operation.
- Read preference set to
secondaryfor writes — some drivers allow you to setreadPreference, but writes always require the primary regardless of read preference. - Replica set election in progress — after a primary failure, the replica set holds an election (typically 10–30 seconds). During this window, no primary exists and all writes fail.
- Network partition — the primary is isolated from the majority of the replica set and steps down voluntarily to prevent split-brain writes.
- Member in
RECOVERINGstate — a node that is syncing, or that just rejoined after being down, enters theRECOVERINGstate and cannot serve reads or writes. - Using a direct connection to a specific host — connecting with
directConnection=trueto a node that is currently a secondary causes immediate write failures.
The cause beneath all of these is the same: MongoDB enforces a single-writer invariant. Writes must go to a primary so the replica set has one authoritative order of operations. When a node steps down (planned maintenance, network partition, voluntary step-down via rs.stepDown()), it instantly refuses writes. The driver’s topology view may take a few seconds to catch up — so for that brief window your application sends writes to what it thinks is the primary but is now a secondary. The server returns not primary and the driver should retry against the new primary, but only if retryWrites=true is set and the operation is retryable.
The other source of confusion is the distinction between the connection string and runtime topology. If you connect with mongodb://host1,host2,host3/db?replicaSet=rs0, the driver uses those three hosts only for initial discovery. After discovery, it reads hello from each member and switches to whatever topology the cluster reports. A host you removed from the URI months ago may still be a current member; a host in the URI may have been retired. As long as one host in the URI is reachable, the driver will discover the real topology — but the corollary is that “I connect to host1” does not mean “writes go to host1.” Writes go to whoever hello says is primary, which may be a host you have never named explicitly.
A third trap is read preference confusion. readPreference=secondary only affects read operations. Writes always go to the primary regardless. The error message not primary and secondaryOk=false is misleading because it implies you should set secondaryOk=true — but doing so just turns the error into silent stale reads, not actual fix. The right fix is to ensure the driver knows it is talking to a replica set and can find the current primary.
Version History That Changes the Failure Mode
MongoDB has reshaped its replica set semantics significantly since 2018. The exact server version determines what fails and what your retry strategy can rely on.
MongoDB 3.6 (November 2017). Introduced mongodb+srv:// connection strings and changed the default protocol to OP_MSG. Retryable writes became opt-in via retryWrites=true. Pre-3.6, your only option on transient not primary was application-level retry.
MongoDB 4.0 (June 2018) — multi-document transactions. Added ACID transactions on replica sets. not primary during a transaction aborts the whole transaction, not just the operation. Application code that wraps writes in session.withTransaction() needs to handle TransientTransactionError explicitly. The MongoDB drivers added retry helpers for this around the same time.
MongoDB 4.2 (August 2019) — distributed transactions across shards, retryable writes default true. From 4.2 onward, all official drivers default to retryWrites=true. If your driver is older, you may still need to set it explicitly. 4.2 also changed the wire protocol for write concerns — pre-4.2 drivers connecting to 4.2 servers occasionally see odd writeConcern downgrades.
MongoDB 4.4 (July 2020). Hidden indexes, refinable shard keys, mirrored reads. From a not primary perspective, the headline change is that the server now sends a topologyVersion with every hello response. Drivers that understand topologyVersion (3.x of most drivers and later) can detect topology change events in roughly half a second instead of waiting for a full heartbeat cycle.
MongoDB 5.0 (July 2021) — write concern default change. This is the version that changed w: 1 to w: "majority" as the default. If your application worked fine on 4.4 with implicit w: 1 and started timing out on 5.0, you almost certainly hit the new majority default with a degraded replica set that cannot reach a majority. 5.0 also introduced time series collections and versioned API.
MongoDB 5.0 — election timeout default 10s. The election timeout default dropped from 30 seconds (in older versions) closer to 10 seconds for typical configurations. Failovers are faster, which means your retry window is shorter and your application is more likely to hit the not primary exception in the short interval between primary step-down and new-primary election.
MongoDB 6.0 (July 2022) — Queryable Encryption (preview). Queryable Encryption requires specific replica set configurations and adds new error modes. If you enabled Queryable Encryption and started seeing not primary errors on collections that worked before, check that all replica set members are 6.0+ and have the encryption configuration applied — 6.0 will refuse writes to an encrypted collection if any member is on a mixed version.
MongoDB 7.0 (August 2023) — compound wildcard indexes, sharding enhancements. The default writeConcern stayed at majority. 7.0 added stricter primary verification during failover, which means a brief window where even reads against the old primary return not primary or secondary while topology is settling. This is the version where “retry your reads” became as important as “retry your writes.”
MongoDB 8.0 (October 2024) — query engine improvements, time series enhancements. No fundamental change to replica set semantics from 7.0. The 8.0 driver bumps added more aggressive retry on not primary for some operations that were previously not retryable. If you upgraded your driver in 2024 and your application got more resilient without any code change, this is why.
Atlas-specific behavior. Atlas always uses mongodb+srv:// and always runs at least a 3-member replica set, even on the free tier. Atlas also applies its own connection routing layer that resolves not primary faster than self-hosted clusters because the Atlas DNS records update on failover. If you self-host and your “not primary” errors persist for 30+ seconds, the gap between Atlas and self-host is usually DNS TTL.
Fix 1: Use a Replica Set Connection String
Never connect to a single member of a replica set using its hostname directly — always use the full replica set URI so the driver can discover the current primary:
# Wrong — connects directly to one host, fails if it's a secondary
mongodb://mongo1.example.com:27017/mydb
# Correct — lists all members; driver discovers and connects to the primary
mongodb://mongo1.example.com:27017,mongo2.example.com:27017,mongo3.example.com:27017/mydb?replicaSet=rs0With authentication:
mongodb://username:[email protected]:27017,mongo2.example.com:27017,mongo3.example.com:27017/mydb?replicaSet=rs0&authSource=adminAtlas connection string (already includes replica set info):
mongodb+srv://username:[email protected]/mydb?retryWrites=true&w=majorityThe mongodb+srv:// scheme uses DNS SRV records to discover all replica set members automatically. Always prefer this format for Atlas clusters.
Fix 2: Check and Fix Read Preference
Read preference controls which replica set member the driver sends read operations to. It does not affect where writes go — writes always go to the primary:
// Node.js (Mongoose)
const mongoose = require('mongoose');
// Wrong — setting readPreference to secondary doesn't affect writes,
// but if you're also accidentally routing writes here, it will fail
mongoose.connect(uri, {
readPreference: 'secondary', // Reads go to secondary
});
// Correct — primary for writes (default), secondaryPreferred for reads
const client = new MongoClient(uri, {
readPreference: 'secondaryPreferred', // Reads prefer secondary, fall back to primary
});Read preference modes:
| Mode | Description |
|---|---|
primary | All reads go to the primary (default) |
primaryPreferred | Reads go to primary, fall back to secondary if unavailable |
secondary | All reads go to a secondary |
secondaryPreferred | Reads prefer secondary, fall back to primary |
nearest | Reads go to the member with lowest network latency |
Common Mistake: Setting
readPreference: 'secondary'is fine for reads. The error occurs when usingdirectConnection=trueto a secondary host or when the connection string targets a secondary directly.
Python (PyMongo):
from pymongo import MongoClient, ReadPreference
# Connect with replica set — driver auto-discovers primary
client = MongoClient(
"mongodb://mongo1:27017,mongo2:27017,mongo3:27017/",
replicaSet="rs0",
readPreference=ReadPreference.SECONDARY_PREFERRED
)
db = client.mydb
# Writes automatically go to primary
db.users.insert_one({"name": "Alice"}) # Goes to primary
# Reads go to secondary (or primary if no secondary available)
users = list(db.users.find())Fix 3: Handle Replica Set Elections Gracefully
When the primary steps down (due to failure, maintenance, or rs.stepDown()), an election occurs. During the election (typically 10–30 seconds), writes fail with not primary. Configure your driver to retry writes automatically:
// Node.js — enable retryable writes (default in modern drivers)
const client = new MongoClient(uri, {
retryWrites: true, // Automatically retry eligible write operations
retryReads: true, // Automatically retry eligible read operations
});# Python — retryable writes (enabled by default in PyMongo 3.9+)
client = MongoClient(
uri,
retryWrites=True,
retryReads=True,
)Retryable writes cover:
insertOne,updateOne,replaceOne,deleteOne- Bulk write operations with
ordered: truethat fail on the first operation findOneAndUpdate,findOneAndReplace,findOneAndDelete
Not covered by retryable writes:
insertManywithordered: falseupdateMany,deleteMany- Any multi-document operation
For operations not covered, add explicit retry logic:
async function writeWithRetry(collection, doc, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await collection.insertOne(doc);
} catch (err) {
if (
attempt < maxAttempts &&
(err.code === 10107 || // NotPrimary
err.message.includes('not primary') ||
err.message.includes('not master'))
) {
console.warn(`Write attempt ${attempt} failed (not primary) — retrying in ${attempt * 1000}ms`);
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
continue;
}
throw err;
}
}
}Fix 4: Check Replica Set Status
After a failover or if the error persists, check the replica set health:
// Connect to mongosh and check replica set status
rs.status(){
"set" : "rs0",
"members" : [
{
"name" : "mongo1:27017",
"health" : 1,
"state" : 1, // 1 = PRIMARY
"stateStr" : "PRIMARY",
...
},
{
"name" : "mongo2:27017",
"health" : 1,
"state" : 2, // 2 = SECONDARY
"stateStr" : "SECONDARY",
...
},
{
"name" : "mongo3:27017",
"health" : 0, // 0 = unhealthy
"state" : 8, // 8 = DOWN
"stateStr" : "DOWN",
...
}
]
}State values to look for:
| State | Meaning |
|---|---|
| 1 (PRIMARY) | Accepts reads and writes |
| 2 (SECONDARY) | Accepts reads (with appropriate read preference) |
| 5 (STARTUP2) | Initial sync in progress |
| 6 (UNKNOWN) | Cannot communicate with this member |
| 8 (DOWN) | Member is unreachable |
| 9 (ROLLBACK) | Rolling back operations after rejoining |
| 10 (REMOVED) | Member removed from the replica set |
If no primary exists:
// Force an election
rs.reconfig(rs.conf(), { force: true })
// Or step down the current primary (from the primary) to trigger election
rs.stepDown(60) // Steps down for 60 seconds
// Check election status
rs.isMaster() // Deprecated but still works
rs.hello() // Modern equivalentFix 5: Fix directConnection Issues
The directConnection=true option bypasses replica set topology discovery and connects directly to the specified host. This is useful for local development but causes not primary errors in production:
// Wrong — directConnection skips topology discovery
const client = new MongoClient('mongodb://mongo1:27017/mydb?directConnection=true');
// Correct — use replica set URI for production
const client = new MongoClient(
'mongodb://mongo1:27017,mongo2:27017,mongo3:27017/mydb?replicaSet=rs0'
);When directConnection=true is valid:
- Connecting to a standalone MongoDB instance (not a replica set)
- Local development with a single MongoDB instance
- Specifically targeting a secondary for maintenance operations
// Reading from a specific secondary for maintenance (not writes)
const secondaryClient = new MongoClient('mongodb://mongo2:27017/mydb', {
directConnection: true,
readPreference: 'secondaryPreferred',
});
// Never use this client for writesFix 6: Fix Write Concern Settings
Write concern controls how many replica set members must acknowledge a write before the driver considers it successful. An overly strict write concern on a degraded replica set causes writes to fail or time out:
// w: 'majority' — write must be acknowledged by the majority of members
// This fails if the majority is unreachable (e.g., 2 of 3 members are down)
const result = await collection.insertOne(doc, { writeConcern: { w: 'majority', wtimeout: 5000 } });
// w: 1 — only the primary needs to acknowledge (less safe but more available)
const result = await collection.insertOne(doc, { writeConcern: { w: 1 } });Connection-level write concern:
const client = new MongoClient(uri, {
writeConcern: {
w: 'majority',
wtimeoutMS: 5000, // Fail if majority doesn't respond in 5 seconds
journal: true, // Wait for journal flush
},
});Real-world scenario: A 3-member replica set loses 2 members. With
w: 'majority', writes fail because 2 members are needed to acknowledge. Drop tow: 1temporarily to keep writes flowing — but understand you risk data loss if the remaining primary crashes before replication occurs.
Still Not Working?
Verify the replica set name matches your connection string:
// In mongosh — check the replica set name
rs.conf().set // e.g., "rs0"
// Connection string must include replicaSet=rs0
mongodb://host1:27017,host2:27017/mydb?replicaSet=rs0Check network connectivity between application and all replica set members:
# From the application server, test each member
nc -zv mongo1.example.com 27017
nc -zv mongo2.example.com 27017
nc -zv mongo3.example.com 27017
# If any fail, the driver may not be able to discover the primaryCheck if the member is in RECOVERING state — a recovering member can’t accept reads or writes. Wait for it to finish syncing (check rs.status() and look at the optime lag).
Force a new initial sync for a stuck member:
# Stop mongod on the problematic member
sudo systemctl stop mongod
# Delete the data directory (this triggers a full resync)
sudo rm -rf /var/lib/mongodb/*
# Restart — mongod will sync from another member
sudo systemctl start mongodVerify the writeConcern default after a 5.0+ upgrade. On MongoDB 5.0+, the implicit writeConcern is majority. If your replica set is healthy but slow (cross-region replication, congested network), w: "majority" writes can hit wtimeout and surface as a timeout that looks like not primary in some driver versions. Run db.adminCommand({ getDefaultRWConcern: 1 }) to see the effective default and compare against what your operation overrides:
// Inspect the cluster default
db.adminCommand({ getDefaultRWConcern: 1 })
// Temporarily relax to w: 1 to confirm whether majority is the bottleneck
collection.insertOne(doc, { writeConcern: { w: 1 } })If w: 1 succeeds and w: "majority" fails, the cluster has a replication-lag problem, not a primary problem.
Check DNS TTL on mongodb+srv:// connection strings. SRV record changes (added member, removed member, IP change) propagate only as fast as the TTL allows. On self-hosted MongoDB behind your own DNS, a TTL of 300s means the driver can spend up to 5 minutes pointing at a removed host. Atlas uses TTL 60s. If your “not primary” lasts longer than 60 seconds, check your DNS TTL with dig +noall +answer _mongodb._tcp.cluster.example.com SRV.
Confirm your driver is recent enough for the server version. Driver-server version skew is real. A PyMongo 3.6 client connecting to a MongoDB 6.0 server will misinterpret some failover signals. The MongoDB compatibility matrix lists supported pairs. If you upgraded the server and kept the driver, upgrade the driver next.
For related MongoDB issues, see Fix: MongoDB Connect ECONNREFUSED, Fix: MongoDB Duplicate Key Error, and Fix: Mongoose Validation Failed. For MongoDB query patterns, see Fix: MongoDB Aggregation Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: MongoDB Aggregation Pipeline Not Working — Wrong Results or Empty Array
How to fix MongoDB aggregation pipeline issues — $lookup field matching, $unwind on missing fields, $match placement, $group _id, type mismatches, and pipeline debugging.
Fix: MongoDB Schema Validation Error — Document Failed Validation
How to fix MongoDB schema validation errors — $jsonSchema rules, required fields, type mismatches, enum constraints, bypassing validation for migrations, and Mongoose schema conflicts.
Fix: PocketBase Not Working — Auth Failing, Real-time Subscriptions Broken, or Collection Rules Blocking Requests
How to fix PocketBase issues — authentication, collection access rules, real-time subscriptions, file uploads, relations, and self-hosted deployment.
Fix: Neon Database Not Working — Connection Timeout, Branching Errors, or Serverless Driver Issues
How to fix Neon Postgres issues — connection string setup, serverless HTTP driver vs TCP, database branching, connection pooling, Drizzle and Prisma integration, and cold start optimization.