MongoDB 8.3 High Availability and Scalability: What Changed On-Premises and in Atlas

MongoDB 8.3 shipped on 4 May 2026, and for anyone who runs sharded clusters it is the first minor release where the self-managed builds get the same sharding lifecycle tooling that Atlas has been driving behind the scenes since 8.0. Three changes matter operationally: the removeShard command is deprecated in favour of a four-step draining workflow you can pause and inspect, a replica set can now be turned directly into a sharded cluster with an embedded config shard in one rolling restart, and a set of overload controls (targeted mirrored reads in 8.2, connection establishment rate limiting, overload-aware retry targeting in MongoDB 8.3) closes the gap that used to open up in the minutes after an election.

This post walks through each of those for MongoDB 8.3 high availability and scalability work, on-premises and in Atlas, and ends with where I would and would not deploy MongoDB 8.3 today.

One thing to get out of the way first, because it changes the whole decision: MongoDB 8.3 is a minor release, not the next long-term line. That has consequences on-premises that Atlas customers never see.

MongoDB 8.3 high availability and scalability on-premises and in Atlas

Which MongoDB 8.x are you actually running?

MongoDB's cadence since 8.0 has two tracks. Major releases (7.0, 8.0) arrive roughly every two years, run on Atlas and on-premises, and carry a five-year lifecycle. Minor releases (8.1, 8.2, 8.3) arrive quarterly-ish and until 8.2 were Atlas-only. Starting with 8.2 the minor track is downloadable for Community and Enterprise Advanced, which is how MongoDB 8.3 ends up on an on-prem server in the first place.

The support policy for minors is the part people miss: once a new minor ships, the previous one stops receiving patches. 8.2 reached end of life on 31 July 2026, ten months after it was released. If you deployed 8.2 on-premises last autumn you are already off support unless you have moved to MongoDB 8.3.

MongoDB 8.3 release tracks: the 8.0 major line versus the 8.1, 8.2 and 8.3 minor line, with support windows
The 8.0 line is the only one with a multi-year patch horizon. MongoDB 8.3 is current, but its patch window ends the day 8.4 (or 9.0) ships.

Three more rules from the versioning policy that shape a self-managed upgrade plan. Minor-to-minor upgrades are strictly sequential, so an 8.1 cluster goes through 8.2 before it can reach MongoDB 8.3, and each hop is a binary upgrade plus an FCV bump. Going from a minor back to a major is a downgrade, and binary downgrades are not supported on Community Edition at all. And two features are explicitly unsupported on the minor track: Atlas Live Migration and mongosync. If you have a cluster-to-cluster sync running for DR, or you are planning a migration into Atlas next year, a MongoDB 8.3 source takes those tools off the table.

The 8.0 line, by contrast, is the one that receives backports. The connection rate limiter that I cover below is documented as available in 8.0.12, and the initial sync index memory controls in 8.0.13. Percona Server for MongoDB, which is what most of our self-managed customers run, is built on the 7.0 and 8.0 lines only; the 8.0.29-13 build from 20 August 2026 carries the same CVE fixes as upstream 8.0.29. There is no Percona build of MongoDB 8.3.

MongoDB 8.3 scalability: what changed in the sharded cluster lifecycle

Config shards, and the three-shard rule

Since 8.0 a sharded cluster no longer needs a dedicated config server replica set. One shard can carry the cluster metadata alongside application data; MongoDB calls this a config shard or embedded config server. On a small cluster that removes three nodes of infrastructure, which is the difference between nine mongod processes and six for a two-shard deployment.

The documentation's guidance is blunt, and I agree with it from operating both shapes: use a config shard at three shards or fewer, and move to a dedicated CSRS beyond that, or earlier if the workload is latency-sensitive enough that you do not want metadata reads and refreshes competing with user I/O on the same WiredTiger cache. Queryable Encryption collections and on-prem queryable backups also require a dedicated config server.

MongoDB 8.3 sharded cluster topology: embedded config shard versus dedicated config server replica set
The embedded shape saves three nodes; the dedicated shape isolates metadata I/O. The transition between them is online in both directions.

In Atlas the decision is made for you. Atlas-Managed Config Servers is on by default for every 8.0+ sharded cluster: at five shards or fewer Atlas runs an embedded config server, and when you add a sixth shard it transitions to a dedicated config server automatically, draining user data off config-0 with chunk migrations and moveCollection, waiting out orphanCleanupDelaySecs, and then adding a replacement shard to keep your shard count.

That transition is online but it is not free: receiving shards see elevated CPU, memory and I/O for the duration, you cannot change the cluster tier while it runs, and Atlas tells you not to cancel it. On a multi-terabyte cluster it can run for days. Clusters using Atlas Search, unsharded time series collections or Queryable Encryption are pinned to whichever config type they started with.

MongoDB 8.3: replica set to sharded cluster in one rolling restart

This is the change I am most pleased about for self-managed estates. Before MongoDB 8.3, converting a replica set into a sharded cluster with a config shard meant first turning it into a dedicated config server and then transitioning, which was awkward because a dedicated CSRS is not supposed to hold user data. MongoDB 8.3 adds --replicaSetConfigShardMaintenanceMode, which relaxes the startup checks so the members can be restarted as --configsvr while still carrying application data.

The sequence, on a three-member replica set rs0, secondaries first:

# Step 1: rolling restart, one member at a time, in maintenance mode
mongod --config /etc/mongod.conf --configsvr --replicaSetConfigShardMaintenanceMode
# wait for rs.status() to show the member back in SECONDARY before the next one
// Step 2: on the primary, flag the replica set as a config server
var conf = rs.conf();
conf.configsvr = true;
conf.version += 1;
rs.reconfig(conf);

// confirm every member has applied it before going further
db.aggregate([
  { $documents: rs.status().members },
  { $group: { _id: null, allConfigSvr: { $min: { $eq: ["$configsvr", true] } } } }
]);
// expected: { _id: null, allConfigSvr: true }
# Step 3: second rolling restart WITHOUT the maintenance flag
mongod --config /etc/mongod.conf --configsvr

# Step 4: start a router pointing at the set
mongos --config /etc/mongos.conf   # sharding.configDB: rs0/host1:27017,host2:27017,host3:27017
// Step 5: through mongos, make rs0 both config server and first shard
db.adminCommand({ transitionFromDedicatedConfigServer: 1 });
// { ok: 1 }

sh.isConfigShardEnabled();          // enabled: true
db.adminCommand({ listShards: 1 }); // shard _id "config" is rs0

Two things to write into the runbook before you run this. First, the application connection string changes from the replica set seed list to the mongos address, so plan the cutover with the application team rather than discovering it during the change window. Second, and this is new in MongoDB 8.3: a replica set that has been a sharded cluster cannot be converted back to a plain replica set. The shard identity document survives, and clearing it is a support-assisted procedure. Treat the conversion as one-way and rehearse it on a restored backup first.

Draining a shard without removeShard

removeShard has been the same opaque call since 2.x: issue it, poll it, and hope the balancer keeps moving. MongoDB 8.3 deprecates it and splits the operation into four commands, all run through mongos with the clusterManager role.

// 1. begin draining; the balancer does the actual chunk moves, so it must be enabled
db.adminCommand({ balancerStatus: 1 });          // mode: "full"
db.adminCommand({ startShardDraining: "shard04" });

// 2. poll; this is the part removeShard never gave you cleanly
db.adminCommand({ shardDrainingStatus: "shard04" });
/* abridged from the MongoDB 8.3 reference:
{
  shard: "shard04",
  remainingCriticalSectionChunks: 0,
  totalChunksToDrain: 15,
  chunksLeftToDrain: 8,
  databases: [ { name: "orders", isPrimary: true, collections: [ ... ] } ],
  ok: 1
} */

// 3. anything the balancer cannot move has to be moved by hand:
//    databases that have shard04 as primary shard ...
db.adminCommand({ movePrimary: "orders", to: "shard03" });
//    ... and unsharded collections that live on shard04
db.adminCommand({ moveCollection: "orders.fx_rates", toShard: "shard03" });

// 4. commit only when chunksLeftToDrain is 0 and databases is empty
db.adminCommand({ commitShardRemoval: "shard04" });

// change of plan? draining is reversible until you commit
db.adminCommand({ stopShardDraining: "shard04" });

The operational gain is not the syntax; it is that draining is now an inspectable, stoppable state. On a cluster where the balancer window is restricted to nights, you can start the drain on Monday, watch chunksLeftToDrain fall over the week, and commit on Friday without ever holding the old command open. Two constraints carry over from removeShard and are worth restating: you cannot take a cluster backup while a drain is in progress, and open change stream cursors may close and not resume across it. None of these commands exist on Atlas, where shard removal is a cluster-configuration change that Atlas executes for you.

Moving data without changing the shard key

The rest of the data-movement toolbox arrived in 8.0 and is unchanged in MongoDB 8.3, but it is what makes the draining workflow usable, so it belongs here. moveCollection relocates an unsharded collection to a named shard. unshardCollection collapses a sharded collection onto one shard. And reshardCollection with forceRedistribution: true rebalances a collection across the current shard set on its existing key, which is how you spread a collection onto a newly added shard without waiting for the balancer's chunk-by-chunk pace.

// spread orders.events across all shards on the same key after adding shard05
db.adminCommand({
  reshardCollection: "orders.events",
  key: { tenant_id: "hashed" },
  forceRedistribution: true
});

// progress: remainingOperationTimeEstimatedSecs is -1 until the clone phase has run long enough to extrapolate
db.getSiblingDB("admin").aggregate([
  { $currentOp: { allUsers: true, localOps: false } },
  { $match: { type: "op", "originatingCommand.reshardCollection": "orders.events" } },
  { $project: { shard: 1, desc: 1, totalOperationTimeElapsedSecs: 1, remainingOperationTimeEstimatedSecs: 1 } }
]);

All three run on the resharding machinery, so all three inherit its preconditions, and these are the numbers I check before approving one on a production cluster: each participating shard needs free storage of at least twice the collection size plus indexes divided by the shard count, I/O utilisation under 50 percent, CPU under 80 percent, and writeConcernMajorityJournalDefault set to true. The operation has a floor of about five minutes even for a tiny collection, and it ends with a critical section where writes to the collection are blocked for roughly two seconds. Applications with aggressive client-side timeouts notice that. Index builds started during the operation can fail silently, so freeze DDL on the namespace for the duration.

The orphan cleanup change that kills your reporting queries

This one is easy to miss in the 8.2 notes and it is the change most likely to show up as a mystery ticket after an upgrade. After a chunk migration the donor shard has to delete the range it gave away. Two defaults changed in 8.2. orphanCleanupDelaySecs went from 900 to 3600 seconds, so orphans now sit on the donor for an hour before deletion. And terminateSecondaryReadsOnOrphanCleanup was introduced with a default of true: when the range deletion finally runs, any long-running read on a secondary that could still be reading the orphaned range is terminated first.

The intent is correctness, because a secondary read that spans a range deletion could return partial results. The effect is that hour-long analytics queries pinned to secondaries with readPreference: secondary die at unpredictable times after every migration. The counter to watch is serverStatus().metrics.operation.killedDueToRangeDeletion; if it climbs, either move those queries to a dedicated analytics node with a balancer window that avoids them, or raise orphanCleanupDelaySecs beyond the longest legitimate query. Note that a new value only applies to range deletions created after the change; existing ones keep the old delay until you step the primary down.

ParameterPre-8.28.2 / 8.3 defaultUnitChange requires
orphanCleanupDelaySecs9003600secondsruntime setParameter; applies to new range deletions only
terminateSecondaryReadsOnOrphanCleanupn/atruebooleanruntime setParameter
mirrorReads.targetedMirroring.tagn/a{} (off)documentruntime setParameter, primary only
ingressConnectionEstablishmentRateLimiterEnabledn/a (8.0.12+)falsebooleanruntime setParameter
initialSyncIndexBuildMemoryPercentagen/a (8.0.13+)10.0percent of RAMruntime setParameter
overloadAwareServerSelectionEnabledn/afalse (8.3)booleanruntime setParameter

