← Back to DevBytes

MongoDB API Bottleneck Detection and Resolution

What is a MongoDB API Bottleneck?

A MongoDB API bottleneck is a performance choke point that occurs when application requests to MongoDB exceed the database's ability to process them efficiently. These bottlenecks manifest as high latency, reduced throughput, timeouts, or increased resource consumption on the database server. They typically arise from inefficient queries, missing indexes, poor schema design, suboptimal aggregation pipelines, or exhausted connection pools. In modern applications, the API layer (driver communication) often becomes the hidden culprit—not the raw disk I/O or CPU—making it essential to detect and resolve issues at the query and data access level.

Why Bottleneck Detection Matters

🚀 Deploy your AI agent in 10 minutes

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

Try it free →

Ignoring MongoDB bottlenecks can degrade user experience, increase infrastructure costs, and lead to cascading failures across microservices. Key reasons to prioritize detection include:

How to Detect Bottlenecks in MongoDB

1. Using the MongoDB Profiler

The built-in profiler records all operations that exceed a specified threshold, storing them in the system.profile collection. This is the most direct way to catch slow queries and commands.


// Enable profiler in mongosh: level 2 captures all operations; slowms sets the threshold in milliseconds
db.setProfilingLevel(2, { slowms: 100 })

// Query the profiler output to find the latest slow queries
db.system.profile.find({ millis: { $gt: 100 } })
  .sort({ ts: -1 })
  .limit(5)
  .pretty()

Look for fields like op (operation type), ns (namespace), command (the actual query), millis (duration in ms), and planSummary (e.g., COLLSCAN). This immediately flags queries that scan entire collections.

2. Analyzing Slow Queries with explain()

The explain method shows execution plans and statistics. Run it on suspicious queries to see index usage, examined vs. returned documents, and execution stages.


// Run in mongosh or via driver
db.orders.find({ customerId: "CUST-123" }).explain("executionStats")

Key metrics: totalDocsExamined vs. nReturned. A high ratio indicates an index is missing or inefficient. If stage is COLLSCAN, you have a full collection scan that needs immediate attention.

3. Monitoring with mongostat and mongotop

These command-line tools give real-time insight into operations per second, resource usage, and per-collection read/write activity.


# Run mongostat to see ops counters, network I/O, and connection counts
mongostat --host localhost:27017

# Run mongotop to identify collections with heavy read/write activity
mongotop --host localhost:27017

If a specific collection shows consistently high read or write time, focus your bottleneck investigation there.

4. Driver-Level Logging and Application Performance Monitoring (APM)

Modern MongoDB drivers support logging and APM events that expose command start, success, and failure. This helps correlate database latency with application endpoints.


// Node.js driver APM example
const { MongoClient } = require('mongodb');
const client = new MongoClient('mongodb://localhost:27017', {
  monitorCommands: true,
});

client.on('commandStarted', (event) => {
  console.log(`Started: ${event.commandName} on ${event.databaseName}`);
});
client.on('commandSucceeded', (event) => {
  console.log(`Succeeded: ${event.commandName} in ${event.duration}ms`);
});
client.on('commandFailed', (event) => {
  console.log(`Failed: ${event.commandName} - ${event.failure}`);
});

Integrate these events with your existing monitoring stack (Datadog, New Relic, etc.) to visualize database latency alongside API response times.

5. MongoDB Atlas Performance Advisor

If you use MongoDB Atlas, the Performance Advisor automatically analyzes slow queries and suggests indexes. It also provides a visual Query Profiler and real-time performance charts. Regularly review its recommendations—they often pinpoint bottlenecks you'd miss manually.

Common Bottleneck Patterns and Resolutions

1. Missing Indexes Causing COLLSCAN

Queries without supporting indexes force MongoDB to examine every document. This is the most common bottleneck.


// Check plan summary in profiler: "COLLSCAN" means no index used
// Resolution: create the appropriate index
db.orders.createIndex({ customerId: 1, orderDate: -1 })

Always ensure indexes cover filter fields, sort fields, and fields used in $match or $lookup pipeline stages.

2. Retrieving Unnecessary Data

Fetching full documents with many fields wastes network bandwidth and memory. Use projection to return only required fields.


// Instead of full document find
db.users.find({ status: "active" }).project({ _id: 1, name: 1, email: 1 })

For large result sets, implement pagination with skip and limit or use range-based pagination for better performance.

3. Aggregation Pipeline Inefficiencies

Aggregation stages like $lookup, $group, and $unwind can become bottlenecks if they process millions of documents without early filtering.


// Example: optimized aggregation with index on orders.customerId and early $match
db.orders.aggregate([
  { $match: { customerId: "CUST-123" } },  // early filter, uses index
  { $lookup: {
      from: "products",
      localField: "productId",
      foreignField: "_id",  // ensure products._id is indexed (usually default)
      as: "productInfo"
  }},
  { $unwind: "$productInfo" },
  { $project: { _id: 1, total: 1, "productInfo.name": 1 } }
])

4. Write Contention and Locking

High-frequency writes to the same document can cause contention. Use bulk writes to batch operations, and consider sharding to distribute write load. Avoid excessive findAndModify loops; use atomic updates where possible.


// Bulk write example in Node.js
const bulkOps = [
  { insertOne: { document: { _id: 1, item: "A" } } },
  { updateOne: { filter: { _id: 1 }, update: { $set: { qty: 10 } } } },
  { deleteOne: { filter: { _id: 2 } } }
];
await collection.bulkWrite(bulkOps, { ordered: false });

5. Connection Pool Exhaustion

When applications open too many connections or fail to reuse them, the pool can starve, causing request queuing. Tune pool size based on workload and ensure connections are closed properly.


// Node.js connection pool configuration
const client = new MongoClient('mongodb://localhost:27017', {
  maxPoolSize: 100,   // default is 100; increase if needed
  minPoolSize: 10,    // maintain a minimum number of connections
  maxIdleTimeMS: 30000, // close idle connections after 30s
});

Monitor connection pool metrics using driver events or server status (db.serverStatus().connections).

6. N+1 Query Problem

Fetching related data by executing one query per document (e.g., in a loop) multiplies database round-trips. Use $lookup in an aggregation pipeline or perform a single find with an $in query to batch-load related data.


// Bad: N+1 queries in a loop
for (const order of orders) {
  const product = await db.collection('products').findOne({ _id: order.productId });
  // process...
}

// Good: single aggregation with $lookup
const ordersWithProducts = await db.collection('orders').aggregate([
  { $match: { _id: { $in: orderIds } } },
  { $lookup: { from: 'products', localField: 'productId', foreignField: '_id', as: 'product' } }
]).toArray();

Practical Code Examples for Bottleneck Resolution

Below are complete, runnable examples showing how to detect and resolve common bottlenecks using Node.js and Python drivers.

Node.js (MongoDB Node.js Driver)


const { MongoClient } = require('mongodb');

async function detectAndResolveBottleneck() {
  const client = new MongoClient('mongodb://localhost:27017', {
    monitorCommands: true,
    maxPoolSize: 50
  });

  // APM listener for latency tracking
  client.on('commandSucceeded', (event) => {
    if (event.duration > 100) {
      console.warn(`Slow command: ${event.commandName} on ${event.databaseName} took ${event.duration}ms`);
    }
  });

  try {
    await client.connect();
    const db = client.db('shop');
    const orders = db.collection('orders');

    // 1. Enable profiling if not already enabled
    await db.command({ profile: 2, slowms: 50 });

    // 2. Explain a query suspected of being slow
    const explainResult = await orders.find({ status: 'pending' }).explain('executionStats');
    console.log('Execution Stats:', JSON.stringify(explainResult.executionStats, null, 2));

    // If COLLSCAN detected, create index
    if (explainResult.executionStats?.executionStages?.stage === 'COLLSCAN') {
      console.log('COLLSCAN detected! Creating index on status + createdAt...');
      await orders.createIndex({ status: 1, createdAt: -1 });
    }

    // 3. Optimize query with projection and limit
    const recentPending = await orders.find(
      { status: 'pending' },
      { projection: { _id: 1, total: 1, createdAt: 1 }, limit: 20 }
    ).toArray();
    console.log(`Fetched ${recentPending.length} pending orders`);

    // 4. Aggregation with early $match and indexed $lookup
    const orderDetails = await orders.aggregate([
      { $match: { status: 'pending' } },  // uses index created above
      { $limit: 50 },
      { $lookup: {
          from: 'customers',
          localField: 'customerId',
          foreignField: '_id',  // assuming customers._id is indexed (primary key)
          as: 'customer'
      }},
      { $project: { _id: 1, total: 1, 'customer.name': 1 } }
    ]).toArray();
    console.log(`Aggregated ${orderDetails.length} orders with customer names`);

    // 5. Bulk write example to reduce round-trips
    const bulkOps = [
      { updateOne: { filter: { _id: recentPending[0]?._id }, update: { $set: { status: 'processed' } } } },
      { insertOne: { document: { orderId: 'log-001', event: 'batch_complete', timestamp: new Date() } } }
    ];
    if (recentPending.length > 0) {
      await orders.bulkWrite(bulkOps, { ordered: false });
    }

  } finally {
    await client.close();
  }
}

