← Back to blog

How to Diagnose MongoDB Performance Issues: A Complete Guide

DBA Copilot Team · · 10 min read

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


Related: MongoDB Slow Queries: How to Find and Fix Them

Want to analyze your database performance automatically?

Try DBA Copilot free — upload your AWR/ASH report and get an AI-powered diagnosis in seconds.

Start for free