MongoDB is fast when the working set (the data and indexes your queries touch regularly) fits in RAM, queries use good indexes, and documents are shaped for how the application reads them. Most performance problems trace back to one of those three. Here’s a practical checklist for a self-hosted MongoDB on a VPS.
1. Memory: the WiredTiger cache
WiredTiger, MongoDB’s storage engine, uses an internal cache of 50% of (RAM − 1 GB) by default, and relies on the OS file cache for the rest. On a dedicated database server, the default is usually right. On a VPS shared with your application, limit it:
# /etc/mongod.conf
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 1.5
In a container, set it explicitly, since MongoDB may see the host’s RAM instead of the container’s limit.
Check whether the cache is under pressure:
const c = db.serverStatus().wiredTiger.cache;
print(c["bytes currently in the cache"] / 1024 / 1024, "MB used of", c["maximum bytes configured"] / 1024 / 1024);
print("pages read into cache:", c["pages read into cache"]);
A fast-growing “pages read into cache” count under normal load means the working set doesn’t fit, so queries hit the disk. Add RAM, reduce indexes, or archive old data. See how much RAM a VPS needs.
2. Indexes
The biggest win in most databases. Look for COLLSCAN and in-memory SORT stages in explain(), and build compound indexes following the Equality–Sort–Range rule. See MongoDB indexes.
3. Find slow queries
db.setProfilingLevel(1, { slowms: 100 })
db.system.profile.find({ millis: { $gt: 100 } }).sort({ millis: -1 }).limit(10).pretty()
Also check mongod.log for “Slow query” entries, and db.currentOp({ secs_running: { $gt: 5 } }) for operations running right now. Kill runaway ones with db.killOp(opid).
4. Schema design for your reads
- Embed data that’s read together (an order with its line items). One read instead of several.
- Reference data that’s large, shared, or grows without bound (comments on a viral post).
- Avoid unbounded arrays: documents that grow forever get slow, and hit the 16 MB limit.
- Avoid
$lookupin hot paths. If you join constantly, reconsider the model. - Use projections to return only the fields you need:
find({…}, { name: 1, price: 1 }).
5. Query patterns
- Paginate with a range on an indexed field (
{ _id: { $gt: lastId } }), not largeskip()values:skip(100000)still walks 100,000 entries. - Avoid unanchored regexes (
/abc/) on large collections. Anchored prefixes (/^abc/) can use an index. - Use
bulkWriteandinsertManyfor batches, instead of thousands of single writes. - In aggregation pipelines, put
$matchand$projectas early as possible.
6. Write concern and journaling
The default write concern { w: "majority" } on replica sets waits until most members have the write. That’s safe, and the right default. For non-critical bulk data (logs, metrics), { w: 1 } lowers latency. Never disable journaling for data you care about.
7. Connections
Each connection uses memory (about 1 MB). Use the driver’s connection pool (one client per process, reused), and set maxPoolSize sensibly. Thousands of connections from many app processes waste RAM.
db.serverStatus().connections
8. OS and disk settings
- Filesystem: XFS is recommended for WiredTiger.
- Transparent Huge Pages: follow the recommendation for your MongoDB version. Older versions want THP disabled, while MongoDB 8.0 recommends enabling it. Check the production notes for your release.
- ulimits: raise open files and processes (
LimitNOFILE=64000in the systemd unit; the official packages set this). - Swappiness: low (
vm.swappiness = 1), so the database isn’t swapped out. - Disk: fast SSD storage. MongoDB is sensitive to I/O latency when data doesn’t fit in RAM.
See sysctl tuning and ulimit and open files.
9. Monitor
| Metric | Command / source |
|---|---|
| Ops per second, queues | mongostat |
| Time per collection | mongotop |
| Cache usage, evictions | db.serverStatus().wiredTiger.cache |
| Replication lag | rs.printSecondaryReplicationInfo() |
| Slow queries | Profiler, mongod.log |
| Dashboards | Prometheus mongodb_exporter + Grafana |
See Grafana and Prometheus monitoring.
10. Scale when tuning isn’t enough
- Vertical: more RAM is usually the best upgrade for MongoDB.
- Read scaling: read from secondaries with
readPreference: "secondaryPreferred"for reporting (accepting slightly stale data). See MongoDB replica sets. - Sharding: for datasets or write loads beyond one server. Choose the shard key carefully, since it’s hard to change later.
- Archive old data to another collection or cheaper storage.
Quick checklist
| Area | Check |
|---|---|
| RAM | Working set fits; cacheSizeGB set when sharing the server |
| Indexes | No COLLSCAN on hot queries; ESR order; no unused indexes |
| Schema | Embed what’s read together; no unbounded arrays |
| Queries | Projections, range pagination, early $match |
| Connections | One pooled client per process |
| OS | XFS, ulimits, low swappiness, THP per version |
| Monitoring | mongostat, profiler, exporter |
Related: MongoDB backup and restore and PostgreSQL vs MongoDB.