Contact Us

Top MongoDB Antipatterns That Hurt Performance

Introduction

Many of us like NoSQL, and MongoDB is one of its most popular choices. We often pick MongoDB for its flexibility and speed. That makes sense, because MongoDB almost never lets you down... at first. Low-maintenance, quick, convenient — it behaves like the perfect helper: it needs nothing extra, accepts any data, never asks awkward questions about the schema, and answers every query in milliseconds.

But then you start to suspect something is wrong. And the key part: it happens gradually, not all at once. First one query takes a bit longer than usual, then another. Where you used to see 10–20 milliseconds, you now see 100. You notice the charts behaving oddly. So you start hunting for the cause: you blame the software version, then the hardware, then decide that MongoDB itself is somehow broken.

But the answer is often right on the surface: MongoDB does not get slow by itself. It honestly executes the rules it was given. Look closely, and behind almost every performance drop sits a specific antipattern.

This article walks through common antipatterns that show up when designing and working with MongoDB. We will also look at real, well-known cases where users ran into MongoDB problems.

1. Huge documents (a.k.a. "over-embedding")

When you first start with NoSQL, there is a tempting feeling that you are about to denormalize everything and it will run very fast. MongoDB does seem to encourage embedding, but the problem is that many people go too far.

Here is roughly what that looks like (and what not to do):

{
  "_id": 24,
  "name": "Roman",
  "orders": [
    {
      "order_id": 101,
      "total": 100,
      "items": [
        {
          "product_id": 123,
          "price": 100
        }
      ]
    }
  ]
}

Now imagine that orders can contain hundreds or thousands of elements.

The catch: MongoDB reads the entire document, even when you need a single element.

In concrete numbers, just imagine:

A 2 KB document: 2–3 ms

A 10 MB document: over 120 ms

Keep in mind that MongoDB has a 16 MB per-document limit, and sooner or later this model runs into it.

A better structure for this case:

A separate users collection

{
  "_id": 24,
  "name": "Roman"
}

A separate orders collection

{
  "_id": 101,
  "user_id": 24,
  "total": 100,
  "items": [
    {
      "product_id": 123,
      "price": 100
    }
  ]
}

2. Unbounded arrays

A special case of the antipattern above is trying to stuff everything into one array. Unlike the first case, where too much related data gets embedded inside a document, here the issue is a single array that grows without a limit.

At first it feels convenient and fast. Over time it turns into a problem that kills performance.

What not to do:

{
  "_id": 14,
  "title": "Trip Tips",
  "comments": [
    {
      "user_id": 101,
      "text": "Nice vacation!"
    },
    {
      "user_id": 122,
      "text": "It is great!"
    }
  ]
}

The point: we are talking about a potentially huge number of comments (tens, hundreds of thousands) that get added frequently.

A better layout:

A separate posts collection

{
  "_id": 14,
  "title": "Trip Tips"
}

A separate comments collection

{
  "_id": 29,
  "post_id": 14,
  "user_id": 101,
  "text": "Nice vacation!"
}

3. Deep nesting

Another common MongoDB design mistake is excessive document nesting. The idea is usually: since MongoDB stores JSON, let's nest everything as deep as possible.

In practice, it gets in the way fast.

What not to do:

{
  "_id": 12,
  "profile": {
    "personal": {
      "name": "Ivan",
      "contacts": {
        "email": "[email protected]",
        "phones": [
          {
            "type": "mobile",
            "history": [
              {
                "number": "+123",
                "meta": {
                  "verified": true
                }
              }
            ]
          }
        ]
      }
    }
  }
}

You can probably guess what this leads to. The problem is not nesting itself (moderate nesting is sometimes fine) but nesting that adds no value: data becomes harder to reach while the structures keep growing. Queries against these mega-documents get complicated, readability suffers, the chance of bugs goes up, and maintenance gets harder. On top of that, indexes start performing poorly.

A sensible way to split this up:

users (the main document)

{
  "_id": 12,
  "name": "Ivan",
  "email": "[email protected]"
}

