JusDB
Free DB Audit
NoSQL Databases

MongoDB Oplog Sizing and the Replication Window

Measure your MongoDB oplog window and write churn, find what fills the oplog, size it with a worked example, and resize it safely on every member or on Atlas.

JusDB Team
Published October 25, 2026
Updated November 9, 2026
17 min read

A secondary comes back from a four-hour maintenance window and never returns to SECONDARY. It stays in RECOVERING, and its log says We are too stale to use <node>:27017 as a sync source. Or a change stream consumer restarts after a long weekend and fails with ChangeStreamHistoryLost. Both have one cause: the oplog window, the time between the oldest and newest oplog entries, was shorter than the gap. On self-managed MongoDB 7.0, 8.0, and 9.0, the default oplog is 5% of free disk space, capped at 50 GB, and a write-heavy workload can turn that into a few hours of history. This guide covers measuring the window and write rate, what fills the oplog, sizing with a worked example, resizing every member, and MongoDB Atlas.

TL;DR
  • The oplog is a capped collection: its window is the configured size divided by your write rate, and a write burst shortens it.
  • Measure the window with rs.printReplicationInfo() on every member, and find what fills it by grouping the last hour of local.oplog.rs by operation and namespace.
  • Size for the longest of secondary downtime, a logical initial sync, a change stream consumer outage, or a mongodump --oplog run, plus a margin.
  • Resize with replSetResizeOplog on each secondary, then the primary, without a restart. Put the same values in mongod.conf.
  • Atlas does not support replSetResizeOplog. Atlas Core clusters with storage auto-scaling keep a minimum oplog window of 24 hours by default.

Scope and review date: November 2026. This post covers self-managed MongoDB 7.0, 8.0, and 9.0 replica sets, including each shard of a sharded cluster, with a section on MongoDB Atlas. Commands, defaults, and limits were checked against the MongoDB 9.0 manual, the matching 7.0 pages, and the Atlas documentation on 9 November 2026. They were not run in a lab. The terminal output and every number in the worked example are illustrative.


What is the oplog window, and why does it shrink?

The replica set oplog is a capped collection, local.oplog.rs, on every member. Each write that changes data appends entries, and secondaries copy and apply them in order. When the oplog reaches its maximum size, MongoDB deletes the oldest entries.

The oplog window is the time between the newest and oldest entries. A secondary that loses contact with its sync source can catch up only if it returns inside that window. Because the size is fixed in bytes, the window depends on how fast you write: a 50 GB oplog holds about 50 hours at 1 GB per hour and about 5 hours at 10 GB per hour.

The oplog as a capped window, and a secondary inside it

Writes enter the window, truncation removes the oldest entries, and the secondary must stay in between

Oplog window and secondary lag Step 1: application writes append entries to the primary's capped oplog. Step 2: a secondary fetches and applies entries from its last applied position; its distance from the newest entry is its lag. Step 3: when the oplog is full, the oldest entries are truncated. Headroom is the window minus the lag; at zero the secondary is stale and needs a resync. Application writes Primary oplog capped collection Secondary applies in order Truncated oldest entries 1 each write adds oplog entries 2 lag: how far it is behind the newest 3 removed once the size limit is reached headroom = window minus lag; 0 = stale

Watch headroom, not lag alone: a secondary 2 hours behind is safe inside a 48-hour window and lost inside a 90-minute one. Atlas reports this difference as Replication Headroom.

Two details matter for sizing. The configured size is the uncompressed size of the entries, not bytes on disk (replication.oplogSizeMB). And the oplog can grow past its size so that it never deletes the majority commit point, so a lagging majority can temporarily make it larger than you set.

How big is the MongoDB oplog by default?

A member sets the default when it first creates its oplog.

Platform and engineDefault oplog size
Linux or Windows, WiredTiger5% of free disk space, at least 990 MB, at most 50 GB
Linux or Windows, in-memory engine5% of physical memory, at least 990 MB, at most 50 GB
64-bit macOS192 MB of free disk space (or of memory for the in-memory engine)

The 50 GB cap applies only to the default: replSetResizeOplog accepts 990 MB up to 1 PB.

javascript
// mongosh, connected directly to one member
const s = db.getSiblingDB("local").oplog.rs.stats(1024 * 1024)
({ maxSizeMB: s.maxSize, usedMB: s.size, onDiskMB: s.storageSize })

Interpretation: maxSizeMB is the configured maximum, usedMB the uncompressed size of the current entries, and onDiskMB the compressed size on disk. If usedMB is below maxSizeMB, the oplog is not full yet and its window only reflects the member's age.

How do I measure the oplog window and the write rate?

rs.printReplicationInfo() prints the window for people. In scripts, use db.getReplicationInfo(), which returns a document. Both read the member you are connected to.

rs.printReplicationInfo() on a primary with the default 50 GB oplog

Illustrative output, shaped like mongosh: the oplog is full and holds 8.5 hours

mongosh: rs0 primary
rs0 [direct: primary] admin> rs.printReplicationInfo()actual oplog size'51200 MB'---configured oplog size'51200 MB'---log length start to end'30600 secs (8.5 hrs)'---oplog first event time'Mon Nov 09 2026 00:30:05 GMT+0000 (Coordinated Universal Time)'---oplog last event time'Mon Nov 09 2026 09:00:05 GMT+0000 (Coordinated Universal Time)'

8.5 hours is history, not a forecast: it measures oldest entry to newest, so the window right after a burst is the one that counts. Field meanings are in the db.getReplicationInfo() reference.

In current mongosh, actual oplog size is the larger of the configured and real sizes, so a value above configured oplog size means the oplog has grown past its limit. Once the oplog is full, the used size divided by the window gives the average write rate:

javascript
// Average oplog write rate over the current window (oplog must be full)
const r = db.getReplicationInfo()
({ usedMB: r.usedMB, windowHours: r.timeDiffHours,
   mbPerHour: Math.round(r.usedMB / r.timeDiffHours) })

Interpretation: for the output above, about 6,000 MB per hour. An average hides bursts, so measure hour by hour next.

What fills the oplog faster than you expect?

Oplog volume follows the number of changed documents, not data growth. The oplog documentation names updates to many documents at once, deletes that match inserts, and many in-place updates. In each case the data barely changes while the oplog fills.

  • Bulk updates. The oplog turns a multi-document update into one entry per document, so an updateMany over 10 million documents writes 10 million entries.
  • Multi-document transactions. A transaction writes as many oplog entries as it needs, each within the 16 MB BSON limit (transactions in production).
  • TTL and purge deletes. Only the primary runs TTL deletes, and secondaries replicate each one. A new TTL index on a large collection, or a lower expireAfterSeconds, can expire many documents at once (TTL indexes).
  • Bulk loads. Large ingests with unacknowledged writes can outrun the secondaries.
  • Index builds. A replicated build writes only small startIndexBuild and commitIndexBuild entries. The risk is time: a rolling build takes a member out, and the rolling build procedure says the oplog must be large enough for it to catch up.
javascript
// Top writers in the last hour, by operation and namespace.
// Reads every entry of that hour: run it on a secondary, off peak.
db.getMongo().setReadPref("secondaryPreferred")
const since = Math.floor(Date.now() / 1000) - 3600
db.getSiblingDB("local").oplog.rs.aggregate([
  { $match: { ts: { $gte: Timestamp({ t: since, i: 0 }) } } },
  { $group: {
      _id: { op: "$op", ns: "$ns" },
      entries: { $sum: 1 },
      mb: { $sum: { $divide: [ { $bsonSize: "$$ROOT" }, 1048576 ] } }
  } },
  { $sort: { mb: -1 } },
  { $limit: 10 }
])

Interpretation: op is i, u, d, c (command), or n (no-op). Transactions appear as c entries on admin.$cmd that wrap the real operations. The hourly mb total is your churn rate; run it for your busiest hour, such as the nightly batch.

What happens when the oplog window is too short?

Secondaries fall off and need an initial sync

If a secondary's next entry is gone from its sync source, it cannot catch up. It enters RECOVERING and needs manual intervention (replica set states), and logs the "too stale" line from the introduction, as MongoDB's oplog alert guide shows. The fix is a full resync.

The initial sync needs the window too. A logical initial sync clones the data and then applies the entries written meanwhile. If it finds a gap, it restarts from the beginning (replica set data synchronization), so a window shorter than your initial sync time can loop.

