Skip to content

Fix: MongoDB "not primary" Write Error (Replica Set)

FixDevs · (Updated: )

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 primary

Or in older MongoDB versions:

MongoError: not master

Or after a failover:

MongoServerError: not primary and secondaryOk=false
MongoNotPrimaryError: Command find requires authentication

Or reads that worked suddenly fail:

MongoServerError: not primary or secondary; cannot currently read from this replSetMember.STATE=RECOVERING

Why 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 secondary for writes — some drivers allow you to set readPreference, 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 RECOVERING state — a node that is syncing, or that just rejoined after being down, enters the RECOVERING state and cannot serve reads or writes.
  • Using a direct connection to a specific host — connecting with directConnection=true to 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=rs0

With authentication:

mongodb://username:[email protected]:27017,mongo2.example.com:27017,mongo3.example.com:27017/mydb?replicaSet=rs0&authSource=admin

Atlas connection string (already includes replica set info):

mongodb+srv://username:[email protected]/mydb?retryWrites=true&w=majority

The 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:

ModeDescription
primaryAll reads go to the primary (default)
primaryPreferredReads go to primary, fall back to secondary if unavailable
secondaryAll reads go to a secondary
secondaryPreferredReads prefer secondary, fall back to primary
nearestReads go to the member with lowest network latency

Common Mistake: Setting readPreference: 'secondary' is fine for reads. The error occurs when using directConnection=true to 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: true that fail on the first operation
  • findOneAndUpdate, findOneAndReplace, findOneAndDelete

Not covered by retryable writes:

  • insertMany with ordered: false
  • updateMany, 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:

StateMeaning
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 equivalent

Fix 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 writes

Fix 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 to w: 1 temporarily 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=rs0

Check 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 primary

Check 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 mongod

Verify 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles