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:
- Pinpoint exactly which resource is limiting performance
- Correlate slow application responses with database internals
- Make informed scaling decisions (vertical, horizontal, sharding)
- Tune queries, indexes, and hardware before incidents occur
- Maintain predictable latency under growing load
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:
- opcounters – read, write, update, delete rates
- locks – lock acquisition counts and wait times (especially
Global,Collection,Database) - mem – virtual, resident, and mapped memory; WiredTiger cache details
- network – bytes in/out, number of requests
- connections – current, available, and active connections
- wiredTiger – cache statistics, eviction rate, bytes read into cache
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:
- Create indexes to avoid collection scans and reduce document examination
- Optimize aggregations: place
$matchearly, use$projectto trim fields before grouping - Consider pre-aggregated summary collections or materialized views for analytics queries
- Upgrade to faster CPUs or add shards to distribute computational load
- Limit result sets with
.limit()and use projection to return only needed fields
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:
- Increase WiredTiger cache size via
storage.wiredTiger.engineConfig.cacheSizeGB(ensure enough free RAM for the OS) - Add more RAM to the server
- Design schemas and queries so the working set (frequently accessed data + indexes) fits in RAM
- Use smaller documents; avoid embedding huge arrays that bloat working set
- Shard the collection to distribute data across nodes, reducing per-node working set
// 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:
- Move to faster storage (SSD/NVMe) – single biggest impact for I/O-bound workloads
- Reduce write load: batch inserts, use bulk operations, tune write concern for non-critical data
- Increase WiredTiger cache to hold more dirty pages, reducing flush frequency
- Separate journal and data directories onto different disks to parallelize I/O
- Use
--directoryperdband place high-write databases on faster volumes
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:
- Ensure all collections involved in frequent writes have appropriate indexes – a write without an index may scan and hold locks longer
- Shorten write operations: update only necessary fields, use
$setrather than replacing whole document - Use
writeConcern: majorityonly when needed; lower write concern reduces lock duration on secondary confirmation (though doesn't reduce primary lock time) - Split high-contention collections into multiple collections or shard them
- Consider upgrading to MongoDB version with improved lock granularity (document-level locks in WiredTiger, but still collection-level exclusive for some operations like createIndex)
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:
- Increase
maxConnsin mongod config (watch ulimit on file descriptors) - Configure driver connection pool sizes appropriately; avoid creating a new MongoClient per request
- Use connection pooling and limit idle connections
- Investigate slow operations causing connections to remain open longer; fixing the bottleneck reduces connection churn
// 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:
- Ensure secondaries have equivalent hardware; a weaker secondary cannot keep up with primary’s write rate
- Reduce write load on primary, or shard to distribute writes
- Increase secondary’s WiredTiger cache to speed up replay of oplog
- Check network latency between primary and secondary
- Use priority and hidden members to route read operations away from lagging secondaries
Best Practices for Bottleneck Prevention
- Index Strategically: Use the ESR rule (Equality, Sort, Range) to design compound indexes. Regularly run
db.collection.getIndexes()and remove unused indexes that waste memory and CPU on writes. - Profile Continuously: Run profiling at level 1 with a 100-200ms threshold in production. Aggregate
system.profileweekly to detect regressions. - Monitor Working Set: Ensure
index + working set size < RAM. Usedb.collection.stats()to check data and index sizes, and plan hardware accordingly. - Use Capped Collections or TTL Indexes: For high-throughput time-series data, avoid unbounded growth that eventually causes I/O and memory bottlenecks.
- Batch Operations: Use bulk writes and
insertMany/updateManywhere possible to reduce round trips and lock overhead. - Connection Management: Reuse MongoClient instances; set
maxPoolSizeaccording to expected concurrency. MonitorserverStatus.connectionsregularly. - Shard Early, but Not Prematurely: When a single replica set approaches its I/O or memory ceiling, sharding distributes load across multiple sets. Use
sh.status()to verify balanced chunks. - Set Alerts on Key Metrics: CPU >85%, WiredTiger cache usage >90%, dirty percentage >15%, connections near limit, and query queue depths >0. Proactive alerts catch bottlenecks before users notice.
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.