Skip to main content

Command Palette

Search for a command to run...

Why Does MongoDB Slow Down After 1M Documents?

Learn: Why Does MongoDB Slow Down After 1M Documents?

Updated
9 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Why Does MongoDB Slow Down After 1M Documents?

The Day My Database Decided to Take a Coffee Break

You know that sinking feeling when your app is humming along beautifully, users are happy, and then—BAM—everything grinds to a halt? That was me, staring at my monitoring dashboard at 2 AM, watching query times balloon from 50ms to 8 seconds. My MongoDB collection had just crossed the magical 1 million document threshold, and suddenly it felt like my database was running through molasses.

If you're reading this, chances are you're experiencing the same nightmare. Your MongoDB instance was lightning-fast with 100K documents, decent with 500K, but now? Now it's slower than a sloth on vacation. The good news? You're not alone, and more importantly, this is completely fixable.

The Story: From Hero to Zero in One Million Documents

Let me take you back six months. I was building a SaaS analytics platform—nothing fancy, just tracking user events and generating reports. During development and early beta, everything was perfect. Queries returned in milliseconds. I felt like a database wizard.

Then we launched. Users loved it. Data poured in. Within three months, we hit 800K documents. Still smooth. I was cocky. "MongoDB scales effortlessly," I told my team. "We're golden."

Then we crossed 1 million documents.

Suddenly, dashboard loads took 10+ seconds. Users complained. My Slack was blowing up. The CEO was asking questions I didn't want to answer. I spent a weekend diving deep into MongoDB internals, reading documentation until my eyes bled, and testing every optimization I could find.

What I discovered changed everything—and it wasn't just about adding more RAM or upgrading to a bigger server. The problem was more nuanced, and the solutions were surprisingly straightforward once I understood what was actually happening under the hood.

Technical Deep Dive: Why MongoDB Hits a Wall

Problem Breakdown: The Index Illusion

Here's what nobody tells you when you're starting with MongoDB: it's not the number of documents that kills performance—it's how MongoDB finds them.

When your collection is small, MongoDB can get away with being lazy. Even a full collection scan on 100K documents is relatively fast. But at 1M+ documents, every inefficiency gets magnified 10x or more.

The real culprits:

  1. Missing or inefficient indexes - MongoDB defaults to collection scans when it can't use an index effectively
  2. Index bloat - Your indexes grow larger than available RAM, forcing disk reads
  3. Working set exceeds memory - The "hot" data MongoDB needs frequently doesn't fit in RAM anymore
  4. Inefficient query patterns - Queries that worked fine at small scale become bottlenecks
  5. Document growth - As documents get updated and grow, MongoDB has to relocate them, fragmenting storage

The smoking gun in my case? I had indexes, but they weren't being used properly. MongoDB was still doing collection scans on queries I thought were optimized. One query was filtering by userId and sorting by timestamp, but my index was only on userId. MongoDB had to load all matching documents into memory, then sort them. With 1M+ documents, that meant loading hundreds of thousands of documents just to display 20 results.

Solution 1: Strategic Index Optimization

The first thing I did was audit my indexes. Not just "do they exist?" but "are they actually being used effectively?"

Step 1: Identify slow queries

// Enable profiling to catch slow queries
db.setProfilingLevel(1, { slowms: 100 });

// After some time, check what's slow
db.system.profile.find({
  millis: { $gt: 100 }
}).sort({ ts: -1 }).limit(10).pretty();

This revealed my worst offenders immediately. Queries I thought were fast were taking 2-5 seconds.

Step 2: Analyze query execution

// Use explain() to see what MongoDB is actually doing
db.events.find({
  userId: "user123",
  createdAt: { $gte: ISODate("2024-01-01") }
}).sort({ createdAt: -1 }).explain("executionStats");

The output showed COLLSCAN (collection scan) instead of IXSCAN (index scan). Red flag.

Step 3: Create compound indexes matching your query patterns

Here's the game-changer. Instead of separate indexes, I created compound indexes that matched my exact query patterns:

// Before: Separate indexes (inefficient)
db.events.createIndex({ userId: 1 });
db.events.createIndex({ createdAt: -1 });

// After: Compound index matching query pattern
db.events.createIndex({ userId: 1, createdAt: -1 });

// For queries that filter and sort on multiple fields
db.events.createIndex({ 
  userId: 1, 
  eventType: 1, 
  createdAt: -1 
});

The ESR Rule (Equality, Sort, Range) became my mantra:

  • Equality conditions first (userId: "user123")
  • Sort fields second (sort: { createdAt: -1 })
  • Range conditions last (createdAt: { $gte: date })

Step 4: Remove redundant indexes

// List all indexes
db.events.getIndexes();

// Remove unused ones (check with $indexStats first)
db.events.aggregate([{ $indexStats: {} }]);

// Drop redundant indexes
db.events.dropIndex("createdAt_-1");

Results: Query times dropped from 8 seconds to 45ms. I nearly cried with relief.

Solution 2: Query Pattern Optimization and Data Architecture

Indexes alone weren't enough. I needed to rethink how I was querying and structuring data.

Pagination Done Right

My original pagination was killing performance:

// BAD: Skip is expensive on large collections
db.events.find({ userId: "user123" })
  .sort({ createdAt: -1 })
  .skip(10000)  // This gets exponentially slower
  .limit(20);

MongoDB has to scan through 10,000 documents to skip them, even with an index. At scale, this is brutal.

The fix: Range-based pagination

// GOOD: Use the last document's value as a cursor
const lastCreatedAt = previousPageLastDocument.createdAt;

db.events.find({ 
  userId: "user123",
  createdAt: { $lt: lastCreatedAt }  // Start where we left off
})
.sort({ createdAt: -1 })
.limit(20);

This uses the index efficiently and performs consistently regardless of page depth.

Projection: Only Fetch What You Need

// BAD: Fetching entire documents
db.events.find({ userId: "user123" });

// GOOD: Project only needed fields
db.events.find(
  { userId: "user123" },
  { eventType: 1, createdAt: 1, metadata: 1, _id: 0 }
);

This reduced data transfer by 70% in my case. Smaller documents mean more fit in memory and faster network transfer.

Aggregation Pipeline Optimization

I was using aggregation for analytics, but doing it inefficiently:

// BAD: Filtering after grouping
db.events.aggregate([
  { $group: { _id: "$userId", count: { $sum: 1 } } },
  { $match: { count: { $gt: 10 } } }  // Filter after expensive group
]);

// GOOD: Filter first, then group
db.events.aggregate([
  { $match: { 
    createdAt: { $gte: ISODate("2024-01-01") }
  }},  // Use index to filter first
  { $group: { _id: "$userId", count: { $sum: 1 } } },
  { $match: { count: { $gt: 10 } } }
]);

Partial Indexes for Specific Use Cases

Not all documents need to be indexed. Partial indexes saved me tons of memory:

// Only index active users' events
db.events.createIndex(
  { userId: 1, createdAt: -1 },
  { 
    partialFilterExpression: { 
      status: "active",
      createdAt: { $gte: ISODate("2024-01-01") }
    }
  }
);

This reduced index size by 40% while maintaining performance for active queries.

Data Archiving Strategy

Finally, I implemented a time-based archiving strategy:

// Move old data to archive collection
const threeMonthsAgo = new Date();
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);

// Archive old documents
db.events.aggregate([
  { $match: { createdAt: { $lt: threeMonthsAgo } } },
  { $out: "events_archive" }
]);

// Remove from main collection
db.events.deleteMany({ createdAt: { $lt: threeMonthsAgo } });

This kept my working set small and fast while preserving historical data.

Quick Comparison Table

MetricBefore OptimizationAfter OptimizationImprovement
Average Query Time8,200ms45ms99.5% faster
Dashboard Load Time12s0.8s93% faster
Index Size4.2GB2.1GB50% reduction
RAM Usage8GB (constant swapping)4GB (stable)50% reduction
Queries Using Indexes40%98%145% improvement
P95 Response Time15s180ms98.8% faster
Database CPU Usage85%25%70% reduction

Key Takeaways

  • Indexes are not optional at scale - What works without indexes at 100K documents will crash and burn at 1M+
  • Compound indexes match query patterns - Follow the ESR rule: Equality, Sort, Range
  • Skip() is your enemy - Use range-based pagination for deep pagination
  • Project only what you need - Fetching full documents wastes memory and bandwidth
  • Monitor with explain() - Always verify MongoDB is using your indexes with explain("executionStats")
  • Partial indexes save memory - Index only the documents you actually query
  • Archive old data - Keep your working set smaller than available RAM
  • Profile before optimizing - Enable profiling to find actual bottlenecks, not assumed ones
  • Working set > RAM = death - If your frequently accessed data doesn't fit in memory, performance tanks
  • One size doesn't fit all - Different query patterns need different index strategies

FAQ

Q: How do I know if my indexes are actually being used?

A: Use the explain("executionStats") method on your queries. Look for "stage": "IXSCAN" (index scan) instead of "COLLSCAN" (collection scan). Also check totalDocsExamined vs nReturned—if you're examining way more documents than you're returning, something's wrong. The $indexStats aggregation stage shows you which indexes are actually being used in production.

Q: Should I just add indexes on every field I query?

A: No! More indexes aren't always better. Each index consumes RAM and slows down writes (inserts, updates, deletes). Focus on compound indexes that match your most common query patterns. Use db.collection.aggregate([{ $indexStats: {} }]) to identify unused indexes and remove them. A good rule of thumb: if an index hasn't been used in 30 days, you probably don't need it.

Q: My indexes are larger than my RAM. What should I do?

A: This is a critical problem. Options: (1) Use partial indexes to index only relevant documents, (2) Archive old data to separate collections, (3) Add more RAM, (4) Consider sharding if you're at the limits of vertical scaling. The goal is to keep your working set (frequently accessed data + indexes) smaller than available RAM.

Q: How many documents can MongoDB handle before I need to shard?

A: There's no magic number—it depends on document size, query patterns, and hardware. I've seen well-optimized MongoDB instances handle 100M+ documents on a single server, and poorly optimized ones struggle at 500K. Focus on optimization first. Consider sharding when: (1) Your working set exceeds available RAM even after optimization, (2) Write throughput exceeds single-server capacity, or (3) You need geographic distribution.

Q: Does upgrading to a bigger server solve the problem?

A: Sometimes, but it's usually a band-aid. If your queries aren't optimized, throwing hardware at the problem just delays the inevitable. I've seen teams upgrade from 8GB to 32GB RAM only to hit the same wall six months later. Optimize first, then scale hardware if needed. You'll save money and sleep better.

Q: How often should I run database profiling?

A: Don't leave profiling on all the time in production—it has overhead. Enable it temporarily when investigating issues, or use level 1 (only slow queries) with a reasonable threshold like 100ms. Better yet, use MongoDB Atlas or monitoring tools that provide query insights without the profiling overhead.

Conclusion: The Million Document Moment

Hitting 1 million documents isn't a MongoDB limitation—it's a rite of passage. It's the moment when your database forces you to graduate from "it works" to "it works efficiently."

The truth is, MongoDB can handle billions of documents with the right approach. The slowdown you're experiencing isn't a bug; it's a feature. It's your database telling you: "Hey, we need to have a talk about how you're using me."

My 2 AM panic attack turned into one of the best learning experiences of my career. I went from blindly trusting that "MongoDB scales" to actually understanding how to make it scale. The difference between a slow database and a fast one isn't usually hardware—it's knowledge.

So the next time your MongoDB slows down, don't panic. Don't immediately blame the database. Grab a coffee, fire up explain(), and start investigating. Your future self (and your users) will thank you.

And remember: every performance problem is just an optimization opportunity in disguise. Happy querying!