MongoDB 8.3 high availability: closing the post-election gap

Elections in MongoDB have been fast for years; a healthy replica set picks a new primary in well under ten seconds. The availability loss that actually hurts is what happens in the two or three minutes afterwards: the new primary has a cold cache, every application pool reconnects at once, and the retry logic in drivers and in mongos hammers whichever node answered first. The changes in MongoDB 8.2 and MongoDB 8.3 target exactly that window.

MongoDB 8.3 high availability controls and where they act during a failover
None of the MongoDB 8.3 controls shorten the election. They shorten what comes after it.

Targeted mirrored reads (8.2)

Mirrored reads have been on by default since 4.4: the primary forwards one percent of eligible reads (find, count, distinct, and the filter portion of update and findAndModify) to electable secondaries, fire-and-forget, so their caches are not stone cold when one of them wins an election. The limitation was that it sprayed evenly across every electable member and could not reach hidden nodes. 8.2 adds targetedMirroring, which mirrors to members matching a replica set tag, at its own sampling rate, and hidden members are allowed.

// tag the member you intend to fail over to (a DR-site member, or the one with priority 2)
var cfg = rs.conf();
cfg.members[2].tags = { warm: "standby" };
cfg.version += 1;
rs.reconfig(cfg);

// on the primary: keep the 1% general mirroring, and additionally mirror 25% of reads to the tagged member
db.adminCommand({
  setParameter: 1,
  mirrorReads: {
    samplingRate: 0.01,
    maxTimeMS: 1000,
    targetedMirroring: { tag: { warm: "standby" }, samplingRate: 0.25, maxTimeMS: 1000 }
  }
});

// confirm it is doing something
db.serverStatus({ mirroredReads: 1 }).mirroredReads;

A few cautions from running this. The setting lives on the primary only, so after a failover the new primary has whatever mirrorReads value it was started with; put it in mongod.conf under setParameter on every member, not just the current primary. Only one tag can be supplied, and every member carrying that tag is targeted. Mirrored reads consume connections from a pool capped by mirrorReadsMaxConnPoolSize (default 4, new in 8.2), so a high targeted sampling rate on a busy primary will start dropping mirrors rather than slowing the primary, which is the right failure mode but means the warm-up is best-effort.

And it warms the WiredTiger cache, not the filesystem cache or the plan cache in any guaranteed way; on a member with much less RAM than the primary it cannot do much.

Connection establishment rate limiting (8.2, in 8.0.12)

Of everything in this post, this is the change I would enable first on a production replica set, and the fact that it is backported to 8.0.12 means you do not need the minor track to get it. When a primary steps down, every application server's pool detects the topology change and reconnects. A fleet of 400 application pods with 50-connection pools is 20,000 TLS handshakes and SCRAM exchanges landing on the new primary inside a few seconds, and on a mid-sized node that alone can push CPU to saturation and delay the very operations the pools are reconnecting to run.

Before 8.2 the only knobs were maxIncomingConnections, which is a hard cap, and driver-side jitter, which you do not control across every team.

# mongod.conf: admit 500 new connections/sec, absorb a 4-second burst above that,
# queue up to 2000 more, reject the rest with a retryable error
setParameter:
  ingressConnectionEstablishmentRateLimiterEnabled: true
  ingressConnectionEstablishmentRatePerSec: 500
  ingressConnectionEstablishmentBurstCapacitySecs: 4
  ingressConnectionEstablishmentMaxQueueDepth: 2000
// what to graph after enabling it
var s = db.serverStatus();
printjson({
  queued:   s.connections.queuedForEstablishment,
  rejected: s.connections.establishmentRateLimit.rejected,
  exempted: s.connections.establishmentRateLimit.exempted,
  avgQueuedMicros: s.queues.ingressSessionEstablishment.averageTimeQueuedMicros,
  tlsHandshakeMicros: s.metrics.network.averageTimeToCompletedTLSHandshakeMicros,
  authMicros:         s.metrics.network.averageTimeToCompletedAuthMicros
});