Change streams lose their resume point

Resuming with resumeAfter, startAfter, or a past startAtOperationTime needs that point to still be in the oplog (change streams). Otherwise the server returns ChangeStreamHistoryLost, code 286 in the server's error code list. An open Jira report, SERVER-128753, shows 8.0.18 returning ChangeStreamFatalError (280) in a similar case, so check the oplog's first event time before trusting the code. Our change streams production guide covers storing resume tokens and recovering from this error.

Backups and rolling maintenance overrun

mongodump --oplog captures entries written during the dump, and the replSetResizeOplog reference warns that truncation can make it miss entries. Every member taken offline for maintenance must also rejoin inside the window.

javascript
// On the primary: lag and headroom for each secondary
const info = db.getReplicationInfo()
const st = rs.status()
const p = st.members.find(m => m.stateStr === "PRIMARY")
st.members.filter(m => m.stateStr === "SECONDARY").map(m => {
  const lagH = (p.optimeDate - m.optimeDate) / 3600000
  return { member: m.name, syncsFrom: m.syncSourceHost,
           lagHours: lagH.toFixed(2),
           headroomHours: (info.timeDiffHours - lagH).toFixed(2) }
})

Interpretation: this uses the primary's window. A secondary that syncs from another secondary (syncsFrom) depends on that member's window, so measure it there too.

How do I size the oplog from my write rate?

First decide how many hours you need, then find the most oplog data your workload writes in any stretch of that length. MongoDB's replica set troubleshooting guide says the oplog should cover the longest downtime you expect on a secondary and at a minimum 24 hours of operations, and that many users prefer 72 hours or a week.

The example continues the cluster from the terminal card. All numbers are illustrative arithmetic, not measurements.

RequirementHours (illustrative)
Longest planned secondary outage6
Longest mongodump --oplog run5
Logical initial sync, timed on staging20
Longest change stream consumer outage to survive36
Target: largest requirement x 1.554

The hourly aggregation shows a steady 4 GB per hour plus a nightly job from 02:00 to 04:00 UTC at 12 GB per hour. That explains the 8.5-hour window: 1.5 hours before the job (6 GB), the job (24 GB), and 5 hours after it (20 GB) fill 50 GB.

StepArithmetic (illustrative)Result
Baseline over 54 hours4 GB/h x 54 h216 GB
Extra per nightly run(12 - 4) GB/h x 2 h16 GB
Worst 54-hour stretch (3 runs)216 + 3 x 16264 GB
Size to set, rounded up270 GB x 1024276,480 MB

The 1.5 margin is our convention, not a MongoDB rule. Because the size is uncompressed, the oplog usually takes less disk than its size; compare storageSize with size for your ratio, but plan free space without compression until you have measured it.

Shrink the churn before you grow the oplog

Batching a job lowers its peak rate and lag, not the total it writes. To write less, change the job: dropping a collection writes one oplog entry, while deleting its documents writes one per document, so a monthly purge can drop a per-month collection instead.

Should I use a fixed size or a minimum retention period?

You can also set oplogMinRetentionHours. The mongod then deletes an entry only when the oplog is at its maximum size and the entry is older than that many hours by the host clock. The default is 0: size alone decides.

Size-based truncation vs a minimum retention period

Switch modes to see what bounds the window and what bounds the disk

Size only (default)

QuestionSize only
What you setMaximum size in MB
When an entry is deletedWhen the oplog reaches its size, oldest first
Window during a write burstShrinks, sometimes from days to hours
Disk useBounded by the size, apart from the majority commit point
Self-managed settingoplogSizeMB, or replSetResizeOplog with size
Atlas Core equivalentSet Oplog Size, only with storage auto-scaling off
Main riskThe window collapses and secondaries go stale

Size plus minimum retention

QuestionSize plus minimum retention
What you setMaximum size in MB and a minimum number of hours
When an entry is deletedOnly when the size is reached and the entry is older than the hours
Window during a write burstStays at least the configured hours
Disk useCan grow without a limit and may exhaust the disk
Self-managed settingoplogMinRetentionHours, or replSetResizeOplog with minRetentionHours
Atlas Core equivalentSet Minimum Oplog Window, 24 hours by default with storage auto-scaling
Main riskA burst fills the disk; clock drift between members changes what is kept

