What's Actually Happening
MongoDB index builds can take hours or days on large collections, blocking operations or consuming excessive resources. Foreground builds block all operations, while background builds can still impact performance.
The Error You'll See
Foreground build blocking:
```bash $ mongo --eval "db.collection.createIndex({field: 1})"
# Operation hangs, other queries fail: Error: waiting for replication timed out ```
Build progress not visible:
```bash $ db.currentOp({"command.createIndexes": true})
{ "inprog": [ { "opid": 12345, "command": { "createIndexes": "collection", "indexes": [{key: {field: 1}}] }, "msg": "Index Build: building index", "progress": { "done": 45, "total": 10000000 # 10 million documents } } ] } ```
Logs show slow progress:
```bash $ tail -f /var/log/mongodb/mongod.log
2026-04-16T00:40:00Z I INDEX [conn123] build index on: collection field 2026-04-16T00:45:00Z I INDEX [conn123] index build progress: 5% complete 2026-04-16T00:50:00Z I INDEX [conn123] index build progress: 10% complete ```
Why This Happens
- 1.Foreground build - Blocks all reads/writes during build
- 2.Large collection - Millions of documents to scan
- 3.Insufficient memory - Sorting/indexing runs out of RAM
- 4.No background option - MongoDB 4.0+ removed background builds
- 5.Concurrent builds - Multiple indexes competing for resources
- 6.Slow disk I/O - Disk bottleneck during build
Step 1: Check Current Index Build Status
```bash # List all index builds in progress db.currentOp({ $or: [ {"command.createIndexes": {$exists: true}}, {"command.createIndexes": {$exists: false}, "msg": /^Index Build/} ] })
# Get detailed build progress db.currentOp({"msg": "Index Build"})
# Check build progress percentage db.currentOp({"msg": "Index Build"}).inprog.forEach(function(op) { print("OpID: " + op.opid); print("Progress: " + op.progress.done + "/" + op.progress.total); print("Percentage: " + (op.progress.done / op.progress.total * 100).toFixed(2) + "%"); });
# Check collection size db.collection.stats()
# Estimate documents to index db.collection.count()
# Check if index exists already db.collection.getIndexes() ```
Step 2: Check Available Memory
```bash # Check MongoDB memory usage db.serverStatus().mem
# Check WiredTiger cache db.serverStatus().wiredTiger.cache
# System memory free -h
# Check if index fits in memory # Rule: index build needs ~1.5x index size in RAM for sorting db.collection.stats().indexSizes
# If indexSize > available RAM, build will be slow ```
Step 3: Use Build Index Command (MongoDB 4.2+)
```javascript // MongoDB 4.2+ uses createIndexes command with options db.runCommand({ createIndexes: "collection", indexes: [ { key: {field: 1}, name: "field_1" } ] })
// Build multiple indexes in one command (more efficient) db.runCommand({ createIndexes: "collection", indexes: [ {key: {field1: 1}, name: "field1_1"}, {key: {field2: 1}, name: "field2_1"}, {key: {field3: -1}, name: "field3_-1"} ] })
// Check commit quorum (MongoDB 4.4+) db.runCommand({ createIndexes: "collection", indexes: [{key: {field: 1}}], commitQuorum: "majority" // Wait for majority of nodes })
// Or use "votingMembers" for all voting members commitQuorum: "votingMembers"
// Or specify number commitQuorum: 2 ```
Step 4: Build Index During Low Traffic
```bash # Schedule index build during maintenance window # Use mongosh script:
// Check current load db.serverStatus().connections db.serverStatus().opcounters
// Build during off-hours db.collection.createIndex({field: 1})
// Monitor build progress setInterval(function() { var ops = db.currentOp({"msg": "Index Build"}).inprog; if (ops.length > 0) { print("Progress: " + ops[0].progress.done + "/" + ops[0].progress.total); } else { print("No index builds in progress"); } }, 30000); // Every 30 seconds
# Or use cron job to start build # 0 2 * * * mongosh --eval "db.collection.createIndex({field: 1})" ```
Step 5: Kill Stuck or Slow Index Build
```bash # Find the operation ID db.currentOp({"msg": "Index Build"})
// Output shows opid like: 12345 or "shard01:12345"
# Kill the index build db.killOp(12345)
// For replica sets, may need to kill on primary db.killOp("shard01:12345")
# Verify build stopped db.currentOp({"msg": "Index Build"}) // Should be empty
# Check if index was partially created db.collection.getIndexes()
# Drop partially built index db.collection.dropIndex("field_1") ```
Step 6: Build Index on Secondary First
```bash # For replica sets, build on secondary first (MongoDB 4.0+)
# Step 1: Stop secondary from applying index builds # On secondary, set: cfg = rs.conf() cfg.settings.chainingAllowed = false // Prevent chaining rs.reconfig(cfg)
# Step 2: Build index on secondary (standalone mode) # Stop secondary, restart without replSet mongod --dbpath /data --port 27018
# Connect and build index mongo --port 27018 db.collection.createIndex({field: 1})
# Step 3: Restart secondary with replSet mongod --dbpath /data --replSet rs0 --port 27017
# Step 4: Repeat on all secondaries
# Step 5: Finally build on primary (fast rollback) db.collection.createIndex({field: 1})
# Alternative: Use rolling index build # rs.stepDown() on primary, build on new primary ```
Step 7: Optimize Index Build Performance
```javascript // Pre-sort data if possible // Sort reduces random disk I/O
// Use partial index to reduce documents indexed db.collection.createIndex( {field: 1}, {partialFilterExpression: {status: "active"}} )
// Sparse index (only index documents with field) db.collection.createIndex( {field: 1}, {sparse: true} )
// Use smaller index keys // Keep indexed fields small
// Compound index order matters db.collection.createIndex( {field1: 1, field2: 1}, // field1 should have higher cardinality {name: "compound_idx"} )
// Check index size after build db.collection.stats().indexSizes ```
Step 8: Handle Large Collections
```bash # For very large collections:
# Option 1: Build on empty collection, then insert data db.collection.createIndex({field: 1}) // Then bulk insert data (index builds during insert)
# Option 2: Split collection // Create new collection with index, migrate data in batches db.newCollection.createIndex({field: 1})
for (var i = 0; i < 10; i++) { db.collection.find({batchId: i}).forEach(function(doc) { db.newCollection.insert(doc); }); }
# Option 3: Use TTL index for time-series data db.collection.createIndex( {createdAt: 1}, {expireAfterSeconds: 3600} )
# Option 4: Archive old data db.collection.aggregate([ {$match: {createdAt: {$lt: new Date("2025-01-01")}}}, {$out: "archive_collection"} ]) db.collection.deleteMany({createdAt: {$lt: new Date("2025-01-01")}}) ```
Step 9: Monitor Build Resource Usage
```bash # Check CPU usage during build top -p $(pgrep mongod)
# Check disk I/O iostat -x 1
# Check MongoDB process db.serverStatus().process
# Check WiredTiger statistics db.serverStatus().wiredTiger
# Monitor connections during build db.serverStatus().connections
# Check for write conflicts db.serverStatus().metrics.commands
# Use mongostat for real-time monitoring mongostat --host localhost --port 27017 5
# Use mongotop for collection-level I/O mongotop --host localhost --port 27017 10 ```
Step 10: Use Index Build Quorum Settings
```javascript // MongoDB 4.4+ commit quorum options
// Build index without waiting for all nodes db.runCommand({ createIndexes: "collection", indexes: [{key: {field: 1}}], commitQuorum: 0 // Don't wait for any nodes })
// Wait for specific number of nodes commitQuorum: 2 // Wait for 2 nodes to be ready
// Wait for majority commitQuorum: "majority"
// Wait for all voting members commitQuorum: "votingMembers"
// Check current quorum during build db.currentOp({"msg": "Index Build"}).inprog[0].commitQuorum
// Check which nodes have built the index rs.status().members.forEach(function(m) { print(m.name + ": " + m.state); }); ```
MongoDB Index Build Checklist
| Check | Command | Expected |
|---|---|---|
| Build progress | db.currentOp() | Shows percentage |
| Collection size | db.collection.count() | Document count |
| Memory available | db.serverStatus().mem | Sufficient RAM |
| Disk I/O | iostat -x 1 | No bottleneck |
| Existing indexes | db.collection.getIndexes() | No duplicates |
| Build time | currentOp.progress | Estimate completion |
Verify the Fix
```bash # After index build completes
# 1. Check index exists db.collection.getIndexes() // Should show new index
# 2. Verify index size db.collection.stats().indexSizes
# 3. Test query uses index db.collection.find({field: "value"}).explain("executionStats") // Should show winningPlan using index
# 4. Check query performance db.collection.find({field: "value"}).explain("executionStats").executionStats.executionTimeMillis // Should be fast
# 5. Verify no performance impact db.serverStatus().opcounters // Normal operation rates
# 6. Check index is replicating rs.status() // All nodes should have index ```
Related Issues
- [Fix MongoDB Query Timeout](/articles/fix-mongodb-query-timeout)
- [Fix MongoDB Slow Query Performance](/articles/fix-mongodb-slow-query-performance)
- [Fix MongoDB Index Not Used](/articles/fix-mongodb-index-not-used)