detectAndResolveBottleneck().catch(console.error);

Python (PyMongo)


import pymongo
from pymongo import MongoClient
import time

def detect_and_resolve():
    client = MongoClient(
        'mongodb://localhost:27017',
        maxPoolSize=50,
        monitor=True  # PyMongo 4.x supports event listeners
    )

    # APM listeners (requires pymongo 4.x+)
    def command_started(event):
        print(f"Started: {event.command_name} on {event.database_name}")

    def command_succeeded(event):
        if event.duration_millis > 100:
            print(f"SLOW: {event.command_name} took {event.duration_millis}ms")

    client.event_listeners.add_command_listener(command_started, command_succeeded)

    db = client.shop
    orders = db.orders

    # 1. Enable profiler
    db.command('profile', 2, slowms=50)

    # 2. Explain a query
    plan = orders.find({'status': 'pending'}).explain()['executionStats']
    print("Execution stages:", plan.get('executionStages', {}).get('stage'))

    if plan.get('executionStages', {}).get('stage') == 'COLLSCAN':
        print("COLLSCAN detected! Creating index...")
        orders.create_index([('status', pymongo.ASCENDING), ('createdAt', pymongo.DESCENDING)])

    # 3. Optimized query with projection
    recent_pending = list(orders.find(
        {'status': 'pending'},
        projection={'_id': True, 'total': True, 'createdAt': True}
    ).limit(20))
    print(f"Fetched {len(recent_pending)} pending orders")

    # 4. Aggregation
    pipeline = [
        {'$match': {'status': 'pending'}},
        {'$limit': 50},
        {'$lookup': {
            'from': 'customers',
            'localField': 'customerId',
            'foreignField': '_id',
            'as': 'customer'
        }},
        {'$project': {'_id': 1, 'total': 1, 'customer.name': 1}}
    ]
    order_details = list(orders.aggregate(pipeline))
    print(f"Aggregated {len(order_details)} orders with customer info")

    # 5. Bulk write
    if recent_pending:
        bulk_ops = [
            pymongo.UpdateOne(
                {'_id': recent_pending[0]['_id']},
                {'$set': {'status': 'processed'}}
            ),
            pymongo.InsertOne({
                'orderId': 'log-001',
                'event': 'batch_complete',
                'timestamp': time.time()
            })
        ]
        orders.bulk_write(bulk_ops, ordered=False)

    client.close()

detect_and_resolve()

Best Practices for Sustained Performance

Conclusion

Detecting and resolving MongoDB API bottlenecks is an ongoing discipline that combines profiling, explain analysis, driver monitoring, and thoughtful schema design. By proactively identifying slow queries, missing indexes, connection pool exhaustion, and aggregation inefficiencies, developers can prevent minor performance hiccups from turning into major outages. The techniques and code examples provided here form a practical toolkit you can integrate into your development workflow today. Start with the profiler, move to explain, apply the targeted fixes (indexes, projections, aggregation optimizations), and reinforce with monitoring and best practices. Your users will experience faster load times, and your infrastructure will scale gracefully as demand grows.

🚀 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