Trade-off: a retention period moves the risk from replication to disk space. Use it only with a disk-space alert, and size the oplog so retention rarely has to override it.

The configuration reference adds that once retention has grown the oplog past its size, the mongod may keep that disk space afterwards. For the example, the 270 GB size plus a 36-hour minimum (the longest single requirement) is a reasonable pair.

javascript
// Current minimum retention; an empty result means none is set
db.getSiblingDB("admin").serverStatus().oplogTruncation.oplogMinRetentionHours

Interpretation: a value set at runtime with replSetResizeOplog overrides the startup setting until the next restart.

How do I resize the oplog safely on every member?

replSetResizeOplog changes only the member you run it on. The change oplog size procedure runs it on each secondary, then the primary, with no restart, as a user with a role such as clusterManager or clusterAdmin.

Resize the oplog on a three-member replica set

Eight steps; step 5 briefly locks the primary's oplog, and step 8 is only for shrinking

  1. Record the size and window on every member

    Connect to each member directly and keep the numbers in the change ticket.

    db.getSiblingDB("local").oplog.rs .stats(1024 * 1024).maxSize rs.printReplicationInfo()
  2. Check free disk on every member

    Plan for the full uncompressed size until you have measured the on-disk ratio.

    df -h /var/lib/mongo
  3. Resize the first secondary

    The size is in megabytes and must be a double, so cast it with Double() in mongosh.

    db.adminCommand({ replSetResizeOplog: 1, size: Double(276480) })
  4. Resize the other secondaries

    One at a time, including hidden and delayed members. Confirm maxSize on each.

  5. Resize the primary last

    Brief oplog lock

    The command holds an exclusive lock on the oplog, blocking new writes, until it finishes. Run it in a quiet minute.

    db.adminCommand({ replSetResizeOplog: 1, size: Double(276480) })
  6. Set a minimum retention period, if you use one

    Also per member, and only with a disk-space alert in place.

    db.adminCommand({ replSetResizeOplog: 1, minRetentionHours: 36 })
  7. Write the values into mongod.conf

    A resynced member creates its oplog from oplogSizeMB, and a runtime retention value is lost at restart without the file setting.

  8. Shrinking only: compact oplog.rs on a secondary

    Member stops syncing

    A smaller size does not return disk space. Compact blocks oplog sync on that member, so never run it on the primary.

    db.getSiblingDB("local") .runCommand({ compact: "oplog.rs" })

Rollback point: growing the oplog can be undone with another resize. Shrinking deletes the oldest entries at once, which can invalidate change streams and leave syncing members stale, and nothing brings them back.

yaml
# /etc/mongod.conf excerpt after the change
replication:
  replSetName: rs0
  oplogSizeMB: 276480
storage:
  dbPath: /var/lib/mongo
  oplogMinRetentionHours: 36

Interpretation: oplogSizeMB does nothing on a member that already has an oplog; it sizes the next member you build or resync, which would otherwise get the 5% default. Keep sizes equal on all members, and on a sharded cluster repeat this on every shard and the config server replica set.

How does oplog sizing work on MongoDB Atlas?

Atlas does not support replSetResizeOplog. As of November 2026, Atlas additional settings and Atlas storage describe three cases.

  • Atlas Core with storage auto-scaling (the default). Atlas manages the oplog by a minimum oplog window (oplogMinRetentionHours), 24 hours by default. Atlas states that storage auto-scaling needs a window of up to 60 seconds per GB of configured disk, about 8.3 hours on 500 GB, and alerts when scaling is blocked by insufficient oplog size.
  • Atlas Core with a fixed size. Set Oplog Size needs storage auto-scaling off and the minimum window at 0. The minimum is 990 MB, uncompressed, and Atlas rejects a size that leaves less than 25% of the disk free.
  • Atlas Infinite (public preview). Replication runs in the storage layer, so nodes cannot fall behind the oplog window. The oplog still serves change streams, and its retention sets how far back they can resume. You cannot set its size.
