← Back to DevBytes

MongoDB Server Bottleneck Detection and Resolution

Understanding MongoDB Server Bottlenecks

A MongoDB server bottleneck is any resource constraint or performance limitation that prevents the database from handling its workload efficiently. Bottlenecks manifest as high latency, reduced throughput, increased queue depth, or outright errors. They can occur at multiple layers: CPU saturation, memory pressure, disk I/O contention, lock contention, network limits, or suboptimal schema and query design. Detecting and resolving these bottlenecks is a fundamental skill for database administrators and developers working with MongoDB in production environments.

Why Bottleneck Detection Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Left unaddressed, a bottleneck degrades user experience, causes timeouts, and can cascade into broader system failures. For example, a single long-running aggregation might saturate CPU, starving other operations and triggering connection storms. Memory pressure forces WiredTiger cache evictions, increasing disk reads and slowing everything down. Bottleneck detection allows you to:

Effective detection transforms reactive firefighting into proactive capacity planning.

How to Detect Bottlenecks in MongoDB

MongoDB exposes rich diagnostic data through server commands, utilities, and profiling. Below are the essential techniques every developer should master.

1. Inspecting Server Status Metrics

The db.serverStatus() command returns a comprehensive snapshot of resource utilization. Key sections for bottleneck analysis include:

Example snippet to retrieve and filter critical fields:

// Connect via mongosh
const status = db.serverStatus();

// CPU and locks overview
print(`Uptime: ${status.uptime} seconds`);
print(`Connections: ${status.connections.current} (available: ${status.connections.available})`);

// WiredTiger cache pressure
const wt = status.wiredTiger;
if (wt) {
  const cacheUsage = wt.cache["bytes currently in the cache"];
  const cacheMax = wt.cache["maximum bytes configured"];
  const usagePercent = ((cacheUsage / cacheMax) * 100).toFixed(1);
  print(`WiredTiger Cache: ${usagePercent}% used (${cacheUsage}/${cacheMax})`);
  print(`Eviction triggered: ${wt.cache["eviction server evicting"]}`);
}

// Lock statistics
const globalLock = status.locks?.Global;
if (globalLock) {
  print(`Global lock acquisitions: ${globalLock.acquireCount}`);
  print(`Global lock wait (micros): ${globalLock.timeAcquiringMicros?.R}`);
}

High eviction counts combined with a saturated cache indicate memory pressure. Excessive lock wait times signal write or read contention.

2. Using mongostat and mongotop

The mongostat utility provides a live, per-second view of operations, resource usage, and queue depth. It's ideal for spotting sudden spikes.

# Run mongostat every second, 10 iterations
mongostat --host myhost --port 27017 -n 10 1

# Typical output columns:
# insert query update delete getmore command dirty used flushes vsize res qr qw ar aw netIn netOut conn time
#   *0    *0    *0    *0       0      0|0  0.0%  1.2%     0   1.2G  512M |0  |0  |0 |0   120b  45k    2  Oct 28 09:32:45.123

Watch qr (readers queued) and qw (writers queued). Values consistently above zero indicate requests waiting for locks – a classic bottleneck. dirty percentage approaching the WiredTiger dirty cache threshold suggests I/O pressure. used cache percentage near 95%+ points to memory saturation.

mongotop shows read/write activity per collection in real time, helping identify which namespace is generating heavy I/O.

mongotop --host myhost 5
# Output:
#                   ns    total   read    write    2024-10-28T09:35:01
#   mydb.orders         1024ms   800ms   224ms
#   mydb.sessions         512ms   512ms     0ms

3. Profiling Slow Queries

The database profiler records all operations that exceed a specified threshold. This is the most direct way to find problematic queries.

// Enable profiling for operations slower than 100ms
db.setProfilingLevel(1, { slowms: 100 });

// To view the slowest recent queries from system.profile
db.system.profile.find({ millis: { $gt: 200 } })
  .sort({ millis: -1 })
  .limit(5)
  .pretty();

// Sample output:
{
  "op": "query",
  "ns": "mydb.orders",
  "command": { "find": "orders", "filter": { "status": "pending", "created": { "$lt": ISODate(...) } } },
  "keysExamined": 0,
  "docsExamined": 523000,
  "hasSortStage": false,
  "millis": 412,
  "planSummary": "COLLSCAN",
  ...
}

Key fields to inspect: millis (duration), docsExamined vs keysExamined (index selectivity), and planSummary (COLLSCAN means missing index). Profiling introduces overhead, so in production use level 1 with a reasonable threshold, or sample at level 2 only temporarily.

4. Examining Current Operations

When a bottleneck is happening right now, db.currentOp() shows all active operations, including their query, duration, and whether they are waiting for a lock.

// Show operations running for more than 5 seconds
db.currentOp({
  "active": true,
  "secs_running": { $gt: 5 }
}).forEach(op => {
  print(`${op.op} on ${op.ns} running for ${op.secs_running}s`);
  print(`  Command: ${JSON.stringify(op.command)}`);
  if (op.lockStats) {
    print(`  Lock wait time (micros): ${op.lockStats.timeAcquiringMicros}`);
  }
});

Look for long-running writes holding exclusive locks, or numerous reads queued behind a write. This reveals lock contention bottlenecks instantly.

5. Query Explain Plans

