MySQL vs MongoDB: the “NoSQL because trendy” mistake that kills VPS performance

Was this helpful?

You rent a modest VPS. Two vCPU, a few gigs of RAM, “SSD” storage that turns out to be “somebody else’s noisy neighbor,” and a production workload that
isn’t fancy—just users, orders, sessions, and a few background jobs. Then someone says: “Let’s use MongoDB, it’s NoSQL, it scales.” You ship it. It works.
For a week. Then latency spikes, load average climbs, and your graphs look like a seismograph during a minor apocalypse.

This failure mode is boringly common: a database choice made on vibes instead of physics. The physics are RAM, disk I/O, and how your engine behaves when it
doesn’t fit in memory. On small VPSes, that’s the whole game.

What actually kills VPS performance

“Database performance” on a VPS usually isn’t about exotic query plans or theoretical throughput. It’s about what happens when your working set doesn’t fit in RAM,
your disk is slower than you think, and your configuration assumes a server class box instead of a budget VM.

1) RAM misses become disk reads, and disk reads become latency

MySQL (InnoDB) and MongoDB (WiredTiger) both rely heavily on caching. When your cache is too small, they page data in from storage constantly. On a shared VPS,
that storage might be “SSD” with a queue depth held hostage by another tenant. Your app sees it as random 200–800ms stalls and starts retrying. Retries increase
concurrency. Concurrency increases disk pressure. Now you have a feedback loop with the personality of a wood chipper.

2) Overcommit and the OOM killer do not negotiate

On small machines, “just give the database more cache” often means “let Linux kill it later.” MongoDB in particular can look stable until the working set grows
slightly, then it starts competing with the OS page cache and everything else. When the kernel decides memory is gone, it doesn’t send a polite email. It picks a
process and ends it.

3) Write amplification is the silent budget murderer

Databases don’t write what you think they write. They write WAL/journal, they write dirty pages, they compact/merge, they fsync, they do metadata updates.
The “one document update” you imagined can be multiple small random writes plus background maintenance. On VPS storage, random writes are expensive, and sustained
random writes are how you find out your “SSD” is really “a RAID card from 2013 shared with 40 strangers.”

4) The wrong data model turns CPU into heat and I/O into tears

The most expensive query is the one you didn’t index because you assumed “NoSQL means no schema means no planning.” The second most expensive query is the one
that can’t be indexed effectively because the data model encourages shape-shifting documents, deep nesting, or “just store arrays and filter in application code.”
This is where MongoDB gets used badly: not because MongoDB is bad, but because it’s forgiving until it suddenly isn’t.

One quote that belongs on every on-call rotation: “Hope is not a strategy.” — General Gordon R. Sullivan.

Fast diagnosis playbook

When a VPS melts down, you don’t have time for philosophical debates about relational theory. You need a triage sequence that finds the bottleneck in minutes.
Here’s the order that wins most incidents.

First: is it CPU, memory, or disk?

  • Disk-bound: high iowait, slow fsync, queue depth rising, latency spikes correlate with write bursts.
  • Memory-bound: swap activity, major page faults, OOM kills, database cache misses, sudden performance cliff as dataset grows.
  • CPU-bound: high user CPU, slow queries that are compute-heavy, regex scans, JSON parsing, compression, encryption overhead.

Second: is the database the bottleneck or the application?

  • Connection pool saturation and thread pile-ups often look like “database slow” but are “app mis-sized.”
  • Lock contention looks like slow queries but is often one hot row/document causing everyone else to queue.

Third: identify the top 1–3 queries/operations doing damage

  • In MySQL: slow query log + EXPLAIN + InnoDB status.
  • In MongoDB: profiler + explain() + currentOp.

Fourth: verify the cache behavior

  • InnoDB: buffer pool hit rate, dirty page flushing, redo log pressure.
  • WiredTiger: cache usage, eviction pressure, checkpoint behavior.

Joke #1: If you “fix” latency by restarting the database every morning, you’ve invented a scheduler, not reliability.

MySQL vs MongoDB: the real tradeoffs

MySQL: predictable, boring, and brutally efficient when modeled well

MySQL with InnoDB is a general-purpose OLTP workhorse. It likes structured data, stable query patterns, and well-chosen indexes. It’s not fashionable.
That’s a feature.

On a small VPS, MySQL often wins because:

  • InnoDB caching is straightforward: you size the buffer pool, and you can usually reason about hit rates.
  • Query optimizer + indexes handle many access patterns without forcing you into embedding everything into one record.
  • Operational tooling is mature: slow logs, performance_schema, common dashboards, and predictable backup/restore workflows.
  • Data size overhead is often lower than JSON-heavy document models, especially if you normalize repetitive fields.

MongoDB: powerful, flexible, and easy to misuse into a disk-thrashing machine

MongoDB shines when your data is naturally document-shaped, when you need flexible schema evolution, when denormalization simplifies reads, and when you accept the
operational tradeoffs. But on small VPSes, it’s often chosen for the wrong reason: “NoSQL scales.” That’s not a plan; that’s a horoscope.

MongoDB can be perfectly fast on a VPS if:

  • Your working set fits in RAM or you have fast, consistent storage.
  • You design indexes like your job depends on them (it does).
  • You control document growth and avoid pathological updates.
  • You understand WiredTiger eviction and cache sizing.

The VPS reality: small boxes punish overhead

Document databases are often used with bigger documents than necessary. Bigger documents mean fewer fit in cache. Fewer fit in cache means more reads from disk.
On a constrained VPS, every cache miss is a tax you pay with interest.

Meanwhile, MySQL’s “boring tables” tend to be compact, indexed, and accessed through predictable query plans. It’s not magic; it’s just less overhead per unit of useful data.

Transactions, constraints, and correctness under pressure

For many production apps, the hidden performance killer isn’t speed—it’s the compensating logic you add when correctness is uncertain. MySQL’s constraints,
foreign keys (when used carefully), and transactional semantics reduce the amount of “verify in application code” work.

MongoDB has solid transaction support in modern versions, but multi-document transactions add overhead and can shift your bottleneck to locks, oplog pressure, or
replication lag. If your app frequently needs cross-entity invariants, you’ll either embed aggressively (which creates big hot documents) or you’ll build a relational
system on top of a document store. That’s rarely the cheapest path on a VPS.

Replication and durability costs are real costs

If you run MongoDB properly, you run a replica set. If you run MySQL properly, you run replication (or at least backups and binary logs). Both have overhead.
But MongoDB’s “default correctness posture” often drags teams into running three nodes even when they only budgeted for one. On a small deployment, that’s not
just cost—it’s operational complexity and more failure surfaces.

Historical context and interesting facts

A little history helps explain why these systems behave the way they do, and why “NoSQL because trendy” is a recurring organizational mistake.

  1. MySQL was created in the mid-1990s and became the default database of the early web because it was fast, simple, and easy to deploy.
  2. InnoDB became MySQL’s default storage engine in MySQL 5.5 (2010), bringing crash recovery and row-level locking as the standard experience.
  3. MongoDB first appeared in 2009, born in an era when developers were fighting rigid schemas and scaling SQL horizontally was still considered “hard.”
  4. The “NoSQL” term was popularized around 2009 as a banner for alternatives to relational databases, not as a guarantee of performance.
  5. MongoDB’s WiredTiger storage engine became default in MongoDB 3.2 (2016), replacing MMAPv1 and drastically changing memory and disk behavior.
  6. MySQL’s JSON type arrived in MySQL 5.7 (2015), a quiet admission that “semi-structured” data is normal—and relational engines can handle it.
  7. Document stores often trade write amplification for read convenience via denormalization; on slow disks, those writes show up as latency and checkpoint pressure.
  8. Replica sets and consensus (e.g., elections) introduce operational states that single-node deployments never see, like rollbacks and split-brain prevention behavior.

Three corporate mini-stories from the trenches

Mini-story #1: The incident caused by a wrong assumption

A mid-sized SaaS team launched a new feature: user activity timelines. Someone argued that a document store was “obviously the right fit” because timeline entries
looked like JSON events. They picked MongoDB and stored each user’s timeline as an array inside a single document: one document per user, append new events to the array.

In staging, it was beautiful. Reads were fast: fetch one document, render a timeline. Writes were also “fast” because the dataset was tiny and everything lived in memory.
The VPS spec was modest but seemed fine.

In production, a few heavy users created huge documents. Updates became expensive because each append wasn’t just “add an element”—it triggered document growth behavior,
more frequent page writes, and cache churn. Suddenly the working set didn’t fit. Disk I/O spiked during peak hours, and the database started evicting cache constantly.

The wrong assumption was simple: “A user timeline is one document.” It wasn’t. It was a collection with natural pagination boundaries. The fix was also simple:
store events as separate documents with an index on (user_id, created_at), paginate reads, and cap retention. The postmortem wasn’t about MongoDB being slow.
It was about them using MongoDB like a convenient blob store and then being shocked when the blob got big.

Mini-story #2: The optimization that backfired

Another company ran MySQL on a VPS and was tired of disk usage. A well-meaning engineer decided to compress more: bigger row formats, more compact fields, and
aggressive use of JSON to “avoid joins.” It looked like a win: data shrank, backups got smaller, and dashboards showed reduced storage growth.

Then CPU utilization climbed, p95 latency doubled, and the application started timing out during traffic bursts. The team initially blamed the network.
It was not the network.

The “optimization” backfired because it shifted work from disk to CPU at the worst time. JSON extraction and function-heavy queries prevented index usage.
Compression reduced storage but increased CPU cost and made reads less cache-friendly. Their buffer pool hit rate looked fine, yet queries still burned cycles.

They rolled back the JSON-heavy design, restored proper relational columns for the hot paths, and used compression only where it didn’t block indexing.
The lesson wasn’t “never compress.” The lesson was: don’t optimize for storage without measuring CPU and query plans. On a small VPS, you don’t have spare CPU to waste.

Mini-story #3: The boring but correct practice that saved the day

A payments-adjacent team ran MongoDB for an internal event store and MySQL for billing. They didn’t have a big ops budget, so they did one unsexy thing consistently:
they tested restores monthly. Not “we have backups.” Actual restores, to a clean machine, with a checklist and a clock.

One night their VPS provider had a host-level storage issue. The VM rebooted into a filesystem that mounted, but the database files were inconsistent. The database
wouldn’t start cleanly. Panic lasted about ten minutes, which is a personal best.

They restored the latest snapshot, replayed logs where applicable, and were back within their RTO. Post-incident analysis showed their monitoring was decent,
but their real advantage was confidence: they already knew the backup chain worked and how long it took.

The boring practice saved the day because disasters are never the fun kind. They’re the kind where you realize you have no idea whether your backups are real.

Practical tasks: commands, outputs, decisions

These are the checks I actually run on small production servers. Each task includes: a command, what typical output means, and what decision you make next.
Run them during an incident, or better: run them now while the system is calm and record baselines.

Task 1: Confirm whether you’re CPU-bound or I/O-bound

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0  41200  98200 981200    0    0    12    34  180  420 12  4 80  4  0
 4  2      0  19800  62100 712300    0    0   220   540  260  880 15  6 44 35  0
 3  1      0  20500  62100 710900    0    0   180   610  250  840 14  6 48 32  0

Meaning: The wa (iowait) jumped to ~35%. That’s a classic “disk is the bottleneck” signature, especially if latency complaints line up.

Decision: Stop guessing about queries first; check disk latency and database cache pressure next.

Task 2: Check real disk latency and queueing

cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (server)  12/30/2025  _x86_64_  (2 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          14.22    0.00    5.11   31.67    0.40   48.60

Device            r/s     w/s   rKB/s   wKB/s  avgrq-sz avgqu-sz   await  r_await  w_await  %util
vda              35.0   120.0   980.0  6200.0     86.2     5.90   42.10    18.40    49.20  96.50

Meaning: await at 42ms and %util near 100% means the disk is saturated. Writes are especially slow.

Decision: Reduce write pressure (batching, indexes, checkpoint tuning), or upgrade storage tier. On a VPS, sometimes the right fix is “pay for better I/O.”

Task 3: See if you’re swapping (the quiet killer)

cr0x@server:~$ free -m
               total        used        free      shared  buff/cache   available
Mem:            3940        3605          45          60         289         110
Swap:           2047        1380         667

Meaning: Swap is actively used and available memory is tiny. If the DB is swapping, every request becomes a storage operation.

Decision: Reduce database cache size, reduce app concurrency, or add RAM. If you do nothing, your “database performance tuning” becomes “Linux paging tuning.”

Task 4: Check for OOM kills

cr0x@server:~$ journalctl -k --since "2 hours ago" | tail -n 20
Dec 30 09:41:12 server kernel: Out of memory: Killed process 2145 (mongod) total-vm:5064820kB, anon-rss:2860100kB, file-rss:0kB, shmem-rss:0kB, UID:110 pgtables:7012kB oom_score_adj:0
Dec 30 09:41:12 server kernel: oom_reaper: reaped process 2145 (mongod), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Meaning: The kernel killed mongod. This is not a “MongoDB bug.” It’s a capacity and sizing failure.

Decision: Immediately lower cache usage, fix memory limits, and add headroom. Then review whether this VPS can host the workload safely.

Task 5: Confirm which process is eating memory

cr0x@server:~$ ps -eo pid,comm,rss,pcpu --sort=-rss | head
 2145 mongod   2923400  88.2
 1320 node      420800  12.1
  901 mysqld    210500   6.8
  755 redis-server 80400  1.2

Meaning: MongoDB is dominating RSS. On a small box, that can starve everything else.

Decision: If MongoDB is required, cap WiredTiger cache. If it’s optional, reconsider the architecture rather than “fight the physics.”

Task 6: MongoDB check: WiredTiger cache pressure

cr0x@server:~$ mongosh --quiet --eval 'db.serverStatus().wiredTiger.cache'
{
  "bytes currently in the cache" : 1702453248,
  "maximum bytes configured" : 2147483648,
  "tracked dirty bytes in the cache" : 392154112,
  "pages evicted by application threads" : 18342,
  "pages queued for eviction" : 742,
  "eviction server candidate queue empty when topping up" : 0
}

Meaning: Cache is near max and eviction activity is high. Dirty bytes are substantial, which implies write pressure and checkpoint work.

Decision: If the working set doesn’t fit, you either add RAM, shrink documents/indexes, or accept slower storage. Tuning won’t turn a 4GB VPS into a 64GB cache.

Task 7: MongoDB check: identify slow operations via profiler

cr0x@server:~$ mongosh --quiet --eval 'db.setProfilingLevel(1, { slowms: 50 })'
{ "was" : 0, "slowms" : 50, "sampleRate" : 1, "ok" : 1 }
cr0x@server:~$ mongosh --quiet --eval 'db.system.profile.find().sort({ts:-1}).limit(3).pretty()'
{
  "op" : "query",
  "ns" : "app.events",
  "command" : { "find" : "events", "filter" : { "userId" : "u_123" }, "sort" : { "ts" : -1 }, "limit" : 50 },
  "keysExamined" : 0,
  "docsExamined" : 51234,
  "millis" : 231,
  "planSummary" : "COLLSCAN"
}

Meaning: COLLSCAN plus huge docsExamined is a missing/incorrect index. On a VPS, collection scans are how you heat the datacenter.

Decision: Add an index matching filter+sort (e.g., { userId: 1, ts: -1 }) and verify with explain(). If indexes grow beyond RAM, you’ll still lose—plan capacity.

Task 8: MongoDB check: current operations and lock waits

cr0x@server:~$ mongosh --quiet --eval 'db.currentOp({ "secs_running": { "$gte": 2 } }).inprog.map(o => ({op:o.op, ns:o.ns, secs:o.secs_running, waiting:o.waitingForLock, desc:o.desc}))'
[
  {
    "op" : "command",
    "ns" : "app.events",
    "secs" : 18,
    "waiting" : true,
    "desc" : "conn1421"
  }
]

Meaning: Long-running ops waiting for locks are often caused by a few heavy writes, poorly bounded updates, or indexes being built at the wrong time.

Decision: Stop the bleeding: pause background jobs, rate-limit writers, reschedule index builds, and find the hot collection/document pattern.

Task 9: MySQL check: global status for buffer pool and reads

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';"
+---------------------------------------+-----------+
| Variable_name                         | Value     |
+---------------------------------------+-----------+
| Innodb_buffer_pool_pages_total        | 131072    |
| Innodb_buffer_pool_pages_free         | 128       |
| Innodb_buffer_pool_read_requests      | 983420112 |
| Innodb_buffer_pool_reads              | 8420132   |
+---------------------------------------+-----------+

Meaning: The ratio of read_requests to reads suggests a decent hit rate, but 8.4M disk reads may still be painful on weak storage.

Decision: If reads correlate with latency, increase buffer pool (if memory allows) or reduce dataset/index footprint. If memory is tight, fix queries and indexes first.

Task 10: MySQL check: find top waits and hot statements

cr0x@server:~$ mysql -e "SELECT event_name, count_star, sum_timer_wait/1000000000000 AS total_seconds FROM performance_schema.events_waits_summary_global_by_event_name ORDER BY sum_timer_wait DESC LIMIT 5;"
+--------------------------------------+------------+--------------+
| event_name                           | count_star | total_seconds|
+--------------------------------------+------------+--------------+
| wait/io/file/innodb/innodb_log_file  |     182341 |        812.3 |
| wait/io/file/innodb/innodb_data_file |     491020 |        504.1 |
| wait/synch/mutex/innodb/buf_pool     |   12034011 |        220.7 |
| wait/io/file/sql/binlog              |     320114 |        118.9 |
| wait/lock/table/sql/handler          |      80012 |         62.2 |
+--------------------------------------+------------+--------------+

Meaning: Heavy log file waits often mean fsync pressure: too many small transactions, slow disk, or durability settings forcing frequent syncs.

Decision: Consider batching writes, tuning innodb_log_file_size, and checking innodb_flush_log_at_trx_commit tradeoffs. Don’t “optimize” durability without a business decision.

Task 11: MySQL check: identify slow queries

cr0x@server:~$ sudo tail -n 5 /var/log/mysql/mysql-slow.log
# Time: 2025-12-30T09:52:41.123456Z
# Query_time: 2.184  Lock_time: 0.000  Rows_sent: 50  Rows_examined: 981230
SELECT * FROM events WHERE user_id='u_123' ORDER BY ts DESC LIMIT 50;

Meaning: Lots of rows examined for a small result set: missing composite index, or index exists but can’t support sort.

Decision: Add or fix the index (e.g., (user_id, ts)), then verify with EXPLAIN.

Task 12: MySQL check: verify plan with EXPLAIN

cr0x@server:~$ mysql -e "EXPLAIN SELECT * FROM events WHERE user_id='u_123' ORDER BY ts DESC LIMIT 50\G"
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: events
   partitions: NULL
         type: ALL
possible_keys: idx_user_ts
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 980000
     filtered: 10.00
        Extra: Using where; Using filesort

Meaning: type: ALL means full scan; Using filesort means extra sort work. MySQL is telling you exactly why it’s slow.

Decision: Create the correct composite index and confirm key is used. If it still scans, your data types/collations or query shape might block index use.

Task 13: Check connection pile-ups (app can DoS the DB)

cr0x@server:~$ ss -s
Total: 941 (kernel 0)
TCP:   811 (estab 640, closed 121, orphaned 0, synrecv 0, timewait 121/0), ports 0

Transport Total     IP        IPv6
RAW       0         0         0
UDP       7         6         1
TCP       690       651       39
INET      697       657       40
FRAG      0         0         0

Meaning: Hundreds of established TCP connections can indicate a connection pool misconfiguration or a thundering herd during retries.

Decision: Cap app concurrency, use a sane pool size, and consider a proxy (for MySQL) if connection churn is high.

Task 14: Check filesystem space and inode pressure

cr0x@server:~$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        60G   55G  2.1G  97% /
cr0x@server:~$ df -i
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/vda1      3932160 3919000  13160  100% /

Meaning: Disk is almost full and inodes are exhausted. Databases hate this. So do logs. So does everything.

Decision: Free space immediately (rotate logs, delete old backups), then add monitoring and retention policies so you never hit 97% again.

Task 15: Zapping “mystery latency” by measuring fsync cost

cr0x@server:~$ sudo dd if=/dev/zero of=/var/lib/db/fsync-test bs=4k count=4096 conv=fdatasync
4096+0 records in
4096+0 records out
16777216 bytes (17 MB, 16 MiB) copied, 1.84213 s, 9.1 MB/s

Meaning: 16MiB with fdatasync taking ~1.8s is a red flag for durable-write workloads. Your database will feel this as commit latency.

Decision: If your workload is write-heavy, move to better storage (NVMe with predictable latency) or reduce fsync frequency via batching. Don’t paper over it with “turn off durability” unless the business explicitly accepts data loss.

Task 16: Verify MySQL durability settings before “optimizing” them

cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog','innodb_doublewrite');"
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_doublewrite           | ON    |
| innodb_flush_log_at_trx_commit | 1   |
| sync_binlog                  | 1     |
+------------------------------+-------+

Meaning: These settings prioritize durability. That’s good—until your disk can’t keep up.

Decision: If commit latency is the bottleneck, first address I/O and batching. Only then consider relaxing settings with clear stakeholder sign-off.

Joke #2: “We chose NoSQL for performance” is like saying you bought a pickup truck to win a bicycle race.

Common mistakes: symptoms → root cause → fix

These are not theoretical. These are the patterns that show up on real VPSes at 3 a.m.

1) Symptom: p95 latency spikes every few minutes

Root cause: Checkpointing/flush storms. MongoDB checkpoints or MySQL dirty page flushing hits a saturated disk.

Fix: Reduce write bursts (batch, queue), ensure enough free disk space, increase RAM for cache, and move to lower-latency storage. For MySQL, review log sizes and flushing behavior; for MongoDB, watch eviction and checkpoint timing.

2) Symptom: MongoDB is “fast in dev, slow in prod”

Root cause: Dev dataset fits in RAM. Prod doesn’t. Cache miss rates explode; indexes don’t fit; disk becomes the database.

Fix: Load realistic data in staging. Measure working set. Enforce document size discipline and create indexes for real queries, not imagined ones.

3) Symptom: MySQL CPU high, but disk looks fine

Root cause: Function-heavy queries, JSON extraction in hot paths, or missing indexes causing CPU-intensive scans and sorts.

Fix: Use EXPLAIN, add proper indexes, move computed fields to stored columns when needed, and stop sorting huge result sets without indexes.

4) Symptom: OOM kills or random database restarts

Root cause: Cache sizing assumes more RAM than you have; memory fragmentation; additional services on the same VPS; swap death spiral.

Fix: Cap caches (WiredTiger and InnoDB), reserve RAM for OS and app, and reduce co-located workloads. If you need the DB, give it the box.

5) Symptom: “We added an index and it got slower”

Root cause: Index increases write amplification; background index builds compete for I/O; index doesn’t match query shape and is unused.

Fix: Confirm usage (EXPLAIN or MongoDB explain()). Build indexes off-peak. Use the minimal index set that supports real queries.

6) Symptom: Replication lag grows during traffic peaks

Root cause: Primary is saturated; secondaries can’t apply ops fast enough; disk is bottleneck; too many small transactions.

Fix: Improve primary I/O, batch writes, ensure secondaries have similar storage performance, and avoid long-running transactions that delay apply.

7) Symptom: “MongoDB uses all RAM; that must be bad”

Root cause: Confusing cache usage with memory leak. Databases want memory; the problem is when they steal it from the OS and trigger swap/OOM.

Fix: Set explicit cache limits and leave room for filesystem cache and other processes. Memory without headroom is just future downtime.

Checklists / step-by-step plan

Decision checklist: should this workload be MySQL or MongoDB on a VPS?

  • Choose MySQL if you need joins, strict constraints, stable schema, transactional updates across entities, or predictable performance on small RAM.
  • Choose MongoDB if the core access pattern is document-centric, your documents stay bounded, your indexes fit in memory, and you can run it with replica set discipline.
  • Don’t choose either blindly: model the hot paths and measure working set versus RAM. If you can’t do that, you’re gambling.

Step-by-step: stabilize a struggling VPS database in 60–120 minutes

  1. Stop the bleeding: rate-limit heavy writers, pause non-critical jobs, reduce request concurrency temporarily.
  2. Confirm the bottleneck: vmstat + iostat + memory checks.
  3. Find the top offenders: MySQL slow log / performance_schema; MongoDB profiler/currentOp.
  4. Fix the worst query first: add the right index or rewrite the query to use an existing one.
  5. Cap caches safely: leave RAM for OS and app; avoid swap.
  6. Verify disk health and space: near-full disks behave badly; inode exhaustion is a stealth outage.
  7. Re-test under load: confirm p95 and disk await improve; watch for new bottlenecks.
  8. Write down the baseline: hit rates, disk latency, connection counts, and typical ops/sec. This becomes your future “fast diagnosis.”

Step-by-step: prevent the “NoSQL because trendy” failure

  1. Write down 5 real queries your app will run every second and every minute.
  2. For MongoDB: define document boundaries and maximum document growth rules.
  3. For MySQL: define indexes for the hot paths and keep hot columns typed and indexed (not buried in JSON).
  4. Load realistic data in staging and force a cache miss scenario by constraining RAM or using a smaller instance.
  5. Measure disk fsync latency and verify it matches your durability expectations.
  6. Practice restores on a schedule. Your future self will not have time to learn during an outage.

FAQ

1) Is MongoDB always slower than MySQL on a VPS?

No. MongoDB can be very fast when your access pattern is document-centric and your working set fits in RAM. The usual VPS failure is “dataset grew, cache didn’t.”

2) Why does MongoDB performance fall off a cliff when data grows?

Because cache misses turn into disk reads, and document/index overhead can inflate the working set. Once eviction pressure ramps up on slow storage, latency spikes.

3) Can I just increase WiredTiger cache and be done?

Not safely on a small VPS. If you starve the OS and other services, you trade “slow queries” for swap storms and OOM kills. Cap it deliberately and leave headroom.

4) Is MySQL “better” because it’s relational?

It’s better when your data needs relationships, constraints, and predictable indexed queries. It’s worse when your application truly is document-shaped and you’d otherwise do awkward join gymnastics.

5) Why do people say “NoSQL scales”?

Historically, many NoSQL systems were designed with horizontal distribution as a first-class goal. But scaling is not automatic, and on a single small VPS you are not horizontally scaling anything.

6) What’s the biggest MongoDB schema mistake on VPS?

Unbounded documents (arrays that grow forever, deeply nested structures, or “store the whole history in one doc”). It looks elegant until it becomes a hot, massive object that destroys cache locality and write behavior.

7) What’s the biggest MySQL mistake on VPS?

Running with default configs that assume more RAM and I/O than you have, plus missing composite indexes. Then you try to “fix” it by adding CPU, which doesn’t fix disk stalls.

8) Should I disable fsync/durability settings to get performance back?

Only if the business explicitly accepts data loss and you document the risk. Most “durability off” fixes are temporary band-aids that turn the next incident into data recovery theater.

9) If my app uses JSON heavily, does that mean MongoDB is the obvious choice?

No. JSON as a data interchange format doesn’t imply a document database is optimal. MySQL can store and index structured columns and still keep some JSON for cold fields.

10) What’s the simplest safe strategy for a tiny VPS?

One database, one job, enough RAM for cache, and predictable storage. If you can’t afford that, you can’t afford surprise complexity either—choose the simplest engine that fits the data model.

Conclusion: next steps you can actually do

The “NoSQL because trendy” mistake isn’t choosing MongoDB. It’s choosing it without understanding what your VPS can pay for: RAM misses, fsyncs, compaction, and cache eviction.
MySQL is often the boring winner on small boxes because it’s compact, predictable, and easy to tune for common OLTP patterns. MongoDB can be a great choice when your documents are bounded and your indexes fit—especially when you design around real queries, not dreams.

Practical next steps:

  • Run the fast diagnosis playbook once during a quiet period and record baseline iostat, memory, and DB cache stats.
  • Pick your top 5 queries and prove they use indexes (EXPLAIN or MongoDB explain()).
  • Set explicit cache limits so the OS never needs to “negotiate” with swap.
  • Test a restore. Not “we have backups.” An actual restore to a clean machine.
  • If disk await is consistently high, stop tuning around it and upgrade storage or move the database off the VPS.
← Previous
RAM timings without pain: MHz vs CL and what to buy
Next →
WordPress 404 on Posts: Fix Permalinks Without Breaking SEO

Leave a comment