phones (the user's phone numbers)

{
  "_id": 1,
  "user_id": 12,
  "type": "mobile",
  "number": "+123",
  "verified": true
}

phone_history (change history)

{
  "_id": 2,
  "phone_id": 1,
  "number": "+123",
  "verified": true,
  "changed_at": "2026-01-01T10:00:00Z"
}

Deep nesting in MongoDB leads to complex queries and scaling problems. Best practice: move entities that can grow or change independently — phones and their history, for example — into separate collections and link them with references.

4. Missing indexes

One of the most common ways to overload MongoDB is not using indexes. Suppose we want to run:

db.customers.find({ email: "[email protected]" })

Then we see the magic label COLLSCAN. It means MongoDB performs a full collection scan (Collection Scan), checks every document in order, and applies the filter after reading. Now suppose you have 1,000,000 documents and only one matches. The fix:

db.customers.createIndex({ email: 1 })

After creating the index, the query usually runs through IXSCAN rather than COLLSCAN. An index is a B-tree, so the search follows the structure rather than all the data. One caveat: for very small collections (hundreds of documents), COLLSCAN can be comparable in speed to using an index, so an index is not always required. On the other hand, too many indexes are also a problem, because they add write overhead.

5. Excessive collection polymorphism (without a schema)

This is one of the most underestimated MongoDB antipatterns: at small volumes it works more or less fine, and then it suddenly starts misbehaving.

The issue: different entities end up stored in one collection.

Example documents in a single collection:

  {
    "_id": 1,
    "type": "user",
    "name": "Anna",
    "email": "[email protected]"
  }

  {
    "_id": 2,
    "type": "order",
    "user_id": 1,
    "total": 100
  }

  {
    "_id": 3,
    "type": "product",
    "price": 50
  }

The whole mix ends up in a single collection, distinguished only by type.

MongoDB builds indexes at the collection level. When one collection holds different structures whose fields do not overlap (email, total, price), the indexes become sparse and of little use, and huge compound indexes appear. The result: many indexes, each used rarely, all wasting memory.

The fix: split them into separate collections.

Users:

{
  "_id": 1,
  "name": "Anna",
  "email": "[email protected]"
}

Orders:

{
  "_id": 2,
  "user_id": 1,
  "total": 100
}

Products:

{
  "_id": 3,
  "price": 50
}

Moderate polymorphism is sometimes acceptable, but only when the structures are very similar — in general, it is fine if the documents share a common set of fields and differ only slightly.

For example:

  {
    "type": "payment",
    "amount": 100,
    "method": "card"
  }

  {
    "type": "payment",
    "amount": 200,
    "method": "cash",
    "change": 50
  }

6. Reading the whole document (overfetching)

A common situation: you need the value of one specific field in a document.

Suppose we have a document with this structure:

{
  "_id": 1,
  "email": "[email protected]",
  "user_name": "olga123",
  "first_name": "Olga",
  "last_name": "Ivanova",
  "phone": "+1234567890",
  "gender": "female",
  "country": "Russia",
  "city": "Moscow",
  "postal_code": "000000",
  "address_line_1": "Street 1",
  "address_line_2": "Apt 10",
  "company": "TechCorp",
  "job_title": "Software Engineer",
  "language": "en-US",
  "timezone": "Europe/Moscow",
  "preferred_currency": "RUB",
  "status": "active"
}

And suppose we only need this user's "email".

The usual query in this case:

db.users.find({ _id: 1 })

What actually happens? Even for a point lookup on _id, MongoDB returns the whole document — and it may hold dozens of fields plus nested structures — even when you need only one field.

A better approach:

Project only the fields you need.

db.users.find({ _id: 1 }, { email: 1, _id: 0 })

That was one document. Now suppose we need the "email" of every active user. A good option:

db.users.find(
  { status: "active" },
  { email: 1, _id: 0 }
)

7. Aggregations without $match first

Suppose we need to compute user statistics and keep only active records. The collection structure might be:

{
  "_id": 1,
  "user_id": 42,
  "status": "active",
  "amount": 100
}

A common mistake is to aggregate first and filter second. The result:

[
  {
    $group: {
      _id: {
        user_id: "$user_id",
        status: "$status"
      },
      total: { $sum: "$amount" }
    }
  },
  {
    $match: { "_id.status": "active" }
  }
]

The catch: in this version MongoDB has to aggregate every document, and filtering happens after grouping. It should be:

[
 {
    $match: { status: "active" }
    },
  {
    $group: {
      _id: "$user_id",
      total: { $sum: "$amount" }
  }
 }
]

So $match first, then $group.

The key point: the usual instinct is "aggregate first, filter the result afterwards", while in MongoDB many cases need the opposite — "filter the data first, then aggregate".

Real-world cases from actual users

  1. "When data grew from 30k to 100k, everything got 30 times slower."

before: ~20 ms

after: 600 ms and above

Case analysis and discussion 2. "Mongo performance is extremely low for aggregation queries."

5+ million documents

queries fail or never finish

Case analysis and discussion 3. "Only 750 documents, but 40 seconds to a response"

Case analysis and discussion

Takeaways

There are, of course, many more antipatterns and many more fixes for each problem. This article covered only a few of the common ones.

Almost every MongoDB performance problem comes down to three things:

  1. Reading large volumes of unnecessary data
  2. Missing indexes, or indexes that are not used (or used incorrectly)
  3. Trying to think in SQL

For fun, we can phrase this takeaway as a quote from the Wolf (or Statham — whichever you prefer):

"MongoDB is fast as long as you help it stay fast!"

Useful links on antipatterns

  1. MongoDB common antipatterns
  2. MongoDB issues
  3. MongoDB best practices
  4. MongoDB avoid mistakes
  5. MongoDB analyze slow queries