Skip to content
ExponaBLOG
MONGODB, NOSQL & DATA MODELING · DAY 082 / 100

Common MongoDB errors and fixes: lessons from production

Errors you will see in production.

Expona Admin7 min read

MongoDB powers millions of transactions across e-commerce platforms, real-time analytics dashboards, and SaaS applications every day. Yet even seasoned teams encounter production errors that bring systems to their knees because they didn't anticipate how MongoDB behaves under load. We're sharing the common MongoDB errors and fixes: lessons from production that have caught us and our peers, so you can avoid the 3am alerts.

E11000 duplicate key: unique index violation

The E11000 error appears when you try to insert or update a document that violates a unique index constraint. It's one of the first errors you'll see at scale, and it almost always signals a gap between your application logic and your database schema.

Picture this: you're building a user registration system and add a unique index on the email field. Your application checks if the email exists before inserting, but under concurrent load, two requests slip through the race condition simultaneously. Both hit MongoDB within microseconds, and the second one crashes with E11000.

javascript

// Index definition
db.users.createIndex({ email: 1 }, { unique: true });

// Race condition scenario
const registerUser = async (email, password) => {
  const existing = await db.collection('users').findOne({ email });
  if (existing) throw new Error('Email exists');
  
  // Between this check and insert, another request sneaks in
  await db.collection('users').insertOne({ email, password });
};

The fix is to rely on MongoDB's unique index as your enforcement mechanism, not application logic. Wrap the insert in a try-catch, and handle the E11000 error as a duplicate:

javascript

const registerUser = async (email, password) => {
  try {
    await db.collection('users').insertOne({ email, password });
  } catch (err) {
    if (err.code === 11000) {
      throw new Error('Email already registered');
    }
    throw err;
  }
};

One subtle variant: compound unique indexes. If you have { tenantId: 1, username: 1 } as unique, MongoDB treats null as a value. If two documents have tenantId: null, you'll get E11000 on the second insert even if you didn't explicitly set null. Use sparse: true to exclude null values from the uniqueness check:

javascript

db.users.createIndex(
  { tenantId: 1, username: 1 },
  { unique: true, sparse: true }
);

Sort exceeded memory limit: add an index

This error hits when MongoDB tries to sort a result set without an index backing the sort operation. In memory, MongoDB caps sort operations at 32MB by default. Exceed that and the query fails.

Consider a reports collection with millions of documents. You're sorting orders by createdAt to show a dashboard:

javascript

db.orders.find({ status: 'completed' }).sort({ createdAt: -1 }).limit(10);

Without an index on createdAt, MongoDB loads all matching documents into memory, sorts them, then returns the top 10. If you have 5 million completed orders, that's potentially gigabytes of data shuffled in RAM.

The solution is an index that covers both the filter and the sort:

javascript

// This index helps both the filter and sort
db.orders.createIndex({ status: 1, createdAt: -1 });

MongoDB can now use the index to skip directly to the completed orders sorted by date, and return the top 10 without materializing the full result set.

Watch out for this pattern: if you filter on one field and sort on another with different directions, you might need to experiment. In general, put equality fields first, then range/sort fields, with sort direction matching your needs:

javascript

// For: find({ status, userId }).sort({ createdAt: -1 }).limit(10)
db.orders.createIndex({ status: 1, userId: 1, createdAt: -1 });

Run explain() to verify your index is being used:

javascript

db.orders.find({ status: 'completed' })
  .sort({ createdAt: -1 })
  .explain('executionStats');
// Look for "executionStages.stage": "SORT_MERGE" or "COLLSCAN"
// "COLLSCAN" means no index is being used for the sort

Connection pool timeout: too many slow queries

The connection pool sits between your application and MongoDB. It maintains a fixed number of connections, reusing them across requests. When all connections are tied up waiting for slow queries to finish, new requests queue up and eventually timeout.

This often manifests as intermittent timeouts that seem random. In reality, one or two problematic queries are starving the entire pool.

