Performance Optimization and Monitoring: Production-Level Tuning
Performance optimization and monitoring are the "last mile" of a production environment—mastering them can prevent 90% of performance incidents.
1. What You'll Learn
- Connection Pool Tuning
- Query Optimization (hint / projection / lean)
- Analysis of the Slow Query Log
- APM monitoring tools
- Production Deployment Checklist
graph LR
App[Node.js Applications] -->|Request| Pool[Connection Pool<br/>maxPoolSize=50]
Pool -->|Connect 1| M1[mongod<br/>Primary]
Pool -->|Connect 2| M2[mongod<br/>Secondary]
Pool -->|Connect 3| M3[mongod<br/>Secondary]
M1 -->|Copy| M2
M1 -->|Copy| M3
App -.->|explain| M1
App -.->|indexStats| M1
style Pool fill:#d4edda
style M1 fill:#cce5ff
2. Connection Pool Tuning
Why is a connection pool important? Every time MongoDB establishes a TCP connection, it requires: a three-way handshake + SCRAM authentication (3 round trips) + session initialization—taking approximately 10–50 ms. In high-concurrency scenarios, if there is no connection pool, creating a new connection for every request can lead to a connection storm that overwhelms the database. Connection pooling reduces connection establishment time to < 1 ms by preheating and reusing connections.
How Connection Pools Work:
graph LR
subgraph "Node.js Applications"
R1[Request1] -->|Loan Out| Pool[(Connection Pool<br/>min=5 max=50)]
R2[Request2] -->|Loan Out| Pool
R3[Request3] -->|Wait in line| Queue[Waiting Queue<br/>waitQueueTimeoutMS]
end
Pool -->|Active Connections| Active[mongod Primary]
Pool -->|Idle Connections| Idle[Free Space Recovery<br/>maxIdleTimeMS]
R1 -.->|Return| Pool
R3 -.->|Get the Return Link| Pool
style Pool fill:#d4edda
style Queue fill:#fff3cd
style Active fill:#cce5ff
Guide to Tuning Connection Pool Parameters:
| Parameter | Default Value | Tuning Strategy | Risk of Setting Too High | Risk of Setting Too Low |
|---|---|---|---|---|
| maxPoolSize | 100 | Peak concurrency × 1.5 | Database connections exhausted | Request queue timeout |
| minPoolSize | 0 | Set to 10% of peak | Slow startup, high memory usage | Cold start delay |
| maxIdleTimeMS | 0 | 30000 (30 seconds) | Frequent creation of new connections | Connection leaks |
| waitQueueTimeoutMS | 0 | 10000 (10 seconds) | Request backlog | Requests in permanent wait |
| serverSelectionTimeoutMS | 30000 | 5000 | Long delay | Fast failure |
Formula for Calculating the Number of Connections: maxPoolSize = peak concurrency * average query time (seconds) * safety factor (1.5). For example, if there are 100 concurrent connections and the average query time is 0.1 seconds, then maxPoolSize = 100 * 0.1 * 1.5 = 15.
// === mongoose Connection Pool Configuration ===
mongoose.connect(uri, {
maxPoolSize: 50, // Maximum Number of Connections(Default 100)
minPoolSize: 5, // Minimum Number of Connections(Default 0)
maxIdleTimeMS: 30000, // Connection Idle Timeout(Default: Unlimited)
waitQueueTimeoutMS: 10000 // Connection timeout
});
// === Monitor Connection Pool ===
const db = mongoose.connection.db;
const stats = await db.admin().command({ serverStatus: 1 });
console.log('Connections:', stats.connections);
| maxPoolSize | Applicable |
|---|---|
| 10 | Small Applications / Development |
| 50 | Medium Applications / Web Services |
| 100 | Large Applications / High Traffic |
| 200+ | Cluster Mode / CDN Origin |
3. Query Optimization
Query Optimization Methodology: The core goal of query optimization is to "reduce the database's workload"—by scanning fewer documents, transferring fewer fields, and skipping unnecessary serialization. Optimization steps: ① Use explain() to diagnose the problem; ② Verify whether the index is hit; ③ Reduce the number of returned fields; ④ Reduce the number of returned records; ⑤ Skip unnecessary Mongoose overhead.
Four Essential Techniques for Query Optimization:
| Optimization Technique | Principle | Effect | Code |
|---|---|---|---|
| hint() forces index usage | bypasses the query optimizer's incorrect choice | COLLSCAN → IXSCAN | .hint({field: 1}) |
| select() projection | Returns only necessary fields | Data transfer ↓90%+ | .select('sku title price') |
| lean() skip hydrate | Don't construct mongoose Document | Performance up 5x | .lean() |
| limit() Limits the number of records | Prevents returning too many documents | Reduces memory usage | .limit(20) |
Common Slow Query Patterns and Solutions:
| Slow Query Mode | Cause | Solution |
|---|---|---|
| COLLSCAN (Full Table Scan) | Missing index or index miss | Create an appropriate index + hint() |
| Too many fields returned | find() No select |
Add .select() projection |
| $where + JS execution | Execute JS functions per document | Switch to native operators |
| $regex: no anchor | Cannot use index | Add ^ prefix to set an anchor |
| Deep pagination skip | skip (slow with large datasets) | Switch to cursor pagination |
| N+1 queries | Querying one by one in a loop | Switch to $in or $lookup |
(1) hint() forces indexing
// === Force Index Usage ===
const products = await Product.find({ category: 'Electronics' })
.hint({ category: 1 });
// === mongoose Equivalent ===
const products = await Product.find({ category: 'Electronics' })
.hint('category_1');
(2) projection: Reduce the number of fields
// === Query only the necessary fields ===
const products = await Product.find()
.select('sku title price') // Don't fetch description, images, etc.
.lean();
// === Reduce Network Traffic 90%+ ===
(3) lean() skips hydrate
// === General Query: Build mongoose Document (slow) ===
const products = await Product.find();
// === lean(): Return plain object directly (fast) ===
const products = await Product.find().lean();
(4) Avoid $where
// Slow: $where executes JavaScript
db.products.find({ $where: 'this.price > 1000' });
// Fast: Use $gt
db.products.find({ price: { $gt: 1000 } });
4. Slow Query Analysis
Slow Query Analysis Process: Identify slow queries → Enable Profiler → Analyze explain() → Pinpoint bottlenecks → Optimize → Verify. There are three profiling levels: 0 (off), 1 (log slow queries), and 2 (log all queries). Use Level 1 in production environments; Level 2 incurs a performance overhead.
Slow Query Diagnosis Workflow:
graph TD
Start[Identifying Slow Queries] --> Enable[Enable Profiling Level 1<br/>slowms=100]
Enable --> Collect[Collect Slow Query Logs<br/>system.profile]
Collect --> Explain[explain executionStats<br/>Analyze the Execution Plan]
Explain --> Stage{Stage Type?}
Stage -->|COLLSCAN| AddIdx[Add an Index]
Stage -->|IXSCAN| CheckProj[Check the projection/Number of Articles]
Stage -->|FETCH| OptProj[Optimization select]
AddIdx --> Verify[Verify the effectiveness of the optimization]
CheckProj --> Verify
OptProj --> Verify
Verify --> Done[Performance Meets Standards ✅]
style COLLSCAN fill:#f8d7da
style Done fill:#d4edda
explain() outputs key fields:
| Field | Description | Normal Value | Abnormal Value |
|---|---|---|---|
| stage | Scan Method | IXSCAN | COLLSCAN |
| totalKeysExamined | Index keys scanned | ~ nReturned | >> nReturned |
| totalDocsExamined | Documents scanned | ~ nReturned | >> nReturned |
| nReturned | Number of documents returned | — | — |
| executionTimeMillis | Execution Time | < 100 ms | > 1000 ms |
| indexUsed | Index Used | Composite Index Name | None (COLLSCAN) |
Golden Ratio: totalDocsExamined : nReturned ≈ 1:1. If only 20 records are returned after scanning 10,000, it means the index is not precise enough.
// === Enable the slow query log ===
mongoose.connection.db.admin().command({
setParameter: 1,
slowms: 100 // Record > 100ms Queries
});
// === mongoose Enable in the middle debug ===
mongoose.set('debug', true);
// Output all queries to console
// === mongoose-debug Slow Query Log ===
mongoose.set('debug', (collectionName, method, query, doc) => {
const start = Date.now();
console.log(`${collectionName}.${method}(${JSON.stringify(query)})`);
});
5. Index Optimization
Core Principles of Index Optimization: More indexes aren’t necessarily better—each index takes up disk space, increases write overhead (since indexes must be updated with every insert, update, or delete), and consumes memory (MongoDB tries to keep indexes in RAM). The key to optimizing indexes is to create useful indexes, remove unnecessary ones, and choose the right index type.
Index Health Check Metrics:
| Metric | How to Obtain | Normal Value | Abnormal Value |
|---|---|---|---|
| Index Usage | $indexStats | accesses.ops > 0 | ops = 0 (not used) |
| Index Size | db.stats() | < 50% of RAM | > RAM (frequent swapping) |
| Number of indexes | getIndexes() | < 10 / collection | > 20 (too many) |
| Write Latency | serverStatus | < 10 ms | > 100 ms (index is slowing down writes) |
Index Optimization Decisions:
| Scenario | Action | Reason |
|---|---|---|
| Index with ops=0 | Delete | Pure waste of space and write performance |
| Redundant indexes | Keep composite indexes, remove single-field indexes | {a:1, b:1} already covers {a:1} |
| Low-base index | Delete or modify some indexes | isActive has only two values, so it has low selectivity |
| Check for Indexes Not Used | Add or use hint() | COLLSCAN Performance Disaster |
// === Index Usage Analysis ===
db.products.aggregate([{ $indexStats: {} }]);
// Find unused indexes
// === Index Usage in Slow Queries ===
db.system.profile.find({ millis: { $gt: 100 } })
.sort({ ts: -1 })
.limit(20)
.forEach(profile => {
console.log('Query:', profile.command.find);
console.log('Plan:', profile.planSummary);
console.log('Time:', profile.millis, 'ms');
});
6. APM Monitoring Tools
Why Do You Need APM? Issues in production environments don’t give you advance warning—sudden spikes in connections, an increase in slow queries, and memory leaks all occur gradually. APM (Application Performance Monitoring) provides real-time metric collection, visual dashboards, and threshold alerts, allowing you to identify and resolve problems before users complain.
Monitoring Metrics Pyramid:
graph TB
L4[Business Metrics<br/>Orders/Error Rate/Response Time] --> L3[Application Metrics<br/>QPS/Latency/Node.js Memory]
L3 --> L2[Database Metrics<br/>Number of connections/Slow Queries/Index Hit Rate]
L2 --> L1[Infrastructure<br/>CPU/Memory/Disk/Internet]
style L4 fill:#d4edda
style L1 fill:#fff3cd
Key Performance Indicators:
| Metric Category | Specific Metric | Alert Threshold | Collection Method |
|---|---|---|---|
| Connections | Current Connections | > maxPoolSize × 80% | serverStatus.connections |
| Query | Number of Slow Queries | > 10/min | system.profile |
| Query | Average Query Time | > 200 ms | Mongoose Debug / APM |
| Index | Index Hit Rate | < 95% | $indexStats |
| Memory | Resident Memory | > 80% of available RAM | serverStatus.mem |
| Disk | Disk Usage | > 80% | db.stats() |
| Instance | Replication Delay | > 10s | rs.status().lag |
| Tool | Features | Applications |
|---|---|---|
| MongoDB Atlas Monitoring | Official, built-in | Atlas users |
| Prometheus + mongo_exporter | Open source, self-hosted | Large-scale production |
| Datadog APM | Commercial, Full-Stack | Enterprise |
| New Relic | Business, Full-Stack | Enterprise |
| Prometheus + Grafana | Open source, free | Self-hosted monitoring |
7. Production Deployment Checklist
Key Considerations for Production Environments: Production deployment isn’t just about “making sure the code runs”; it’s about ensuring high availability (a single point of failure does not affect service), data security (no data loss or leaks), observability (the ability to quickly pinpoint issues), and recoverability (the ability to restore from backups).
The Four Pillars of Production Deployment:
| Pillar | Objective | Key Measures |
|---|---|---|
| High Availability | 99.9%+ Availability | 3-node replica set + automatic failover |
| Data Security | No Data Loss + No Data Leaks | Write Concern: majority + TLS + RBAC |
| Observability | Pinpoint issues within 5 minutes | Logs + Metrics + Alerts + APM |
| Recoverability | RPO < 1h, RTO < 4h | Scheduled Backups + PITR + Recovery Drills |
## 10. Performance Optimization Checklist
---
### (1) Database
- [ ] Dungeon Collection 3 Node(High Availability)
- [ ] Write Concern majority(No data loss)
- [ ] Read Preference primaryPreferred
- [ ] Slow Query Monitoring Enabled
- [ ] Index Usage Monitoring
- [ ] Connection Pool maxPoolSize Settings
### (2) Application Layer
- [ ] mongoose lean() For read-only queries
- [ ] projection Search only the required fields
- [ ] bulkWrite Bulk Operations
- [ ] Avoid $where / $regex Unanchored
- [ ] Error-handling middleware
- [ ] Health Check Endpoints /healthz
### (3) Operations and Maintenance
- [ ] Daily mongodump Backup
- [ ] Backup Retention 7-30 days
- [ ] Monitoring Alerts(Number of connections / Slow Queries / Disk)
- [ ] TLS/SSL Encrypted Transmission
- [ ] SCRAM + RBAC Access Control
- [ ] Atlas PITR or oplog Continuous Backup
8. Hands-On: Comprehensive Performance Tuning
Practical Methodology for Performance Tuning: Performance tuning isn’t about making guesswork-based optimizations; it’s a closed-loop process of “measure → analyze → optimize → verify.” First, use explain() to measure current performance and identify bottlenecks (full table scans? Excessive data transfer? Hydrate overhead?), then implement targeted optimizations, and finally use explain() again to verify the results.
Closed-Loop Performance Tuning:
graph LR
A[Measurement: explain + Slow Log] --> B[Analysis: Bottleneck Identification]
B --> C[Optimization: Index/Projection/lean]
C --> D[Verify: use explain again]
D -->|Did not meet the standard| A
D -->|Meet the requirements| E[Launch ✅]
style A fill:#cce5ff
style C fill:#d4edda
style E fill:#d4edda
ShopHub Optimization Case Study: Alice from TechCorp optimized the product list API, reducing the response time from 3 seconds to 50 ms—① explain() revealed a COLLSCAN → added a composite index {category:1, isActive:1, createdAt:-1}; ② Returned all fields → used select() to query only 5; ③ Returning a Mongoose document → Added lean(); ④ find and count were sequential → Used Promise.all for parallel processing.
// === Slow Queries Before Optimization ===
app.get('/api/products', async (req, res) => {
const products = await Product.find({ isActive: true });
// 100 Full-Table Scan of 10,000 Documents, ~3 seconds
res.json(products);
});
// === After optimization ===
app.get('/api/products', async (req, res) => {
const { page = 1, limit = 20, category, search } = req.query;
// 1. Building Indexes to Optimize Queries
const query = { isActive: true };
if (category) query.category = category;
if (search) query.title = new RegExp(search, 'i');
// 2. Projection + lean + limit
const products = await Product.find(query)
.select('sku title price thumbnail') // Projection
.hint({ isActive: 1, category: 1 }) // Forced Index
.limit(Math.min(+limit, 100)) // Maximum Limit
.skip((+page - 1) * +limit)
.lean(); // Performance Optimization
// 3. Parallel count
const total = await Product.countDocuments(query);
res.json({ data: products, meta: { page: +page, limit: +limit, total } });
});
// After optimization:~50ms(Performance ↑60x)
▶ Example 1: explain() Diagnostics + Index Optimization
// === Scenario: ShopHub product list queries are slow, Alice uses explain to diagnose ===
// 1. Diagnose the Current Query
const explain = await Product.find({ category: 'Electronics', isActive: true })
.sort({ createdAt: -1 })
.limit(20)
.explain('executionStats');
console.log('Stage:', explain.queryPlanner.winningPlan.stage);
// Output:COLLSCAN ❌ Full Table Scan!
console.log('Docs examined:', explain.executionStats.totalDocsExamined);
// Output:1000000(Scanned all of them 100 10,000 Documents)
console.log('Docs returned:', explain.executionStats.nReturned);
// Output: 20 (Returned only 20 docs)
console.log('Time:', explain.executionStats.executionTimeMillis, 'ms');
// Output: 3200ms (too slow!)
// 2. Create a composite index
db.products.createIndex({ category: 1, isActive: 1, createdAt: -1 });
// 3. Once again explain Verification
const explain2 = await Product.find({ category: 'Electronics', isActive: true })
.sort({ createdAt: -1 })
.limit(20)
.hint({ category: 1, isActive: 1, createdAt: -1 })
.explain('executionStats');
console.log('Stage:', explain2.queryPlanner.winningPlan.stage);
// Output:IXSCAN ✅ Using Indexes!
console.log('Docs examined:', explain2.executionStats.totalDocsExamined);
// Output:20(Precise Scanning)
console.log('Time:', explain2.executionStats.executionTimeMillis, 'ms');
// Output:5ms(↑640x Performance Improvements!)
Output: The explain() diagnostic identified a COLLSCAN → created a composite index → IXSCAN, and the query time dropped from 3,200 ms to 5 ms.
▶ Example 2: Practical Guide to Comprehensive Performance Tuning (Connection Pool + Indexing + Lean + Monitoring)
// === Scene:Product List API Performance Optimization(3s → 50ms)===
// === Before Optimization (slow) ===
app.get('/api/products', async (req, res) => {
const products = await Product.find(); // Full Table Scan + Return all fields
res.json(products);
});
// 100 10,000 Documents,~3000ms,~50MB Data
// === After optimization (fast) ===
// 1. Enable mongoose debug(Monitoring Queries During Development)
mongoose.set('debug', (coll, method, query) => {
console.log(`${coll}.${method}(${JSON.stringify(query)})`);
});
// 2. Optimizing the Connection Pool
mongoose.connect(uri, {
maxPoolSize: 50, // Adjust Based on Concurrency
minPoolSize: 5,
maxIdleTimeMS: 30000,
waitQueueTimeoutMS: 10000
});
// 3. Enable the slow query log(>100ms)
mongoose.connection.db.admin().command({
setParameter: 1,
slowms: 100
});
// 4. Create Appropriate Indexes
db.products.createIndex({ category: 1, isActive: 1, createdAt: -1 });
db.products.createIndex({ sku: 1 }, { unique: true });
db.products.createIndex({ title: 'text', description: 'text' });
// 5. Optimize Queries:projection + lean + hint + limit
app.get('/api/products', async (req, res) => {
const { page = 1, limit = 20, category, search } = req.query;
const query = { isActive: true };
if (category) query.category = category;
if (search) query.title = new RegExp(search, 'i');
const [products, total] = await Promise.all([
Product.find(query)
.select('sku title price thumbnail rating') // Projection:Return only 5 field
.hint({ category: 1, isActive: 1, createdAt: -1 }) // Forced Index
.sort({ createdAt: -1 })
.skip((page - 1) * limit)
.limit(Math.min(+limit, 100))
.lean(), // Skip mongoose hydrate
Product.countDocuments(query)
]);
res.json({
success: true,
data: products,
meta: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) }
});
});
// 100 10,000 Documents,~50ms(Performance ↑60x),~200KB Data(Reduce 99.6%)
// === 6. explain() Verify that the index is active ===
const explain = await Product.find({ category: 'Electronics', isActive: true })
.sort({ createdAt: -1 })
.limit(20)
.explain('executionStats');
console.log('Stage:', explain.queryPlanner.winningPlan.stage);
// Output:IXSCAN(An index was used)
console.log('Docs examined:', explain.executionStats.totalDocsExamined);
console.log('Keys examined:', explain.executionStats.totalKeysExamined);
console.log('Returned:', explain.executionStats.nReturned);
console.log('Time:', explain.executionStats.executionTimeMillis, 'ms');
// === 7. Monitoring Alerts ===
// 7.1 Monitor Connection Count
const connStatus = await mongoose.connection.db.admin().command({ serverStatus: 1 });
if (connStatus.connections.current > 1000) {
console.warn(`⚠️ Too many connections: ${connStatus.connections.current}`);
// Send an Alert (Email/DingTalk/Slack)
}
// 7.2 Monitoring Slow Queries
const slowQueries = await mongoose.connection.db.collection('system.profile')
.find({ millis: { $gt: 100 } })
.sort({ ts: -1 })
.limit(10)
.toArray();
slowQueries.forEach(q => {
console.log(`[${q.ts}] ${q.command.find}: ${q.millis}ms`);
console.log(` Plan: ${q.planSummary}`);
});
// 7.3 Index Usage Rate
const indexStats = await mongoose.connection.db.collection('products')
.aggregate([{ $indexStats: {} }])
.toArray();
const unused = indexStats.filter(s => s.accesses.ops === 0);
unused.forEach(i => {
console.log(`⚠️ Index not used: ${i.name}`);
// Automatic Deletion(Caution in Production Environments)
// await mongoose.connection.db.collection('products').dropIndex(i.name);
});
// === 8. APM Integration(Datadog/New Relic)===
const tracer = require('dd-trace').init();
tracer.use('mongoose', { service: 'shopdb' });
// All mongoose Query Auto-Tracking,Performance Data Reporting Datadog
Results: Through comprehensive optimization of the connection pool, indexes, Lean, and projections, query performance improved from 3 seconds to 50 milliseconds (a 60x increase), and data transfer was reduced by 99.6%.
❓ FAQ
lean()?save and populate). It is intended for use with the query-only API.query.explain('executionStats') Check the stage: IXSCAN (index scan) / COLLSCAN (full table scan).📖 Summary
- Connection pool tuning: maxPoolSize / minPoolSize
- Query Optimization: hint / projection / lean / avoid $where
- Slow Query Analysis: setProfilingLevel / mongoose debug
- Index usage monitoring: $indexStats / system.profile
- APM Tools: Atlas / Prometheus / Datadog
- Production Checklist: Replica Sets + Backups + Monitoring + Security
📝 Exercises
- Basic Problem (⭐): Use
lean()to optimize the product list API and compare the performance differences. - Basic Question (⭐): Enable the slow query log and analyze the top 10 slow queries.
- Advanced Problem (⭐⭐): Use a hint to force indexing, and compare the performance of COLLSCAN and IXSCAN.
- Advanced Problem (⭐⭐): Analyze $indexStats to identify unused indexes and delete them.
- Challenge (⭐⭐⭐): Perform a comprehensive performance tune-up (connection pool + indexes + lean + monitoring), and compare performance before and after.



