How to Diagnose MongoDB Performance Issues: A Complete Guide
MongoDB performance problems rarely announce themselves clearly. You notice slowdowns, timeouts, or high CPU, but pinpointing the root cause requires systematic investigation. This guide walks you through MongoDB's built-in diagnostic tools — what to run, what to look for, and what to do about what you find.
Where to Start: The Diagnostic Toolkit
MongoDB ships with several built-in commands for performance analysis. Unlike Oracle's AWR or PostgreSQL's pg_stat_statements, MongoDB's diagnostics are spread across multiple commands — each revealing a different layer of the system.
| Command | What it shows | When to use |
|---|---|---|
db.serverStatus() |
Server-wide metrics, connections, opcounters, cache | Always — the baseline |
db.currentOp() |
Currently running operations | During an active incident |
mongostat |
Real-time operation rates | Live monitoring |
mongotop |
Read/write time per collection | Finding hot collections |
db.system.profile.find() |
Historical slow queries | Post-incident analysis |
rs.status() |
Replica set health and lag | Replication problems |
The general approach: start with serverStatus and mongostat to understand the overall workload, then drill into currentOp during an active incident, and use the profiler for post-mortem analysis.
Step 1: Check the Server Status
db.serverStatus() is your first stop. It gives a complete snapshot of what MongoDB is doing right now.
db.serverStatus()
The output is large. Focus on these sections:
Connections
db.serverStatus().connections
// {
// current: 342,
// available: 158,
// totalCreated: 12847
// }
What to look for: current approaching current + available (the total). MongoDB's default maxIncomingConnections is 1,000,000 on Linux, but application connection pools can exhaust available connections much sooner.
If available is dropping toward zero, your application has a connection leak or is not pooling connections correctly.
WiredTiger Cache
db.serverStatus().wiredTiger.cache
Key metrics: - "bytes currently in the cache" vs "maximum bytes configured" — how full the cache is - "tracked dirty bytes in the cache" — data modified but not yet written to disk - "pages evicted by application threads" — if this is high, the cache is under pressure
What to look for: dirty bytes exceeding 20% of the cache size. WiredTiger keeps dirty pages in cache until they're written to disk. When dirty pages exceed the threshold (default 20%), application threads start helping with eviction — causing latency spikes.
const cache = db.serverStatus().wiredTiger.cache;
const dirtyPct = cache["tracked dirty bytes in the cache"] / cache["maximum bytes configured"] * 100;
print("Dirty cache %:", dirtyPct.toFixed(1));
Global Lock Queue
db.serverStatus().globalLock.currentQueue
// { total: 0, readers: 0, writers: 0 }
What to look for: any value above zero. A non-zero queue means operations are waiting for locks. If this number is consistently above zero, you have lock contention — usually caused by long-running write operations blocking reads.
Operation Counters
db.serverStatus().opcounters
// { insert: 1234, query: 45678, update: 892, delete: 23, getmore: 12, command: 3456 }
These are cumulative since server start. To get rates per second, sample twice with a known interval:
const s1 = db.serverStatus().opcounters;
sleep(10000); // 10 seconds
const s2 = db.serverStatus().opcounters;
print("Queries/s:", (s2.query - s1.query) / 10);
print("Updates/s:", (s2.update - s1.update) / 10);
Scan and Order (sorts without index)
db.serverStatus().metrics.operation.scanAndOrder
This counter increments every time MongoDB performs an in-memory sort because no suitable index exists. Any non-zero value during normal operations warrants investigation.
Step 2: Real-Time Monitoring with mongostat
mongostat gives you a rolling view of MongoDB's activity, similar to iostat for disk I/O.
mongostat --rowcount 60 1
This samples every 1 second for 60 seconds. Save the output to a file during an incident:
mongostat --rowcount 300 1 > mongostat_$(date +%Y%m%d_%H%M%S).txt
Reading mongostat Output
insert query update delete getmore command dirty used flushes vsize res qrw arw net conn
*0 *0 *0 *0 0 1|0 0.1% 58.4% 0 1.00g 224m 0|0 0|0 79k|20k 52
15 234 8 2 0 45|0 2.3% 61.2% 1 1.00g 231m 0|0 0|0 1.2m|45k 52
Key columns:
| Column | Meaning | Alert threshold |
|---|---|---|
dirty |
WiredTiger dirty cache % | > 20% |
used |
WiredTiger cache used % | > 95% |
qrw |
Queue depths: readers|writers | Any > 0 |
arw |
Active readers|writers | Context-dependent |
conn |
Current connections | Near your pool limit |
Pattern to watch: dirty steadily increasing means your workload is writing faster than WiredTiger can flush. If dirty crosses 20%, expect eviction pressure and latency spikes.
Step 3: Find Hot Collections with mongotop
mongotop shows which collections are consuming the most read and write time, sampled over an interval.
mongotop 5
This samples every 5 seconds. Example output:
ns total read write
2024-01-15T10:00:05+0000
mydb.orders 145ms 140ms 5ms
mydb.events 89ms 12ms 77ms
mydb.sessions 34ms 34ms 0ms
admin.local 2ms 2ms 0ms
What to look for:
- Collections with consistently high write time — potential hotspots from high insert/update rates without appropriate indexing
- Collections with high read time relative to expected query volume — could indicate full collection scans (COLLSCAN)
- Sudden spikes in a collection that's normally quiet
Save output during an incident:
mongotop 2 30 > mongotop_$(date +%Y%m%d_%H%M%S).txt
Step 4: Investigate Active Operations
During an active slowdown, db.currentOp() shows you exactly what's running right now.
// Show only active operations (not idle connections)
db.currentOp({ active: true })
// Show operations running longer than 5 seconds
db.currentOp({ active: true, secs_running: { $gt: 5 } })
// Show operations waiting for locks
db.currentOp({ waitingForLock: true })
Reading currentOp Output
{
"inprog": [
{
"opid": 12345,
"op": "query",
"ns": "mydb.orders",
"secs_running": 47,
"planSummary": "COLLSCAN",
"waitingForLock": false,
"client": "10.0.1.5:52341",
"query": { "status": "pending", "amount": { "$gt": 1000 } }
}
]
}
Critical signals:
- planSummary: "COLLSCAN" — the query is scanning the entire collection without an index. This is almost always the cause of slow queries.
- secs_running > 30 — long-running operations consuming resources
- waitingForLock: true — the operation is blocked by another operation
Killing a Long-Running Operation
If a runaway query is blocking others:
db.killOp(12345)
Use with caution in production — only kill operations you are certain are safe to terminate.
Step 5: Find Slow Queries with the Profiler
The MongoDB profiler records queries that exceed a threshold duration. Enable it to capture slow queries for post-incident analysis.
// Check current profiler status
db.getProfilingStatus()
// Enable profiling for queries taking longer than 100ms
db.setProfilingLevel(1, { slowms: 100 })
// Enable profiling for ALL queries (development only — high overhead)
db.setProfilingLevel(2)
Important: Level 2 profiling has significant performance overhead. Only use it briefly in production.
Query the profiler:
// Most recent slow queries
db.system.profile.find().sort({ ts: -1 }).limit(20).pretty()
// Queries on a specific collection
db.system.profile.find({ ns: "mydb.orders" }).sort({ millis: -1 }).limit(10)
// Queries with full collection scans
db.system.profile.find({ planSummary: /COLLSCAN/ }).sort({ millis: -1 })
// Queries examining far more documents than they return
db.system.profile.find({
$expr: { $gt: [{ $divide: ["$docsExamined", "$nreturned"] }, 100] }
}).sort({ millis: -1 })
The last query is particularly useful: a ratio of docsExamined / nreturned above 10-20 suggests the query is reading many documents to return few — a clear sign of missing or inefficient indexing.
Step 6: Check Index Usage
Find indexes that have never been used — they consume RAM and slow down writes without helping reads:
db.orders.aggregate([{ $indexStats: {} }])
Example output:
[
{ "name": "_id_", "key": { "_id": 1 }, "accesses": { "ops": 45231, "since": ISODate("2024-01-01") } },
{ "name": "status_1", "key": { "status": 1 }, "accesses": { "ops": 0, "since": ISODate("2024-01-01") } },
{ "name": "created_at_1", "key": { "created_at": -1 }, "accesses": { "ops": 12847, "since": ISODate("2024-01-01") } }
]
status_1 with 0 accesses is a candidate for removal. Dropping unused indexes frees RAM and speeds up writes.
To check indexes for all collections:
db.getCollectionNames().forEach(col => {
const stats = db[col].aggregate([{ $indexStats: {} }]).toArray();
const unused = stats.filter(i => i.accesses.ops === 0 && i.name !== "_id_");
if (unused.length > 0) {
print(col + ": unused indexes:", unused.map(i => i.name).join(", "));
}
});
Common Performance Patterns and Fixes
Pattern 1: High CPU, slow queries, COLLSCAN in profiler
Root cause: Queries running without indexes, causing full collection scans.
Fix:
// Identify the query pattern from the profiler
// Create an appropriate index
db.orders.createIndex({ status: 1, created_at: -1 })
// Verify the index is used
db.orders.find({ status: "pending" }).sort({ created_at: -1 }).explain("executionStats")
// Look for: winningPlan.stage === "IXSCAN"
Pattern 2: Growing dirty cache, write latency spikes
Root cause: WiredTiger cannot flush dirty pages fast enough — usually slow disk I/O or very high write rate.
Fixes:
- Move data directory to faster storage (NVMe SSD)
- Tune WiredTiger cache size: increase storage.wiredTiger.engineConfig.cacheSizeGB
- Review write patterns: batch small writes, use bulk operations
Pattern 3: Connection pool exhaustion
Root cause: Application not reusing connections, or connection pool too small for the load.
Fix: - Configure connection pool in the MongoDB driver:
// Node.js example
const client = new MongoClient(uri, { maxPoolSize: 50, minPoolSize: 5 });
- Check for connection leaks: connections opened but never closed
Pattern 4: Lock queue consistently above zero
Root cause: Long-running write operations blocking reads (or writes blocking other writes).
Fixes:
- Identify the long-running operation with db.currentOp()
- Break large write operations into smaller batches
- Use bulk writes with ordered: false where possible
Exporting Diagnostics for Analysis
Save all diagnostic outputs during an incident for later analysis:
#!/bin/bash
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTDIR="mongodb_diag_${TIMESTAMP}"
mkdir -p $OUTDIR
mongosh --quiet --eval "JSON.stringify(db.serverStatus(), null, 2)" > $OUTDIR/serverStatus.json
mongosh --quiet --eval "JSON.stringify(db.currentOp({active:true}), null, 2)" > $OUTDIR/currentOp.json
mongosh --quiet --eval "JSON.stringify(db.stats(), null, 2)" > $OUTDIR/dbStats.json
mongosh --quiet --eval "JSON.stringify(rs.status(), null, 2)" > $OUTDIR/rsStatus.json 2>/dev/null || true
mongostat --rowcount 60 1 > $OUTDIR/mongostat.txt &
mongotop 2 30 > $OUTDIR/mongotop.txt &
wait
echo "Diagnostics saved to $OUTDIR/"
Frequently Asked Questions
How do I know if my MongoDB performance problem is hardware or software?
Start with mongostat — if dirty% is high and growing, it's usually an I/O problem (disk can't keep up with writes). If CPU is high and scanAndOrder is climbing, it's a query/index problem. If connections are exhausted, it's an application configuration problem.
What is a good value for WiredTiger cache size? The default is 50% of RAM minus 1GB. For a dedicated MongoDB server with 32GB RAM, the default is about 15GB. Increase it if you have large working sets that don't fit in cache.
Should I use the profiler in production? Level 1 (slow queries only, above a threshold) has minimal overhead and is safe to leave on permanently. Level 2 (all queries) should only be enabled briefly for debugging specific issues.
How do I find which query is causing a COLLSCAN?
Check db.system.profile.find({ planSummary: /COLLSCAN/ }) for recent slow queries. Then run the query manually with .explain("executionStats") to see the full execution plan.
My mongotop shows high write time on one collection — what should I do? First check if the collection has appropriate indexes for its write patterns. High write time with good indexes usually means the collection is genuinely busy (high write volume). High write time without indexes means every insert is slower because of missing index optimization (indexes still need to be updated on write, but poor read indexes won't help write performance — check for excessive indexes instead).
Analyse Your MongoDB Diagnostics Automatically
Working through serverStatus, profiler output, mongostat and currentOp output simultaneously takes experience — knowing what's normal for your workload and what constitutes an anomaly.
DBA Copilot automates this. Upload your MongoDB diagnostic files and the AI identifies the dominant bottlenecks, correlates findings across multiple outputs, and generates a plain-language diagnosis with prioritised recommendations.
Supported: serverStatus.json, currentOp.json, dbStats.json, mongostat.txt, mongotop.txt, rsStatus.json, indexStats.json, MongoDB logs (mongod.log), and profiler output.
Try DBA Copilot free — no credit card required