The numbers above are a starting shape, not a recommendation; size the rate from your own measured reconnect behaviour. The way to get that number is to look at averageTimeToCompletedTLSHandshakeMicros and averageTimeToCompletedAuthMicros during a planned stepdown in staging with the limiter off, then set the rate so the queue drains in a few seconds rather than letting the handshakes starve the oplog appliers. The default MaxQueueDepth of 0 rejects anything that would queue, which is almost never what you want once the limiter is on; pick a real depth. And test the driver behaviour: a rejected connection surfaces as a network error the driver retries with backoff, which is fine for modern drivers and not fine for anything hand-rolled.

Overload-aware server selection (8.3)

MongoDB 8.3 adds overloadAwareServerSelectionEnabled, off by default. It changes how a mongos or a mongod acting as an internal client picks a target when an operation fails with an error labelled SystemOverloadedError: instead of retrying against the same member it prefers one that has not recently reported overload. Alongside it, MongoDB 8.3 gives the internal retry path a token bucket (shardRetryTokenBucketCapacity, shardRetryTokenReturnRate) and explicit backoff controls (defaultClientRetryAttempts, default 3, plus base and max backoff in milliseconds).

Together these are the sharded-cluster equivalent of the connection limiter: they stop a mongos fleet from converting one slow shard secondary into a cluster-wide retry storm. I have not yet run this one under a production-shaped load test, so I am reporting what it does rather than how much it helps; it is on my list for the next MongoDB 8.3 lab pass.

Rebuilding a member faster

Initial sync is the other place where HA quietly degrades: while a replaced member is syncing, a three-member set is running with a majority of two out of two live members, and a second failure means read-only. 8.2 lets index builds during initial sync use a percentage of RAM (initialSyncIndexBuildMemoryPercentage, default 10, bounded by initialSyncIndexBuildMemoryMinMB 200 and MaxMB 16384) instead of the old fixed budget. On a 256 GB node that is 16 GB for index builds instead of a few hundred megabytes, and it is backported to 8.0.13 and 7.0.26. Raise it during a rebuild and drop it back afterwards; 40 percent on a member that is otherwise idle during sync is a reasonable ceiling.

// on the syncing member, before it reaches the index build phase
db.adminCommand({ setParameter: 1, initialSyncIndexBuildMemoryPercentage: 40 });

// catch-up visibility, new in MongoDB 8.3
db.serverStatus().metrics.repl.network.oplogFetcherLagSeconds;
db.serverStatus().metrics.repl.network.oplogGetMoresProcessed;

What “majority” means since 8.0

One 8.0 change still surprises teams moving from 7.0 and it matters for how you read HA metrics. w: "majority" now acknowledges when a majority of members have written the oplog entry, not when they have applied it. Writes get faster; a read from a secondary immediately after an acknowledged write, without causal consistency, can still miss it. rs.status() exposes the distinction per member as optimeWritten alongside optime, and if you alert on replication lag you should decide which of the two you mean. For application correctness, sessions with causal consistency or readConcern: "majority" behave exactly as before.

MongoDB 8.3 high availability and scalability on-premises versus Atlas

The same server code runs in both places, but the operational surface for MongoDB 8.3 high availability and scalability is different enough that the runbooks do not transfer. The table is how I frame it for customers deciding where a new sharded workload should live.

ConcernSelf-managed (Community / EA / Percona)Atlas
Version track8.0.x for a multi-year patch horizon; MongoDB 8.3 only if you need its features and accept sequential minor upgrades and no mongosyncPin "major version" (8.0) or opt into auto-upgrade to latest; once auto-upgraded onto a minor you cannot return to the major until the next major ships
Config server typeYour call; embedded up to 3 shards, dedicated beyond, with transitionTo/FromDedicatedConfigServer and the MongoDB 8.3 direct conversionAtlas-managed; embedded to 5 shards, automatic online transition beyond, blocked for Search/QE/unsharded time series
Adding or removing shardsaddShard; MongoDB 8.3 draining workflow; you own balancer windows and movePrimaryChange shard count in the cluster config; draining commands are not exposed
Vertical scalingRolling hardware or VM changes, your rolling-restart runbookTier change with rolling restart; reactive and predictive compute auto-scaling, storage auto-scaling
Post-failover overloadConfigure the 8.2/8.3 parameters above yourself, per member, in mongod.confServer parameters largely not user-settable; rely on Atlas tier headroom and driver settings
Cross-region HAReplica set members across sites with priorities and tags; zone sharding by handMulti-region and multi-cloud replica sets, Global Clusters (always dedicated config servers)
KubernetesPercona Operator for MongoDB or the MongoDB Enterprise operator; you own storage classes and PDBsAtlas Kubernetes Operator manages Atlas resources, not pods

The honest summary is that Atlas removes the sharding lifecycle work, which is where most self-managed sharded clusters go wrong, and in exchange takes away the server-parameter surface that lets you tune the post-election window. If your failure mode is "we mis-sized a shard migration", Atlas is the safer place. If your failure mode is "a reconnect storm took the primary to 100 percent CPU", self-managed 8.0.12+ with the connection limiter is the more controllable one. For most of the estates we run, a self-managed 8.0.x line on Percona builds with the backported controls enabled is where the risk is lowest today.

Where I would deploy MongoDB 8.3, and where I would not

As of September 2026: on Atlas, take MongoDB 8.3 on non-production and on production clusters that do not depend on Live Migration, and keep production sharded clusters pinned to the 8.0 major until you have read the no-revert rule twice. On-premises, stay on the 8.0 line for anything that has to be supportable in 2028, and reach for MongoDB 8.3 only when a feature forces it: the direct replica set to config shard conversion, the shard draining workflow, or the search, vector search and Queryable Encryption capabilities that are the stated reason the minor track exists on-prem at all.

If you do go to MongoDB 8.3 self-managed, budget for a binary upgrade every quarter, because the patch window closes the day the next minor ships, and check the Linux kernel: 8.2 carried a TCMalloc incompatibility with kernels 6.19 through 7.0.13 that crashed the process, and the fix was to move the kernel, not the database.

Whichever version you land on, MongoDB 8.3 high availability and scalability still come down to operations. The four things that decide availability in most MongoDB incidents I get called into are not version features. They are an oplog window shorter than the longest maintenance operation, a balancer running during peak hours, secondaries with less RAM than the primary they were expected to replace, and nobody having rehearsed the failover. MongoDB 8.3 gives you better tools for the aftermath of an election; it does not replace the drill.

Everything above should be tested against your own workload in a staging environment that mirrors production topology before any of it is applied to production, with a verified backup and a rehearsed restore in place. If you want a second pair of eyes on a sharded cluster design, an 8.0 to MongoDB 8.3 upgrade plan, or a failover drill, that is the kind of work the MinervaDB MongoDB consulting team does every week, on-premises and in Atlas.

References

Release Notes for MongoDB 8.3 · Release Notes for MongoDB 8.2 · MongoDB Versioning · Config Shard · Convert Replica Set to an Embedded Config Shard · startShardDraining · moveCollection · Reshard a Collection · Server Parameters · Atlas: Transition to Dedicated Config Servers · Atlas Architecture Center: Scalability · MongoDB Software Lifecycle Schedules · Percona Server for MongoDB 8.0.29-13

About MinervaDB Corporation 354 Articles
Full-stack Database Infrastructure Architecture, Engineering and Operations Consultative Support(24*7) Provider for PostgreSQL, MySQL, MariaDB, MongoDB, ClickHouse, Trino, SQL Server, Cassandra, CockroachDB, Yugabyte, Couchbase, Redis, Valkey, NoSQL, NewSQL, SAP HANA, Databricks, Amazon Resdhift, Amazon Aurora, CloudSQL, Snowflake and AzureSQL with core expertize in Performance, Scalability, High Availability, Database Reliability Engineering, Database Upgrades/Migration, and Data Security.