We debugged this in a ride-hailing platform: surge pricing calculations were triggering a full collection scan on historical trip data. The query took 15 seconds. With a pool of 10 connections and frequent requests, after about 5-6 surge price requests landed, all connections were blocked, and every other request timed out.

javascript

// Slow query blocking the pool
db.trips.find({ 
  pickupTime: { $gte: ISODate('2024-01-01') },
  status: 'completed'
}).sort({ pickupTime: -1 });
// Without indexes, this scans millions of documents

The fix involves three steps:

  1. Identify slow queries: Enable the MongoDB profiler or use application-level timing. Any query taking longer than 100ms is a red flag at scale.
  2. Add indexes: Most slow queries lack proper indexes. The query above needs { pickupTime: -1, status: 1 }.
  3. Increase pool size judiciously: If you have legitimately long queries, increase maxPoolSize, but don't use it as a band-aid for unindexed queries.
javascript

// Monitoring query performance in your driver
const client = new MongoClient(uri, {
  maxPoolSize: 50,  // Default is often 10
  serverSelectionTimeoutMS: 5000
});

// Add timing middleware
const startTime = Date.now();
const result = await collection.find(query).toArray();
const duration = Date.now() - startTime;
if (duration > 100) {
  console.warn(`Slow query: ${duration}ms`, query);
}

Document exceeds maximum size: split it

MongoDB enforces a 16MB hard limit per document. Exceed it and insertion fails. This constraint exists for good reasons: large documents degrade query performance, waste RAM, and create network overhead.

We hit this in a SaaS analytics platform where customers could store event logs directly as nested arrays. A power user with a few years of events ended up with a single document pushing 25MB:

javascript

// Anti-pattern: storing all events in one document
{
  customerId: '12345',
  events: [
    { timestamp: '...', action: 'login' },
    { timestamp: '...', action: 'view_page' },
    // ... millions of events
  ]
}

The fix is to split the data into separate collections or use a time-series approach:

javascript

// Solution 1: Separate events collection
db.events.insertOne({
  customerId: '12345',
  timestamp: ISODate('2024-01-01'),
  action: 'login'
});

// Solution 2: Use time-series collection (MongoDB 5.0+)
db.createCollection('events', {
  timeseries: {
    timeField: 'timestamp',
    metaField: 'metadata',
    granularity: 'hours'
  }
});

db.events.insertOne({
  timestamp: ISODate('2024-01-01T10:00:00Z'),
  metadata: { customerId: '12345' },
  action: 'login'
});

Time-series collections are optimized for this pattern and automatically bucket data, reducing storage overhead by 80-90% in many cases.

Common mistakes

  • Ignoring index cardinality: An index on a field with only a few distinct values (like status: 'active', 'inactive') doesn't help much. Indexes work best on high-cardinality fields or in compound indexes after low-cardinality equality filters.
  • Creating too many indexes: Each index slows down writes because every insert/update/delete must update all indexes. We've seen production systems slow to a crawl with 20+ indexes on a single collection. Audit and remove unused indexes regularly.
  • Treating MongoDB like a relational database: Normalizing every relationship into separate collections and joining with $lookup is expensive. Embrace denormalization where it makes sense; duplicate data strategically to avoid joins.
  • Not handling connection errors: Transient network issues happen. If your code doesn't retry with exponential backoff, you'll see random failures under load.

Key takeaways

  • E11000 errors are a sign to lean on MongoDB's unique indexes, not application-level validation, to enforce uniqueness under concurrency.
  • Sort operations without indexes are a silent killer; always profile queries and add indexes when explain shows COLLSCAN or sort memory warnings.
  • Connection pool exhaustion is usually caused by a handful of slow queries; identify them, add indexes, and resist the urge to blindly increase pool size.
  • The 16MB document limit is a feature, not a bug; split large datasets into multiple documents or use time-series collections for high-volume event data.
  • Index strategy matters as much as schema design; too few indexes cause slow queries, too many slow down writes. Find the balance and iterate based on production metrics.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles