A MongoDB node's p99 latency jumps from milliseconds to seconds while CPU sits half idle. In db.serverStatus().wiredTiger.cache, bytes currently in the cache is pinned near maximum bytes configured, the dirty share is close to 20%, and page evict attempts by application threads climbs on every sample. On MongoDB 7.0, 8.0, and 9.0 this means the same thing: WiredTiger hit its eviction trigger and made your query threads evict pages before they can continue. This guide covers cache sizing, the eviction thresholds, the metrics, and when to choose a bigger cache over a better query or index.
- The default WiredTiger cache is the larger of 50% of (RAM minus 1 GB) or 0.256 GB, calculated from
hostInfo.system.memLimitMB. In containers, setcacheSizeGB(orcacheSizePcton 8.2 and later) explicitly. - Eviction worker threads start at 80% full or 5% dirty. Application threads are pulled into eviction at 95% full, 20% dirty, or 10% of the cache in updates. That is the latency stall.
- Watch the fill, dirty, and update ratios together with the rate of application-thread eviction. A full cache alone is normal.
- MongoDB recommends against raising the cache above the default. Fix collection scans, missing indexes, and long transactions first.
- Do not change eviction settings through
wiredTigerEngineRuntimeConfigor source-only parameters unless MongoDB Support asks you to.
Scope and review date: November 2026. This post covers self-managed MongoDB 7.0, 8.0, and 9.0 replica set members and shards on Linux, including containers. Defaults, metric names, and parameters were checked on 9 November 2026 against the MongoDB 9.0 manual, the 7.0 to 9.0 release notes, the WiredTiger documentation, and the MongoDB source for 7.0.0, 8.0.34, and 9.0.4. The commands were not run in a lab, so try them on a staging member first.
How does MongoDB use memory with WiredTiger?
MongoDB uses two caches. The WiredTiger internal cache holds uncompressed pages of collections and indexes, and it is the only place a query reads or changes data. The operating system filesystem cache sits below it and holds data file blocks in their on-disk, compressed form. The WiredTiger storage engine page says MongoDB "automatically uses all free memory that is not used by the WiredTiger cache or by other processes" for the filesystem cache.
Compression is why the second cache matters. By default, collections use snappy block compression, time series collections use zstd, and indexes use prefix compression. One gigabyte of filesystem cache can therefore hold more documents than one gigabyte of WiredTiger cache. A page that falls out of WiredTiger but is still in the filesystem cache costs a decompression, not a disk read.
How a read reaches WiredTiger, and how eviction frees space
Follow steps 1 to 3 for a cache miss; step 4 is eviction, which application threads join past the trigger
The dashed path is the stall: once the cache passes 95% full or 20% dirty, the thread running your query must evict pages before it can continue, so eviction cost shows up as query latency.
How big is the WiredTiger cache by default?
The configuration file reference gives the formula: the larger of 50% of (RAM minus 1 GB), or 0.256 GB. A 16 GB server gets a 7.5 GB cache, and a 64 GB server gets 31.5 GB. The default assumes a single mongod per machine. Other memory consumers, including the filesystem cache, connections, index builds, and query execution, live outside this number.
"RAM" here means the value MongoDB reports as hostInfo.system.memLimitMB. The hostInfo reference describes it as the memory usage limit, which can be lower than total system memory in a container, and says this limit "is used as the maximum RAM available to calculate WiredTiger internal cache."
// mongosh, connected directly to the member you are checking
const sys = db.hostInfo().system;
const max = Number(db.serverStatus().wiredTiger.cache["maximum bytes configured"]);
({
memSizeMB: sys.memSizeMB,
memLimitMB: sys.memLimitMB,
cacheMB: Math.round(max / 1048576)
})Interpretation: on a 64 GB virtual machine with no container limit, memLimitMB equals memSizeMB, and cacheMB is about 32,256 (31.5 GB). If memLimitMB shows the host's memory while the process runs under a smaller limit, the cache was sized for memory the process cannot have.
How do you size the WiredTiger cache in Docker or Kubernetes?
Set it explicitly. The WiredTiger page warns that "WiredTiger may not account for the memory limits of the specific container in certain cases", and the configuration reference says that a mongod in a container without access to all system RAM must have cacheSizeGB or cacheSizePct set below the container's memory. A cache sized from host RAM inside a smaller cgroup limit risks the kernel's out-of-memory killer under load.
# Inside the container: the limit the kernel enforces
cat /sys/fs/cgroup/memory.max # cgroup v2
cat /sys/fs/cgroup/memory/memory.limit_in_bytes # cgroup v1Interpretation: compare this with memLimitMB from the previous section. max on cgroup v2 means no limit is set. If the two disagree, trust the cgroup value and size the cache from it.
For a container with an 8 GiB limit, applying the default formula to the limit gives 3.5 GB. Leave headroom for connections, aggregation memory, and index builds inside the same limit.
# mongod.conf for a container with an 8 GiB memory limit
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 3.5Interpretation: the value accepts decimals between 0.256 and 10000. The command-line form, --wiredTigerCacheSizeGB 3.5, also works as an argument to the official Docker image. After start, confirm maximum bytes configured is 3.5 GB.
cacheSizePct on MongoDB 8.2 and later
The MongoDB 8.2 release notes add storage.wiredTiger.engineConfig.cacheSizePct (--wiredTigerCacheSizePct), which sizes the cache as a percentage of memory, up to 80%. You can set either cacheSizeGB or cacheSizePct, not both, and the 8.0 manual does not document the percentage option, so 8.0 deployments keep cacheSizeGB.
The manual describes the percentage base both as physical RAM and as available memory, so confirm the result inside a container by reading maximum bytes configured after the restart.
What are the WiredTiger eviction thresholds?
WiredTiger keeps the cache below its maximum by evicting pages. The WiredTiger cache and eviction tuning guide defines two levels for each kind of pressure: a target, where background eviction worker threads start, and a trigger, where application threads are made to evict as well. MongoDB does not override these values at startup unless you set its eviction parameters, so the WiredTiger defaults apply. We checked the defaults in the WiredTiger configuration source bundled with MongoDB 9.0.4, 8.0.34, and 7.0.0.
| Setting | Default | What happens when it is reached |
|---|---|---|
eviction_target | 80% of the cache | Eviction worker threads start removing pages |
eviction_trigger | 95% | Application threads also evict, which adds latency to operations |
eviction_dirty_target | 5% | Worker threads start writing out modified pages |
eviction_dirty_trigger | 20% | Application threads are throttled to evict dirty data |
eviction_updates_target | Half the dirty target (2.5%) | Worker threads target pages holding update chains |
eviction_updates_trigger | Half the dirty trigger (10%) | Application threads evict pages holding updates |
At 100% full, the WiredTiger guide says operations stall. MongoDB starts WiredTiger with four eviction worker threads (threads_min=4,threads_max=4) in the 7.0 and 8.0 source, and the 9.0.4 source exposes the same default of 4 as server parameters.
The dirty limit also shows up in MongoDB's own documentation. The server parameters reference says, under transactionTooLargeForCacheThreshold, that "the dirty cache is limited to 20% of the total cache size". Under cache pressure, a large transaction can fail with TransactionTooLargeForCache, and a single write can fail with TemporarilyUnavailable after up to 10 internal retries (temporarilyUnavailableMaxRetries). Either error in your application logs points to this section.
Healthy and eviction-stressed WiredTiger metrics
Illustrative values for one replica set member with a 31.5 GB cache, not measurements; switch tabs to compare
Healthy
| Metric | Healthy |
|---|---|
| Cache used (% of maximum) | 79%, held near the 80% target |
| Dirty (% of maximum) | 2% to 5% |
| Updates (% of maximum) | Under 2.5% |
| Application-thread evict attempts | Flat between samples |
| Application time waiting for cache | Flat between samples |
| Most recent checkpoint | A few seconds |
Eviction-stressed
| Metric | Eviction-stressed |
|---|---|
| Cache used (% of maximum) | 95%, pinned at the trigger |
| Dirty (% of maximum) | 19% to 20%, at the dirty trigger |
| Updates (% of maximum) | Near 10% |
| Application-thread evict attempts | Thousands more on every sample |
| Application time waiting for cache | Rising on every sample |
| Most recent checkpoint | Tens of seconds, with long scrub time |
Read the rates, not the levels: 80% full is the design point. The signal is application-thread eviction that grows between samples, together with a dirty or update ratio at its trigger.
How do you read db.serverStatus().wiredTiger.cache?
Start with three ratios against maximum bytes configured: bytes in the cache, tracked dirty bytes, and bytes allocated for updates. This is the same arithmetic mongostat uses for its used and dirty columns. WiredTiger statistics are 64-bit integers, which mongosh can return as Long values, so wrap them in Number() before doing arithmetic.
// mongosh: cache fill, dirty, and update ratios
const c = db.serverStatus().wiredTiger.cache;
const max = Number(c["maximum bytes configured"]);
const pct = (k) => (100 * Number(c[k]) / max).toFixed(1) + "%";
({
cacheGB: (max / 1024 ** 3).toFixed(1),
used: pct("bytes currently in the cache"),
dirty: pct("tracked dirty bytes in the cache"),
updates: pct("bytes allocated for updates")
})Interpretation: compare used with 80% and 95%, dirty with 5% and 20%, and updates with 2.5% and 10%. Values that sit at a target are normal. Values at a trigger mean application threads are evicting.
Cache ratios on a member under write pressure
Illustrative output of the ratio script above, shaped like mongosh on MongoDB 9.0; not a measured result
All three at their triggers: with fill at 95%, dirty at 20%, and updates near 10%, application threads are evicting. The next question is which operations are making the dirty data.
Is eviction landing on application threads?
Counters show what the pressure costs, so sample them twice and compare. The serverStatus reference warns that WiredTiger statistics change between releases, and these have. In 7.0 the counter is pages evicted by application threads and the eviction time sits in thread-yield. From 8.0 they are page evict attempts by application threads and application thread time evicting (usecs) in cache. The script reads either.
// mongosh: per-second rates over 60 seconds, MongoDB 7.0 to 9.0
function sample() {
const wt = db.serverStatus().wiredTiger, c = wt.cache, y = wt["thread-yield"] || {};
return {
t: Date.now(),
appEvict: Number(c["page evict attempts by application threads"] ?? c["pages evicted by application threads"]),
appEvictUs: Number(c["application thread time evicting (usecs)"] ?? y["application thread time evicting (usecs)"]),
waitCacheUs: Number(y["application thread time waiting for cache (usecs)"]),
pagesRead: Number(c["pages read into cache"]),
pagesWritten: Number(c["pages written from cache"])
};
}
const a = sample(); sleep(60000); const b = sample();
const secs = (b.t - a.t) / 1000;
Object.fromEntries(Object.keys(a).filter((k) => k !== "t")
.map((k) => [k + "PerSec", Math.round((b[k] - a[k]) / secs)]))Interpretation: appEvictUsPerSec is microseconds of eviction work per second, spread across all application threads. Near zero is healthy. A value in the hundreds of thousands means query threads together spend a large share of each second evicting. A rising waitCacheUsPerSec is the same stall seen from the waiting side.
How do checkpoints affect dirty data in the cache?
MongoDB has WiredTiger write a checkpoint, a consistent snapshot of data on disk, every 60 seconds (storage.syncPeriodSecs, which the configuration reference says not to change in production). The WiredTiger tuning guide notes that the dirty eviction settings "control how much work checkpoints have to do in the worst case". When a checkpoint starts with a lot of dirty data, it first waits for eviction to bring the dirty level down (the scrub phase), and that competes with the workload.
// mongosh: checkpoint health. 8.0 and later report a "checkpoint" section;
// 7.0 keeps the same counters in "transaction".
const wt = db.serverStatus().wiredTiger;
wt.checkpoint ? {
running: Number(wt.checkpoint["progress state"]) !== 0,
lastMs: Number(wt.checkpoint["most recent time (msecs)"]),
maxMs: Number(wt.checkpoint["max time (msecs)"]),
lastScrubMs: Number(wt.checkpoint["scrub most recent time (msecs)"])
} : {
running: Number(wt.transaction["transaction checkpoint currently running"]) !== 0,
lastMs: Number(wt.transaction["transaction checkpoint most recent time (msecs)"]),
maxMs: Number(wt.transaction["transaction checkpoint max time (msecs)"])
}Interpretation: a checkpoint that takes most of the 60-second interval, or a long scrub time, means the disk cannot absorb the write rate. More cache will not fix that. It gives the next checkpoint more dirty data to write. Look at write volume, disk throughput, and journal and data file placement first.
WiredTiger keeps older versions of documents for as long as an open transaction or snapshot might read them. That makes pages harder to evict, and the update ratio climbs while the long reader runs. MongoDB 8.3 and later abort the oldest transaction when they detect cache pressure for this reason. Check db.currentOp() for long-running operations and transactions before you blame the cache size.
Did read and write tickets change in MongoDB 7.0 and later?
Yes. The MongoDB 7.0 release notes introduced a default algorithm that "dynamically adjust[s] the maximum number of concurrent storage engine transactions" (the read and write tickets). The WiredTiger page says the value "never exceeds 128 read tickets and 128 write tickets". The parameter reference says that changing storageEngineConcurrentReadTransactions or storageEngineConcurrentWriteTransactions "can cause performance issues or errors" and points you to MongoDB Support first.
That changes how you read the metrics. From 7.0, "a concurrent storage engine transaction (ticket) availability of 0 for a prolonged period of time does not indicate an overload". A large queue that persists does.
// MongoDB 8.0 and later (the section was wiredTiger.concurrentTransactions in 7.0).
// The queue length sits under normalPriority in 8.0 and 9.0.
const q = db.serverStatus().queues.execution;
["read", "write"].map((k) => ({
queue: k,
out: q[k].out, available: q[k].available, totalTickets: q[k].totalTickets,
queued: q[k].normalPriority?.queueLength ?? q[k].queueLength
}))Interpretation: a queued value that stays high across samples is the overload signal. totalTickets moving up and down is the algorithm at work. On 9.0, the new throughputProbingStallEscalationThreshold parameter (default 3) forces concurrency up after three consecutive probing intervals with zero throughput and operations waiting.
How do you estimate the working set?
The working set is the data and indexes your workload touches in a normal period. The diagnostics FAQ answers "Must my working set size fit RAM?" with "No". WiredTiger evicts to make room. It only gets expensive when the hot set is so much larger than the cache that the same pages are read in and evicted again and again. Indexes matter most, because an index page missing from the cache can cost several page reads to rebuild.
// Size of one collection and its indexes, and how much of it is in the cache now
db.orders.aggregate([
{ $collStats: { storageStats: { scale: 1048576 } } },
{ $project: {
dataMB: "$storageStats.size",
indexMB: "$storageStats.totalIndexSize",
inCacheMB: { $round: [ { $divide: [
"$storageStats.wiredTiger.cache.bytes currently in the cache", 1048576 ] }, 0 ] }
} }
])Interpretation: dataMB is uncompressed data size, which is the form it takes in the WiredTiger cache. The $collStats scale factor does not apply to fields with "bytes" in the name, hence the manual division. If the indexes for your hot query paths alone exceed the cache, that is a capacity or design problem, not a tuning one.
To find the operations that pay for cache pressure, the database profiler and the slow query log record storage.data.bytesRead (bytes read from disk into the cache) and storage.timeWaitingMicros.cache (time waiting for space in the cache) for each operation.
// With profiling at level 1 on the affected database
db.system.profile.find(
{ "storage.timeWaitingMicros.cache": { $gt: 0 } },
{ ns: 1, op: 1, millis: 1, planSummary: 1, docsExamined: 1,
"storage.data.bytesRead": 1, "storage.timeWaitingMicros.cache": 1 }
).sort( { ts: -1 } ).limit(10)Interpretation: operations with COLLSCAN in planSummary, a large docsExamined, and many megabytes of bytesRead pull cold pages through the cache and push other data out. These are usually the cause, not the victims.
Should you resize the cache or fix queries and indexes?
Fix the workload first in most cases. The configuration reference says to "avoid increasing the WiredTiger internal cache size above its default value". Memory added to the WiredTiger cache comes out of the filesystem cache, which holds the same data compressed, and out of headroom for connections, query memory, and index builds, which all live outside the cache.
// Confirm a suspect from the profiler before changing memory
db.orders.find( { customerId: 42, status: "open" } )
.explain("executionStats").executionStatsInterpretation: if totalDocsExamined or totalKeysExamined is far above nReturned, the query reads pages it does not need. An index that matches the filter and sort removes that read volume and the eviction it causes. The MongoDB indexing guide covers the equality, sort, and range (ESR) order for compound indexes.
A larger cache is the right answer only when selective queries still need a hot set larger than the cache. Even then, adding memory to the instance, or sharding, and letting the default formula grow the cache keeps the filesystem cache in proportion.
Runbook: diagnose cache pressure on one member
Seven steps; steps 1 to 4 only read metrics, and step 7 needs a rolling restart
-
Confirm the cache size the node got
Compare memLimitMB with the container limit and with maximum bytes configured.
db.hostInfo().system.memLimitMB -
Measure fill, dirty, and update ratios
Targets are 80%, 5%, and 2.5%; triggers are 95%, 20%, and 10%.
db.serverStatus().wiredTiger.cache -
Check application-thread eviction
Sample twice, 60 seconds apart. A rising rate confirms the stall.
-
Check checkpoint duration
Checkpoints near 60 seconds point at disk write capacity, not cache size.
db.serverStatus().wiredTiger.checkpoint -
Find the operations waiting for cache
Use the profiler or slow log fields for cache wait and bytes read, and look for long transactions.
db.setProfilingLevel(1, { slowms: 100 }) -
Fix the worst query, index, or transaction
Compare docs examined with docs returned, then measure the ratios again.
-
Resize only if the working set is larger
Grow the instance or container first, so the filesystem cache keeps its share.
-
Apply the new size, one member at a time
Rolling restartSet cacheSizeGB in mongod.conf, restart secondaries first, then step down and restart the primary.
sudo systemctl restart mongod
Main risk: a cache sized past the memory the process can use moves the problem from slow queries to an out-of-memory kill. Recheck memLimitMB after every resize.
The MongoDB source defines eviction parameters such as wiredTigerEvictionDirtyTargetGB and, in 9.0, wiredTigerEvictionThreadsMax, but the manual does not document them. The documented route, wiredTigerEngineRuntimeConfig, carries a warning to avoid it "unless under the direction from MongoDB engineers". Treat all of them as support-directed changes, tested on staging under production-like load.
Frequently asked questions
What is the default WiredTiger cache size in MongoDB?
The default is the larger of 50% of (RAM minus 1 GB) or 0.256 GB. A 16 GB server gets 7.5 GB, and a 64 GB server gets 31.5 GB. MongoDB uses the memory limit reported in hostInfo.system.memLimitMB as RAM, which can be a container limit. The default assumes one mongod per machine, so reduce it when several instances share a host.
Why does MongoDB get slow when the WiredTiger cache is full?
A full cache alone is normal, because background eviction keeps it near 80%. Slowness starts when the cache passes 95% full, or dirty data reaches 20% or updates reach 10% of the cache. At that point WiredTiger makes application threads evict pages before they continue, so query latency includes eviction work. Long transactions and collection scans are common causes.
Should I increase cacheSizeGB to fix MongoDB performance?
Usually not first. MongoDB's documentation advises against raising the WiredTiger cache above its default. Memory moved into the cache comes out of the filesystem cache and other process memory. Check for collection scans, missing indexes, and long transactions first. Raise the cache only when selective queries still need a working set larger than the current cache.
How do I set the WiredTiger cache size in Docker or Kubernetes?
Set storage.wiredTiger.engineConfig.cacheSizeGB, or the --wiredTigerCacheSizeGB option, to a value below the container memory limit. On MongoDB 8.2 and later you can use cacheSizePct instead. MongoDB documents that WiredTiger may not detect a container limit in every case, so confirm the result with hostInfo and serverStatus after the container starts.
Key takeaways
- The default cache is the larger of 50% of (RAM minus 1 GB) or 0.256 GB, calculated from
memLimitMB. Set it explicitly in containers. - Background eviction starts at 80% full and 5% dirty. Application threads join at 95% full, 20% dirty, or 10% updates, and that is where latency appears.
- Read rates between samples: application-thread eviction, time waiting for cache, pages read into cache, and checkpoint duration.
- On 7.0 and later, zero available tickets is not an overload signal. Persistent queues are.
- Fix scans, indexes, and long transactions before you resize. Leave eviction internals alone unless MongoDB Support directs the change.
Tune MongoDB cache pressure with JusDB
JusDB's MongoDB performance tuning team can review your serverStatus output, slow query logs, profiler data, and diagnostic data (FTDC). We find whether eviction stalls come from cache size, container limits, checkpoints, or specific queries and indexes, and we plan the fixes in order.
Bring your topology, versions, instance or container memory limits, and a time window when latency was high. Your team approves every production change.