← Back to BlogDatabase

MongoDB Performance Tuning: 10 Expert Tips

Hammad Nadir April 20, 2025 9 min read
MongoDB Performance Tuning: 10 Expert Tips

Discover the indexing strategies, query optimizations, and schema design patterns that transformed a 3-second query into 12 milliseconds.

MongoDB performance problems have a narrow set of causes, and they appear in a predictable order as a collection grows. Almost every slow query traces to a missing or wrong index, a schema modeled for writing rather than reading, or an application pattern that issues many small queries where one would do.

Here is how to find which of those you have, and what to do about each.

Start With explain, Not With Guesses

Before changing anything, run the slow query through explain with execution statistics and read three numbers.

The stage tells you how the query was served. COLLSCAN means every document was examined; on anything user-facing that is your problem. IXSCAN means an index was used.

The ratio of documents examined to documents returned tells you how selective the index was. Examining fifty thousand documents to return twenty means the index narrowed the search poorly, which usually means the field order is wrong or a field is missing from it.

The presence of an in-memory sort tells you the sort was not served by an index. This has a hard memory limit and will simply fail on large result sets, which is a failure that appears suddenly as data grows rather than degrading gradually.

Enable the profiler on a threshold in production and review what it collects weekly. The queries that matter are rarely the ones you would predict.

Compound Index Field Order Follows a Rule

This is the highest-leverage piece of MongoDB knowledge and the most commonly gotten wrong.

Order compound index fields as equality, then sort, then range.

Fields matched by exact value come first. Fields used for sorting come next. Fields queried by range — greater than, less than, date windows — come last.

The reason is that an index is an ordered structure. Equality matches jump directly to a contiguous region. Within that region, entries are already ordered by the next field, which is what lets a sort be served without an in-memory pass. A range field placed before a sort field breaks that ordering, and the sort must then be done in memory.

A query filtering by status, sorting by creation date, and limiting to a range of scores wants an index on status first, creation date second, score third. Putting score before creation date produces an index that is used but does not serve the sort, and the query will be several orders of magnitude slower than it looks like it should be.

A useful consequence: an index on multiple fields also serves queries on any prefix of those fields. One well-ordered compound index frequently replaces three single-field indexes.

Use Covered Queries Where the Access Pattern Is Hot

If every field a query needs — both the filter fields and the returned fields — exists in an index, MongoDB answers from the index alone and never touches the documents. This eliminates the random reads that usually dominate query time.

Achieving it requires projecting only the fields in the index, and explicitly excluding the identifier field unless it is part of the index.

This is not worth doing everywhere; wide indexes cost write throughput and memory. It is worth doing for the two or three query patterns that run constantly, where it can be a dramatic improvement.

Every Index Costs Write Throughput

Indexes are not free, and the cost is paid on every insert, update, and delete, which must maintain every affected index.

Collections accumulate indexes over time, usually added during performance investigations and never removed. Check index usage statistics and drop the ones with no accesses. An index that has never been used since the last restart is pure overhead.

Watch for redundancy. An index on a single field is redundant if another index already begins with that field. Removing the redundant one is free performance on every write.

The working set matters more than raw index count: indexes are most effective when they fit in memory. Once your indexes exceed available RAM, queries begin reading index pages from disk and performance degrades sharply rather than gradually. Monitor for this, because it is the transition that turns a healthy database into a slow one overnight.

Model for the Queries You Actually Run

Schema design is where the largest performance differences originate, and the governing question is not what the data looks like but which queries run most often and how many round trips each requires.

Embed when data is accessed together and bounded in size. An address inside a user document, line items inside an order — one read retrieves everything the page needs.

Reference when data is large, unbounded, or shared. Never embed an unbounded array: a user document containing every order that user has placed grows without limit and will eventually hit the sixteen megabyte document ceiling, which happens first on your largest customer.

Unbounded array growth causes a subtler problem well before that limit. A document that grows beyond its allocated space must be relocated on disk, and every index pointing at it must be updated. A collection with heavy array appends can spend most of its write capacity on relocation.

Duplicating a few fields is often correct rather than a normalization failure. Storing the product name and price on an order line is a record of the transaction as it occurred, and it removes a lookup from the most frequently rendered page in an e-commerce application.

Eliminate the N+1 Pattern

The most common application-level cause of slow pages is a loop issuing one query per item — fetch fifty orders, then fetch each order's customer individually. Fifty-one round trips where two would do.

Collect the identifiers and issue a single query matching all of them, then join in application memory. Or use an aggregation lookup stage to have the database do it.

This pattern is invisible in development with twenty records and dominant in production with twenty thousand. Query logging with a count per request surfaces it immediately.

Write Aggregations That Filter Early

Aggregation pipelines execute in order, and each stage processes what the previous stage emitted. Put match and limit stages as early as possible so subsequent stages operate on the smallest possible set.

A pipeline that unwinds an array, groups, and then filters is doing all of that work on the entire collection before discarding most of it. Moving the filter to the front frequently changes execution time by orders of magnitude.

The first match stage in a pipeline can use an index. Later ones generally cannot, which makes the position of that first filter unusually important.

Project away fields you do not need early as well, since it reduces the data carried through every subsequent stage.

Paginate With Cursors, Not Offsets

Skipping records to reach a page requires walking every skipped record. Page one is instant, page five hundred is slow, and the cost grows linearly with depth.

Range-based pagination — remembering the last value seen and querying for values after it — uses the index directly and costs the same regardless of depth. It also behaves correctly when records are inserted while a user is paging, which offset pagination does not; offset pagination silently duplicates and skips items under concurrent writes.

Bulk Operations Instead of Loops

Inserting or updating documents one at a time in a loop pays network round-trip latency per operation. Bulk operations batch them into a single request.

For large imports this is commonly a ten-fold improvement or better. Use unordered bulk writes when operations are independent, so one failure does not stop the rest.

Read the Connection Pool Settings

A default connection pool sized for a single process becomes a bottleneck across several application instances, and connection exhaustion presents as slow queries even though the database itself is idle — which sends people investigating the wrong system entirely.

Size the pool against your concurrency, and monitor for requests waiting to check out a connection. That metric is the clearest signal that the pool, not the database, is your constraint.

Where you have replicas, route analytical and reporting queries to secondaries to keep the primary available for user-facing traffic. Be explicit that this means accepting eventual consistency for those reads, which is fine for reports and not fine for a read immediately following a write.

The Order to Work In

Profile to find the actual slow queries. Run explain on each and look for collection scans and in-memory sorts. Add or reorder compound indexes following equality, sort, range. Fix N+1 patterns in the application. Then reconsider the schema for the access patterns that remain slow.

Most collections get the majority of their available improvement from the first three steps, and the three-second query becoming twelve milliseconds is almost always a compound index in the right order rather than anything more sophisticated.

MongoDB Database Performance Indexing