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 $lookup in 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 large skip() values: skip(100000) still walks 100,000 entries.
  • Avoid unanchored regexes (/abc/) on large collections. Anchored prefixes (/^abc/) can use an index.
  • Use bulkWrite and insertMany for batches, instead of thousands of single writes.
  • In aggregation pipelines, put $match and $project as 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=64000 in 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.

  • MongoDB
  • Performance
  • Database