MongoDB Indexing Strategies That Actually Save You Money
Real-world MongoDB indexing patterns I've used to drop query times by 100x and Atlas bills by 60%. Compound indexes, ESR rule, partial indexes, TTL — and the indexes you should never create.
Why this post exists
Three months ago, a client called me about their MongoDB Atlas bill. It had grown from $400/month to $2,800/month over a year. Their data hadn't grown 7x — their query load had. Their index strategy was the problem.
After two days of work, the bill dropped back to $850/month and average query time dropped from 340ms to 12ms. Same data, same queries, dramatically different indexes.
This post is the playbook I used.
Indexing 101 (skip if you know this)
A MongoDB index is a separate B-tree (technically B+ tree) structure that maps field values to document positions. Without an index, a query scans every document in the collection. With the right index, the database walks the tree directly to matching documents.
Indexes cost write performance (every insert/update has to update the indexes too) and disk space. The trade-off is almost always worth it for read-heavy workloads.
The ESR rule (most important thing in this post)
When building a compound index, order the fields by:
Equality → Sort → Range
For a query that filters by status (equality), sorts by totalAmount, and ranges on createdAt:
db.orders.createIndex({ status: 1, totalAmount: -1, createdAt: 1 })status first (equality match)totalAmount next (sort field)createdAt last (range filter)If you reverse this — say, put createdAt first — the index can't help with the sort. MongoDB has to load matching docs into memory and sort them. For a 10M-document collection, this is the difference between 5ms and 5 seconds.
I've fixed dozens of slow queries by just reordering fields in the index per ESR.
Compound vs single-field indexes
Rule of thumb: fewer compound indexes that serve multiple queries beat many single-field indexes.
A single compound index { userId: 1, status: 1, createdAt: -1 } serves:
{ userId } queries (uses index prefix){ userId, status } queries{ userId, status, createdAt } queriesThe "prefix rule": a compound index can serve any query that uses a left prefix of its fields. Design your compound indexes to maximize prefix matches across your common queries.
Partial indexes — the cost-saver
A partial index only indexes documents matching a filter. This is huge for collections where most documents don't need indexing.
Example: an orders collection with millions of completed orders, but you mostly query active ones. Index only documents with status in ["pending", "processing"]. If 95% of orders are "completed", this index is 5% the size of a full index. Disk savings, RAM savings, faster updates.
Use cases I see often:
TTL indexes — automatic cleanup
For data with natural expiry (sessions, OTP codes, audit logs, rate-limit records), a TTL (time-to-live) index automatically deletes documents after a duration.
The TTL monitor runs every 60 seconds and removes expired documents. No cron job needed.
I added TTL indexes to one client's audit log collection (which had grown to 200GB) and it shrunk to 12GB over two days. Atlas storage costs dropped immediately.
Wildcard indexes — when you don't know the schema
If you're querying user-defined fields (e.g., a tags map where keys vary), wildcard indexes index every value in a subdocument. Useful for flexible-schema cases. Costs more disk than a fixed-field index, but lets you query unpredictable shapes.
Text indexes for search
Don't reach for Elasticsearch immediately. MongoDB's built-in text index handles a lot of search needs.
Limitations: no fuzzy matching, no faceting, only one text index per collection. For real search you eventually want Atlas Search (built-in Lucene) or external Elasticsearch. But text indexes get you 80% of the way for free.
Indexes you should never create
Anti-patterns I see:
{ isActive: 1 } on a collection where 99% are active. The index is essentially useless; the query planner ignores it.{a:1, b:1} and {a:1} together. The second is covered by the first's prefix.How to find your slow queries
Atlas's "Performance Advisor" tab is the fastest answer for managed deployments. It analyzes your slow query log and suggests indexes.
For self-hosted MongoDB: enable profiling at slowms=100, then read the profile collection. You'll see exactly which collection scans are happening.
For a specific query, explain("executionStats") shows whether it used an index and how many docs were scanned. Look for COLLSCAN (bad — full scan) vs IXSCAN (good — index scan).
The one-page playbook
For each slow query:
explain("executionStats") — identify what the query planner is doingRun this loop on every slow query and you'll usually cut latency by 10-100x.
Where I help clients
Most of my MERN engagements include database performance work. If your MongoDB Atlas bill is climbing or queries are getting slow, I do focused 1-2 week index audits — deliverable is a documented index plan, the migration scripts, and before/after benchmarks. Contact me if that's useful.
TL;DR
explain("executionStats")You might also like
Building a Production REST API with Node.js and Express in 2026
Layered architecture, validation, error handling, auth, rate limiting, observability — the patterns I use to ship Node.js + Express APIs that don't fall over in production.
How to Hire a Blockchain Developer in Pakistan in 2026
A practical guide to hiring blockchain developers from Pakistan in 2026 — where to find them, what rates to expect, red flags, contract structure, and how to evaluate Solidity skill in 30 minutes.
React Server Components in Next.js 14: Production Patterns That Work
When to use Server Components vs Client Components in Next.js 14, the patterns that survive production, and the foot-guns I keep tripping over after shipping 8+ App Router projects.