bash
# Atlas CLI: 48-hour minimum oplog window on an Atlas Core cluster (M10+)
atlas clusters advancedSettings update Cluster0 \
  --projectId <projectId> \
  --oplogMinRetentionHours 48

Interpretation: confirm the change in the Replication Oplog Window chart. Atlas alerts on Replication Oplog Window, Oplog Data Per Hour, and Replication Headroom, and its alert guide suggests an oplog above the peak of the Oplog GB / Hour chart.

What should I alert on?

These thresholds are our starting points, not MongoDB defaults.

SignalSourceSuggested threshold
Oplog window, every membertimeDiffHoursWarn below the target; page below the longest single requirement
Headroom per secondarySource window minus lagPage below 25% of the target
Replication lagrs.status() optimesWarn above 60 seconds for 5 minutes
Oplog write rateHourly aggregation or Oplog GB / HourWarn at twice the usual peak hour
Member statestateStrPage if RECOVERING or STARTUP2 for 15 minutes
Free disk with retention setFilesystemWarn below 25% free
javascript
// One line per run for a monitoring agent, on every member
const r = db.getReplicationInfo()
printjson({ host: db.hello().me, windowHours: r.timeDiffHours,
            usedMB: r.usedMB, logSizeMB: r.logSizeMB })

Interpretation: each member has its own window, so collect from all of them. Do not shrink the oplog to free disk: only the oplog can reuse that space. Add disk instead.

Frequently asked questions

How long should a MongoDB oplog window be?

Long enough to cover your longest secondary downtime, logical initial sync, change stream consumer outage, and mongodump run with the oplog option, plus a margin. MongoDB's troubleshooting guide says at a minimum 24 hours of operations, and notes that many users prefer 72 hours or a week. Atlas Core clusters with storage auto-scaling keep 24 hours by default.

Does replSetResizeOplog need a restart?

No. replSetResizeOplog resizes the oplog of a running member, and the new size persists after a restart. It affects only the member you run it on, so run it on each secondary and then the primary. It holds an exclusive lock on the oplog while it runs, and MongoDB Atlas does not support it.

What happens when a secondary falls off the oplog?

The entry it needs next has been deleted on its sync source, so it cannot catch up by replication. The member enters RECOVERING, logs that it is too stale to use the sync source, and needs manual intervention: a resync by initial sync from an empty data directory, or from a recent copy of another member's data files.

Key takeaways

  • The oplog window is size divided by write rate. Measure both on every member, using the busiest hour, not the average.
  • Bulk updates, transactions, TTL and purge deletes, and bulk loads fill the oplog without growing your data.
  • Set the target window from your longest requirement, then size for the worst stretch of that length.
  • Resize secondaries first and the primary last, keep sizes equal, and write the values into mongod.conf.
  • A minimum retention period protects the window but not the disk. On Atlas, set the minimum oplog window instead.

Size your MongoDB oplog with JusDB

JusDB's MongoDB performance tuning team can measure your oplog window and write churn on every member, find the jobs that fill it, and plan a resize and alert thresholds that match your maintenance, initial sync, and change stream needs.

Bring your topology, versions, current rs.printReplicationInfo() output, and your batch job schedules. Your team approves every production change.

Share this article

Database engineering notes

Articles like this one, in your inbox. No spam, unsubscribe anytime.

JusDB Team

Official JusDB content team

Keep reading

Moving a Multi-Database Valkey Instance into a 9.x Cluster

Move a Valkey instance that uses SELECT 1 to 15 into a 9.x cluster: keep or remap databases, copy each one, verify counts per database, and keep a way back.

Redis18 minNov 10, 2026
Read

Valkey Keyspace Notifications in Production: Expired Events, hexpired and Cluster Mode

Run Valkey keyspace notifications in production: why expired and hexpired events lag the TTL, how to subscribe on every cluster node, and when a stream is safer.

Redis20 minNov 10, 2026
Read

Valkey GLIDE in Production: AZ-Affinity Reads and Migrating from Legacy Clients

Configure Valkey GLIDE AZ-affinity reads on servers and clients, learn what GLIDE 2.5.3 does not support, and migrate from redis-py, Jedis, ioredis, or go-redis one service at a time.

Redis20 minNov 10, 2026
Read