Understanding MongoDB Profiling
MongoDB profiling is the systematic process of collecting, analyzing, and interpreting performance data about database operations. At its core, profiling records detailed metrics about every query, update, insert, or delete operation that runs against your MongoDB instance. This data reveals exactly how long operations take, which indexes are being used (or missed), the shape of query plans, and the frequency patterns that might otherwise remain invisible.
The MongoDB profiler operates at three distinct levels, controlled by the profile database command or the --profile startup option:
- Level 0: Profiling disabled — no data collected
- Level 1: Only slow operations are logged, based on the
slowmsthreshold - Level 2: Every single operation is logged regardless of execution time
Profiled data is stored in a capped collection called system.profile within each database. Because this collection is capped, it has a fixed size and overwrites oldest entries first, preventing it from consuming unlimited disk space.
Enabling and Configuring the Profiler
To enable profiling, you use the db.setProfilingLevel() helper method. The following example enables level 1 profiling with a 200-millisecond slow operation threshold, while also configuring a 5-megabyte size limit for the system.profile collection:
// Connect to the target database
use myapp_database;
// Enable profiling level 1 with a 200ms slowms threshold
// The second argument sets the system.profile capped collection size to 5MB
db.setProfilingLevel(1, {
slowms: 200,
sampleRate: 0.5,
filter: {
"command.find": { $gt: 1000 }
}
});
// Verify the current profiling status
const status = db.getProfilingStatus();
printjson(status);
// Output:
// {
// "was": 0,
// "slowms": 200,
// "sampleRate": 0.5,
// "filter": { "command.find": { "$gt": 1000 } },
// "ok": 1
// }
The sampleRate option (available in MongoDB 4.0+) allows you to profile only a percentage of operations, reducing overhead in high-throughput environments. The filter option lets you target specific operation types, so you can focus on slow finds while ignoring fast writes, for example.
Reading and Analyzing Profile Data
Once profiling is active, the system.profile collection fills with documents that contain rich diagnostic information. Here is how to query this collection effectively:
// Find the top 10 slowest operations in the last hour
db.system.profile.find({
ts: {
$gte: new Date(Date.now() - 3600000)
}
}).sort({ millis: -1 }).limit(10).pretty();
// Find queries that performed a COLLSCAN (no index used)
db.system.profile.find({
"planSummary": {
$regex: /COLLSCAN/
},
"op": "query"
}).sort({ millis: -1 }).limit(5);
// Aggregate to see which collections have the most slow operations
db.system.profile.aggregate([
{
$match: {
millis: { $gt: 100 },
op: "query"
}
},
{
$group: {
_id: "$ns",
count: { $sum: 1 },
avgDuration: { $avg: "$millis" },
maxDuration: { $max: "$millis" }
}
},
{ $sort: { count: -1 } }
]);
// Examine a specific slow query's execution plan in detail
db.system.profile.find({
"command.find": "orders",
millis: { $gt: 500 }
}).limit(1).forEach(doc => {
print("Query: " + JSON.stringify(doc.command.filter));
print("Keys examined: " + doc.keysExamined);
print("Docs examined: " + doc.docsExamined);
print("Plan summary: " + doc.planSummary);
print("Duration ms: " + doc.millis);
});
Each profile document includes critical fields: op (the operation type), ns (namespace), command (the actual command object), millis (execution time in milliseconds), planSummary (the winning query plan), keysExamined and docsExamined (index keys and documents scanned), and nreturned (documents returned). A high ratio of docsExamined to nreturned often indicates an inefficient query that scans many more documents than it ultimately returns.
Why MongoDB Profiling Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Without profiling, you are flying blind. Production databases can degrade silently: a new application release might introduce unindexed queries, a growing dataset can push previously fast queries into slow territory, or a subtle driver bug might cause redundant operations. Profiling provides the empirical evidence you need to make informed optimization decisions.
Specifically, profiling helps you:
- Identify missing indexes: Queries showing
COLLSCANin their plan summary are scanning entire collections, which becomes catastrophically slow as data grows - Detect poorly constructed queries: Operations that examine thousands of documents but return only a handful signal inefficient filtering
- Uncover hot collection patterns: Aggregating profile data by namespace reveals which collections bear the heaviest load
- Measure the impact of index changes: By profiling before and after adding an index, you can quantify the improvement in both execution time and documents examined
- Catch application-level bugs: Repeated identical queries, missing
$limitclauses, or queries that never use indexes often trace back to application logic flaws
Practical Optimization Workflow
A systematic approach to performance optimization using the profiler involves four distinct phases: capture, analyze, hypothesize, and verify.
Phase 1: Capture Representative Data
Enable profiling during a realistic load period — ideally during peak traffic or via a controlled load test. Avoid profiling at level 2 in production unless you have extremely low throughput, as recording every operation adds measurable overhead. Instead, use level 1 with an appropriate slowms threshold and consider the sampleRate option for high-volume systems.
// For a high-throughput production system, use sampling
db.setProfilingLevel(1, {
slowms: 150,
sampleRate: 0.25 // Profile only 25% of operations
});
Phase 2: Analyze with the MongoDB Shell and Aggregation
Use the aggregation pipeline against system.profile to build a clear picture of performance bottlenecks. The following pipeline identifies the most expensive query shapes by grouping on the command structure:
db.system.profile.aggregate([
{ $match: {
op: "query",
millis: { $gt: 100 }
}},
{ $group: {
_id: {
collection: "$ns",
// Extract the first filter condition to group similar queries
filterKeys: { $ifNull: ["$command.filter", {}] }
},
count: { $sum: 1 },
totalTime: { $sum: "$millis" },
avgTime: { $avg: "$millis" },
p99Time: { $max: "$millis" },
avgDocsExamined: { $avg: "$docsExamined" }
}},
{ $sort: { totalTime: -1 } },
{ $limit: 20 }
]);
Phase 3: Diagnose with Explain Plans
Once you have identified a slow query from the profile data, extract its command object and run it through explain() to see the full query plan. This reveals index usage, rejected plans, and stage-by-stage execution details.
// Retrieve a slow query's command from the profiler
const slowOp = db.system.profile.findOne({
millis: { $gt: 500 },
op: "query"
});
// Reconstruct and explain the query
const collection = slowOp.ns.split('.')[1];
const filter = slowOp.command.filter || {};
const sort = slowOp.command.sort || {};
// Run the explain
db[collection].find(filter).sort(sort).explain("executionStats");
// Key metrics to inspect in the explain output:
// - executionStats.totalKeysExamined vs totalDocsExamined
// - executionStats.executionStages.stage (IXSCAN vs COLLSCAN)
// - executionStats.executionStages.inputStages for index intersection info
// - queryPlanner.winningPlan and rejectedPlans
Phase 4: Implement and Verify Improvements
After creating a new index or restructuring a query, verify the improvement by checking the new explain plan and by comparing profile data before and after the change. A well-designed index should dramatically reduce docsExamined and bring keysExamined close to nreturned.
// Create a compound index based on your analysis
db.orders.createIndex(
{ userId: 1, createdAt: -1 },
{ name: "idx_user_created" }
);
// Verify with explain
db.orders.find({
userId: "usr_12345",
createdAt: { $gte: ISODate("2025-01-01") }
}).sort({ createdAt: -1 }).explain("executionStats");
// After deployment, compare profiler data
db.system.profile.aggregate([
{ $match: {
ns: "myapp.orders",
ts: { $gte: new Date(Date.now() - 3600000) }
}},
{ $group: {
_id: "$planSummary",
avgMillis: { $avg: "$millis" },
count: { $sum: 1 }
}}
]);
Advanced Profiling Techniques
Using the Database Profiler with Filters
MongoDB 4.2 introduced the ability to set a filter expression on the profiler itself. This allows you to capture only operations that match specific criteria, drastically reducing noise in high-volume systems:
// Only profile queries on the "orders" collection taking > 300ms
db.setProfilingLevel(1, {
slowms: 300,
filter: {
$and: [
{ "ns": { $regex: /\.orders$/ } },
{ "op": "query" },
{ "millis": { $gt: 300 } }
]
}
});
Correlating with the Slow Query Log
Beyond the profiler, MongoDB can log slow operations to the server log file. Configure this via the slowOpThresholdMs and slowOpSampleRate parameters. The server log provides timestamps and context that can be correlated with system metrics like CPU spikes or disk latency:
// In mongod.cfg or via setParameter:
db.adminCommand({
setParameter: 1,
slowOpThresholdMs: 200,
slowOpSampleRate: 0.5
});
// Then grep the mongod log for slow queries
// grep -E "slow query|command" /var/log/mongodb/mongod.log
Leveraging the $currentOp for Real-Time Profiling
For diagnosing issues happening right now — long-running operations that haven't completed yet — use the $currentOp aggregation pipeline stage. This provides a real-time view of active operations, including their progress and the query they are executing:
// Find all operations running longer than 30 seconds
db.currentOp().inprog.forEach(op => {
if (op.secs_running > 30) {
printjson({
opid: op.opid,
ns: op.ns,
secsRunning: op.secs_running,
query: op.command,
planSummary: op.planSummary
});
}
});
// Kill a runaway operation if necessary
db.killOp(opid);
Common Optimization Patterns and Solutions
Pattern 1: COLLSCAN on Large Collections
Symptom: Profiler shows planSummary: "COLLSCAN" with high millis and docsExamined close to total collection size.
Solution: Create an index that covers the query's filter fields. Use the profiler to extract the exact filter object, then build a matching index — preferably a compound index if the query filters on multiple fields:
// Profiler reveals: db.users.find({email: "user@example.com"})
// Currently COLLSCAN with docsExamined: 2,500,000
// Create the missing index
db.users.createIndex({ email: 1 });
// Verify: docsExamined should now be 1
Pattern 2: Sparse Index Not Being Used
Symptom: You created a partial or sparse index, but the profiler still shows COLLSCAN for queries that should match.
Solution: Ensure your query filter includes the conditions that make the index eligible. For a sparse index, the query must filter on the indexed field with a non-null condition. For a partial index, the query filter must match the partialFilterExpression exactly or be a subset:
// Create a partial index for active users only
db.users.createIndex(
{ lastLogin: -1 },
{ partialFilterExpression: { status: "active" } }
);
// Query MUST include status: "active" to use this index
db.users.find({
status: "active",
lastLogin: { $gte: thirtyDaysAgo }
}).sort({ lastLogin: -1 });
Pattern 3: Index Intersection Not Optimal
Symptom: Profiler shows planSummary using multiple indexes with an AND_SORTED or AND_HASH stage. While index intersection works, a dedicated compound index is almost always faster.
Solution: Create a compound index matching the filter and sort fields together:
// Profiler shows plan using two separate indexes with AND_HASH
// For: db.products.find({category: "electronics", price: {$lt: 500}})
// Replace with compound index
db.products.createIndex({ category: 1, price: 1 });
// The explain plan should now show a single IXSCAN stage
Pattern 4: Inefficient Sorting
Symptom: Profiler shows high millis even with an index on the filter field. The explain plan reveals an in-memory sort stage after the index scan.
Solution: Build an index that includes the sort field in the correct order, allowing MongoDB to both filter and sort using the index:
// Query: db.events.find({userId: "u1"}).sort({timestamp: -1})
// Index on {userId: 1} exists but sort requires in-memory sort
// Improved index covers both filter and sort
db.events.createIndex({ userId: 1, timestamp: -1 });
// Now the index provides both filtering and sort order
// Explain shows IXSCAN with no SORT stage
Pattern 5: Large Limit with Small Skip
Symptom: Pagination queries using $skip and $limit become progressively slower as the skip value grows, visible in the profiler as increasing millis and docsExamined.
Solution: Replace offset-based pagination with range-based pagination using indexed fields:
// Slow: db.posts.find().sort({_id: -1}).skip(5000).limit(50)
// Each page requires scanning skip + limit documents
// Faster range-based approach:
// First page
const page1 = db.posts.find().sort({_id: -1}).limit(50);
// Get the last _id from page1 results
// Next page uses range query
const lastId = ObjectId("...");
const page2 = db.posts.find({_id: {$lt: lastId}}).sort({_id: -1}).limit(50);
// This always scans exactly 50 documents regardless of page depth
Profiling Overhead and Production Safety
Profiling does introduce overhead — each profiled operation requires a write to the capped system.profile collection. In high-throughput environments, this can become noticeable. To minimize impact:
- Never use profiling level 2 in production unless temporarily for a specific diagnostic session
- Set
sampleRateto a low value like 0.1 or 0.05 for continuous monitoring - Use the profiler filter to capture only operations of interest
- Monitor the size of the
system.profilecollection and set its cap appropriately - Consider running detailed profiling on a dedicated analytics node (a secondary) rather than on primaries
Automating Profile Analysis
For larger deployments, manual shell queries against system.profile become tedious. Consider building automated scripts that periodically analyze profile data and alert on anomalies:
// A Node.js script skeleton for automated profiling analysis
const { MongoClient } = require('mongodb');
async function analyzeProfile(uri) {
const client = new MongoClient(uri);
try {
await client.connect();
const adminDb = client.db('admin');
// Check current profiling level across all databases
const dbs = await adminDb.admin().listDatabases();
for (const dbInfo of dbs.databases) {
const db = client.db(dbInfo.name);
const profileStatus = await db.command({ profile: -1 });
if (profileStatus.was === 0) {
console.log(`WARNING: Profiling disabled on ${dbInfo.name}`);
continue;
}
// Fetch recent slow operations
const slowOps = await db.collection('system.profile')
.find({
millis: { $gt: 200 },
ts: { $gte: new Date(Date.now() - 600000) }
})
.sort({ millis: -1 })
.limit(50)
.toArray();
// Group by namespace and calculate statistics
const stats = {};
for (const op of slowOps) {
if (!stats[op.ns]) {
stats[op.ns] = { count: 0, totalMs: 0, collscans: 0 };
}
stats[op.ns].count++;
stats[op.ns].totalMs += op.millis;
if (op.planSummary?.includes('COLLSCAN')) {
stats[op.ns].collscans++;
}
}
// Alert on collections with high COLLSCAN rates
for (const [ns, stat] of Object.entries(stats)) {
if (stat.collscans > 0) {
console.log(`ALERT: ${ns} has ${stat.collscans} COLLSCAN queries ` +
`out of ${stat.count} slow operations`);
}
}
}
} finally {
await client.close();
}
}
Integrating with MongoDB Atlas Performance Advisor
If you use MongoDB Atlas, the platform provides a Performance Advisor that automatically analyzes slow queries and suggests indexes. This builds on the same profiling infrastructure but adds machine learning to identify query patterns and recommend compound indexes. You can access it through the Atlas UI or programmatically via the Atlas Admin API:
// Fetch suggested indexes from Atlas API
// curl -H "Authorization: Bearer $ATLAS_API_KEY" \
// "https://cloud.mongodb.com/api/atlas/v1.0/groups/{GROUP_ID}/\
// clusters/{CLUSTER_NAME}/performanceAdvisor/suggestedIndexes"
// The response includes index recommendations with impact scores
// that you can implement directly or validate with explain() first
Best Practices Summary
- Start with level 1 and a reasonable slowms threshold — typically 100-300ms depending on your application's SLA
- Always set a sampleRate in production — 0.1 to 0.5 is usually sufficient for pattern detection without excessive overhead
- Analyze profile data regularly — set up a cron job or scheduled task that aggregates recent profile data and reports anomalies
- Correlate profiling with application metrics — combine MongoDB profile data with application-level latency measurements for a complete picture
- Use explain() on extracted queries — the profiler tells you what's slow; explain() tells you why
- Build indexes that cover both filter and sort fields — this eliminates in-memory sorting and dramatically reduces response times
- Monitor index usage with the profiler — regularly check that your existing indexes are actually being used; unused indexes waste memory and slow writes
- Rotate or archive profile data — since system.profile is capped, ensure its size is sufficient for your analysis window
- Never profile at level 2 indefinitely — use it only for short diagnostic sessions, then revert to level 1 or 0
- Test index changes on a staging environment first — use profiler data from production to guide index creation, but validate on staging before deploying
Conclusion
MongoDB profiling transforms database performance work from guesswork into a disciplined, data-driven engineering practice. By enabling the profiler at an appropriate level, querying the system.profile collection with targeted aggregations, and iterating through the capture-analyze-hypothesize-verify cycle, you can systematically eliminate performance bottlenecks. The profiler reveals not just which queries are slow, but precisely why — whether due to missing indexes, inefficient query patterns, or scaling data volumes. Combined with explain plans, real-time $currentOp monitoring, and automated analysis scripts, profiling gives you complete visibility into your MongoDB server's behavior. The investment in learning these profiling techniques pays for itself many times over in reduced infrastructure costs, improved user experience, and the confidence that your database will continue performing well as your application grows.