Performance Tuning
Most MongoDB performance problems trace back to one of a handful of root causes: the working set doesn't fit in RAM, a hot query has no supporting index, or the connection pool is misconfigured. This page walks through the diagnostic tools and the fixes they point to.
Working Set in RAM
Your "working set" is the data and indexes actively touched by your queries. As long as it fits in the WiredTiger cache (roughly 50% of available RAM minus 1GB, by default), reads are served from memory. Once it doesn't fit, MongoDB has to page data in from disk, and latency climbs.
db.serverStatus().wiredTiger.cache["bytes currently in the cache"] db.serverStatus().wiredTiger.cache["maximum bytes configured"] db.serverStatus().wiredTiger.cache["tracked dirty bytes in the cache"]
WiredTiger Cache Tuning
# mongod.conf — explicitly size the cache (leave headroom for the OS
# filesystem cache and other processes on the host)
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 8The Slow Query Log / Profiler
// Level 1: log only operations slower than slowms (default 100ms)
db.setProfilingLevel(1, { slowms: 100 })
// Level 2: log EVERY operation — useful briefly for deep debugging,
// too much overhead to leave on in production
db.setProfilingLevel(2)
// Level 0: off (default)
db.setProfilingLevel(0)
db.system.profile.find().sort({ ts: -1 }).limit(20).pretty()Profiling Level | Behavior |
|---|---|
0 | Profiler off — no logging |
1 | Logs operations slower than |
2 | Logs every operation regardless of duration |
system.profile (a capped collection) with noise, pushing out the slow-query entries you actually care about.currentOp — What's Running Right Now
db.currentOp({ "secs_running": { $gte: 5 } }) // ops running 5+ seconds
// Kill a specific runaway operation once you've identified it
db.killOp(opid)Index Audit
Unused indexes still cost write overhead (every insert/update maintains every index) and disk space, without ever paying off in faster reads. $indexStats shows real per-index usage counts since the server last restarted.
db.orders.aggregate([{ $indexStats: {} }])[
{ name: "_id_", accesses: { ops: 452301 } },
{ name: "customerId_1_status_1", accesses: { ops: 98211 } },
{ name: "legacyField_1", accesses: { ops: 0 } }
]legacyField_1 above has zero accesses — a strong candidate to drop, after confirming it isn't a safety net for an infrequent but important query.
db.orders.dropIndex("legacyField_1")Explaining a Query
db.orders.find({ customerId: id, status: "shipped" })
.sort({ createdAt: -1 })
.explain("executionStats")totalDocsExaminedmuch larger thannReturned→ missing or wrong index.stage: "COLLSCAN"in the winning plan → no usable index at all for this query shape.totalDocsExamined: 0with results returned → a fully covered query (index alone satisfied it).executionTimeMillisclimbing over time on the same query shape → working set or data volume outgrowing current indexes.
Schema Fixes for Hot Queries
Add a compound index following the ESR rule (Equality, Sort, Range) for the query's exact shape.
Project only the fields the caller needs — smaller documents over the wire, less memory pressure.
Denormalize/duplicate a couple of frequently-read fields (extended reference pattern) to avoid a $lookup on the hottest path.
Precompute expensive aggregates (computed pattern) instead of recalculating them on every read.
Cap or bucket unbounded arrays so a single document read doesn't pull megabytes of data you don't need.
Connection Pool Sizing
new MongoClient(uri, {
maxPoolSize: 50, // upper bound on concurrent connections per client
minPoolSize: 10, // kept warm even when idle, avoids cold-connect latency spikes
maxIdleTimeMS: 60000
})Hardware / Atlas Tier Signals
Signal | Suggests |
|---|---|
Sustained high WiredTiger cache eviction rate | Need more RAM or a smaller working set |
High disk IOPS/latency, cache otherwise healthy | Storage tier undersized — faster disk or Atlas tier upgrade |
CPU pegged with routine, well-indexed queries | Need more vCPU — scale up, or shard to spread load |
Connections near the limit under normal traffic | Application-side connection leak or too many separate services sharing the cluster |
Replication lag growing under write load | Secondaries undersized relative to primary write volume |
Summary
Check whether the working set fits in the WiredTiger cache first — many performance problems are really a memory problem.
Use the profiler and currentOp to find slow/long-running operations in real time.
Use $indexStats to find and remove indexes that cost write overhead without ever being read from.
Read explain() output for hot queries and fix the index (ESR rule) before reaching for more hardware.
Size the connection pool from measured concurrency, and watch cache eviction, disk IOPS, CPU, and replication lag as your leading scaling signals.