1. Introduction đą
Building a functionally correct MongoDB application is only half the job â ensuring it performs well at scale requires deliberate attention to queries, indexes, schema design, and infrastructure.
This tutorial covers how to diagnose and resolve performance bottlenecks in MongoDB applications built with Node.js, from query-level tuning to cluster-wide scaling strategies.
Information
2. Performance Fundamentals âī¸
MongoDB performance is shaped by four interconnected layers, each of which can become a bottleneck independently.
Best Practice
3. Query Optimization đ
The single biggest performance lever is ensuring queries use indexes effectively rather than scanning entire collections.
query-optimization.js
// â Inefficient: scans every document
const users = await db.collection('users').find({
bio: { $regex: 'engineer' },
}).toArray();
// â
Efficient: uses an index-backed exact match
const users = await db.collection('users').find({
role: 'engineer',
}).toArray();- Prefer equality and range queries over unanchored $regex.
- Use projections to return only needed fields.
- Avoid $whereand JavaScript-based query operators â they bypass indexes entirely.
4. Index Optimization đ
Indexes are the most impactful performance tool available, but must be chosen deliberately â every index also adds write overhead.
index-optimization.js
// Single field index
await db.collection('orders').createIndex({ customerId: 1 });
// Compound index â order matters!
await db.collection('orders').createIndex({ status: 1, createdAt: -1 });
// Partial index â only indexes matching documents
await db.collection('orders').createIndex(
{ status: 1 },
{ partialFilterExpression: { status: 'pending' } }
);Tip
5. Aggregation Optimization đ
aggregation-optimization.js
const results = await db.collection('orders').aggregate([
{ $match: { status: 'completed' } }, // filter early, uses indexes
{ $sort: { createdAt: -1 } },
{ $limit: 20 },
{ $project: { customerId: 1, amount: 1 } }, // reduce fields as early as reasonable
]).toArray();- Place $match as early as possible to reduce the working document set.
- Use $project to strip unneeded fields before expensive stages like $group.
- Add allowDiskUse: true for pipelines exceeding the 100MB memory limit per stage.
Warning
6. Schema Optimization đī¸
Unlike relational databases, MongoDB rewards denormalization when access patterns favor reading related data together.
Best for data that is read together and doesn't grow unboundedly, such as an order's line items.
embedding.js
{
_id: 1,
customerName: 'Alice',
items: [
{ product: 'Widget', qty: 2 },
{ product: 'Gadget', qty: 1 },
],
}Best for data that is large, shared, or frequently updated independently, such as a customer profile referenced by many orders.
referencing.js
{
_id: 1,
customerId: ObjectId('...'),
items: [ /* ... */ ],
}Best Practice
7. Read Optimization đ
read-optimization.js
// Use lean-style projections to reduce payload size
const users = await db.collection('users')
.find({ isActive: true }, { projection: { name: 1, email: 1 } })
.toArray();
// Use covered queries â index contains all needed fields
await db.collection('users').createIndex({ email: 1, name: 1 });
const result = await db.collection('users')
.find({ email: 'alice@example.com' }, { projection: { _id: 0, email: 1, name: 1 } })
.toArray();Tip
8. Write Optimization âī¸
- Batch writes with insertMany() or bulkWrite() instead of looping individual writes.
- Choose an appropriate write concern â w: 1 is faster but less durable than w: 'majority'.
- Avoid unnecessary indexes on write-heavy collections, since each index must be updated on every write.
write-concern.js
await db.collection('logs').insertOne(
{ event: 'click', timestamp: new Date() },
{ writeConcern: { w: 1 } } // faster, less durable â fine for non-critical logs
);9. Bulk Operations đĻ
bulk-operations.js
const bulkOps = users.map((user) => ({
updateOne: {
filter: { _id: user._id },
update: { $set: { lastActive: new Date() } },
},
}));
await db.collection('users').bulkWrite(bulkOps, { ordered: false });Tip
10. Connection Pooling đ
connection-pooling.js
const client = new MongoClient(uri, {
maxPoolSize: 50,
minPoolSize: 10,
maxIdleTimeMS: 60000,
});Best Practice
11. Caching Strategies đī¸
Caching frequently accessed, rarely changing data reduces load on MongoDB and improves response times significantly.
caching.js
const redis = require('redis').createClient();
async function getProduct(id) {
const cached = await redis.get(`product:${id}`);
if (cached) return JSON.parse(cached);
const product = await db.collection('products').findOne({ _id: id });
await redis.set(`product:${id}`, JSON.stringify(product), { EX: 300 });
return product;
}Information
12. Memory Usage đ§
MongoDB's WiredTiger storage engine relies heavily on RAM to cache frequently accessed data (the working set).
- Ensure your working set fits in RAMwhere possible â disk reads are orders of magnitude slower.
- Monitor cache usage and page faults via serverStatus().
- Avoid retrieving unnecessarily large documents or fields into application memory.
server-status.js
const status = await db.admin().command({ serverStatus: 1 });
console.log(status.wiredTiger.cache['bytes currently in the cache']);13. Storage Optimization đŊ
- Choose appropriately typed fields â e.g., Int32 instead of Double where possible.
- Avoid deeply nested or excessively large documents; keep them well under the 16MB limit.
- Use shorter field names in extremely large collections to reduce storage footprint (minor but compounding).
Reference
14. Compression đī¸
WiredTiger supports block compression for both collections and indexes, reducing disk footprint and often improving I/O throughput.
compression.js
db.createCollection('logs', {
storageEngine: {
wiredTiger: {
configString: 'block_compressor=zstd',
},
},
});| Compressor | Tradeoff |
|---|---|
| snappy (default) | Fast, moderate compression |
| zstd | Better compression ratio, slightly more CPU |
| zlib | Highest compression, most CPU-intensive |
15. Explain Plans đĩī¸
explain.js
const plan = await db.collection('orders')
.find({ status: 'pending' })
.explain('executionStats');
console.log('Docs examined:', plan.executionStats.totalDocsExamined);
console.log('Docs returned:', plan.executionStats.nReturned);
console.log('Stage:', plan.executionStats.executionStages.stage);Important
16. Query Profiler đ
The database profiler records slow operations, making it invaluable for identifying real-world bottlenecks in production traffic.
profiler.js
// Log queries slower than 100ms
await db.setProfilingLevel(1, { slowms: 100 });
// Review captured slow operations
const slowQueries = await db.collection('system.profile')
.find({})
.sort({ ts: -1 })
.limit(10)
.toArray();Caution
17. Performance Advisor đŠē
MongoDB Atlas includes a built-in Performance Advisor that analyzes slow queries and suggests missing indexes automatically.
- Surfaces index suggestions based on real query patterns.
- Flags redundant or unused indexes that can be safely dropped.
- Integrates with the Query Insights tab for visual slow-query analysis.
Tip
18. Monitoring đĄ
monitoring.js
const stats = await db.admin().command({ serverStatus: 1 });
console.log('Connections:', stats.connections.current);
console.log('Opcounters:', stats.opcounters);
console.log('Uptime:', stats.uptime);- Atlas Metricsâ built-in dashboards for CPU, memory, IOPS, and query latency.
- MongoDB Ops Managerâ equivalent monitoring for self-managed deployments.
- Third-party APM tools like Datadog or New Relic for end-to-end tracing.
19. Benchmarking đī¸
Benchmarking measures how your database performs under realistic load before issues appear in production.
terminal
# Example using mongoose-benchmark style load testing tools
npx autocannon -c 50 -d 30 http://localhost:3000/api/products- Benchmark with data volumes and query patterns representative of production.
- Test under concurrent load, not single-threaded sequential requests.
- Re-benchmark after significant schema, index, or infrastructure changes.
20. Scaling Strategies đ
Best Practice
21. Replication Performance đ
read-preference.js
const { MongoClient, ReadPreference } = require('mongodb');
const client = new MongoClient(uri, {
readPreference: ReadPreference.SECONDARY_PREFERRED,
});- Distribute read-only traffic to secondary nodes with secondaryPreferred.
- Be aware of replication lagâ secondaries may briefly serve slightly stale data.
- Use w: 'majority' write concern for critical data to ensure durability across replicas.
22. Sharding Performance âĄ
Sharding distributes data across multiple servers, enabling horizontal scale for very large datasets and high-throughput workloads.
shard-key.js
sh.shardCollection('shopDB.orders', { customerId: 'hashed' });Important
- Prefer high-cardinality fields with even value distribution as shard keys.
- Avoid monotonically increasing shard keys (like timestamps) without hashing, which concentrate writes on one shard.
23. Best Practices â
- Design indexes around actual query patterns, verified with explain().
- Keep the working set within available RAM whenever feasible.
- Use projections to minimize network and memory overhead.
- Batch writes with bulkWrite() instead of individual operations.
- Continuously monitor with the profiler and Atlas Performance Advisor.
24. Common Mistakes đĢ
| Mistake | Consequence |
|---|---|
| No index on frequently queried fields | Full collection scans, high latency |
| Over-indexing write-heavy collections | Slower writes from index maintenance overhead |
| Unbounded arrays in documents | Document growth causing storage fragmentation |
| Poor shard key choice | Uneven load distribution (hotspotting) |
| Ignoring explain() output | Blind optimization guesswork |
Caution
25. Frequently Asked Questions â
Question
Answer
Question
Answer
Question
Answer
26. Summary đ
Summary
- Official Docs: MongoDB Query Optimization
- Atlas Tools: Atlas Performance Advisor