The explain() method reveals how MongoDB executes a query, showing index usage, rejected plans, and stage-level timings. Use it to diagnose individual query bottlenecks.

// Explain with executionStats mode
const exp = db.orders.explain("executionStats").aggregate([
  { $match: { status: "pending", created: { $gte: startDate } } },
  { $group: { _id: "$region", total: { $sum: "$amount" } } }
]);

print(`Total docs examined: ${exp.executionStats.totalDocsExamined}`);
print(`Keys examined: ${exp.executionStats.totalKeysExamined}`);
print(`Execution time millis: ${exp.executionStats.executionTimeMillis}`);
print(`Winning plan: ${JSON.stringify(exp.executionStats.winningPlan)}`);

If totalDocsExamined vastly exceeds totalKeysExamined, an index is missing or not selective enough. The winning plan’s stage tree shows whether in-memory sorts or COLLSCAN stages appear – each a potential bottleneck.

6. Aggregating Profiling Data for Trends

Rather than hunting manually, you can aggregate the system.profile collection to surface patterns – e.g., queries by collection, average latency, or worst offenders over a time window.

db.system.profile.aggregate([
  { $match: { ts: { $gte: new Date(ISODate().getTime() - 3600000) } } }, // last hour
  { $group: {
      _id: "$ns",
      count: { $sum: 1 },
      avgMillis: { $avg: "$millis" },
      maxMillis: { $max: "$millis" }
  } },
  { $sort: { avgMillis: -1 } },
  { $limit: 10 }
]).forEach(doc => {
  print(`${doc._id}: ${doc.count} ops, avg ${doc.avgMillis.toFixed(1)}ms, max ${doc.maxMillis}ms`);
});

7. MongoDB Atlas and Ops Manager Metrics

In managed environments, Atlas provides graphical dashboards showing CPU, memory, disk I/O, and opcounters. Alerts can be set on metrics like “CPU Usage above 90% for 5 minutes” or “Disk I/O utilization spike”. These are invaluable first indicators of a bottleneck. Always cross-reference with server-side diagnostics for root cause.

Common Bottleneck Types and Their Resolution

1. CPU Bottleneck

Symptoms: High CPU usage, mongostat shows many active operations but low throughput, slow query times despite adequate memory.

Detection: db.serverStatus() high opcounters with corresponding CPU spikes. Profiling reveals queries doing large in-memory sorts, heavy aggregations, or COLLSCAN over millions of documents. Explain shows "SORT" stages or "PROJECTION" with heavy computation.

Resolution:

2. Memory Bottleneck

Symptoms: High WiredTiger cache usage (>95%), frequent cache evictions, mongostat shows dirty close to threshold, used near max, increased disk reads.

Detection: Examine db.serverStatus().wiredTiger.cache fields: “bytes currently in the cache” vs “maximum bytes configured”, “eviction server evicting”, and “pages evicted”. High eviction counts and a saturated cache confirm memory pressure. Also check mem.resident vs physical RAM.

Resolution:

// Example: set WiredTiger cache to 4GB in mongod.conf
// storage:
//   wiredTiger:
//     engineConfig:
//       cacheSizeGB: 4

3. Disk I/O Bottleneck

Symptoms: High dirty cache percentage, mongostat shows frequent flushes, queries stall waiting for disk, serverStatus reports high wiredTiger.cache["bytes written"] and read counts.

Detection: Monitor mongostat dirty column and flushes column. If dirty percentage exceeds the eviction trigger threshold (default 20% of cache), MongoDB must flush dirty pages to disk aggressively, causing I/O spikes. Also use iostat or sar on the host.

Resolution:

4. Lock Contention

Symptoms: Rising qr and qw in mongostat, operations queued while a long-running write holds an exclusive lock. db.currentOp() shows many waiting operations.

Detection: db.serverStatus().locks shows high timeAcquiringMicros for Global or Collection locks. Use db.currentOp() to find the blocking operation (often a write with "secs_running" high and "waitingForLock": false but others waiting).

Resolution:

5. Connection Pool Exhaustion

Symptoms: Application errors like “connection refused” or “too many connections”, serverStatus.connections.current approaching maxConns.

Detection: Monitor db.serverStatus().connections – if current - available is consistently high, you are near the limit. Also check application driver connection pool settings.

Resolution:

// mongosh to check connection stats
db.serverStatus().connections;
// Output example: { "current": 142, "available": 358, "totalCreated": 5000 }
// If current is high relative to total available, investigate.

6. Replication Lag

Symptoms: Secondary members are seconds or minutes behind the primary. Applications reading from secondaries see stale data. rs.status() shows large optimeDate differences.

Detection: Run rs.printSecondaryReplicationInfo() or check rs.status()["members"] for each secondary’s optimeDate vs primary.

Resolution:

Best Practices for Bottleneck Prevention

Conclusion

Detecting and resolving MongoDB server bottlenecks is a continuous cycle of monitoring, profiling, and optimization. By mastering tools like serverStatus, mongostat, the profiler, and explain plans, you can pinpoint exactly where performance degrades – whether CPU, memory, I/O, locks, or connections. Each bottleneck type has specific resolution tactics, from index tuning and schema redesign to hardware upgrades and sharding. The best defense is a proactive posture: embed profiling into your development workflow, monitor critical metrics, and act on trends before they become incidents. With the techniques outlined here, you can keep MongoDB deployments fast, resilient, and ready to scale.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles