
A WordPress site on the cheapest tier available dies every few days, always in the small hours, always the same way. The page returns a database connection error, MySQL is stopped, and starting it by hand buys another two or three days. The host gets blamed, a support ticket gets opened, and the reply is usually one line: check the kernel log for the OOM killer. It is there, plain as anything, recording that the kernel killed the database in the middle of the night to keep the rest of the system alive. That detail is worth sitting with. Nothing had crashed. Something made a deliberate decision, wrote it down, and carried on. Once you can read that decision and see why it keeps repeating, the box turns boring, which is the highest compliment a server can be paid. Swap, arithmetic and smaller worker counts get you there.
the cheapest tier is a memory problem, not a CPU problem
Entry-level VPS plans are sized for the plan sheet, not for the stack most people install on them. A 1 GB box sounds fine until you count what’s actually resident: the kernel and systemd, a web server, a PHP or Python application with several worker processes, and a database engine that would quite like to cache your working set in RAM. Each of those is modest. Together they overrun.
MySQL is usually the biggest single consumer, and its defaults assume a larger machine. PHP-FPM and Gunicorn compound it in a way that surprises people, because each worker is a separate process holding its own interpreter and loaded modules. Ten workers is not one process using slightly more memory. It’s ten processes.
The Indian angle is that the regional data centres worth using for latency often start their ladder at the tier that’s too small for a database-backed site. If you’re still choosing, my comparison of hosting providers covers what each tier gives you. Spend on RAM before anything else; CPU is rarely the constraint on a small site.
reading free -h


free -h
# total used free shared buff/cache available
# Mem: 957Mi 612Mi 71Mi 18Mi 274Mi 201Mi
# Swap: 2.0Gi 318Mi 1.7Gi
Almost everyone reads the wrong column. The free figure looks alarming, 71 MiB out of nearly a gigabyte, and people conclude they’re about to fall over. They’re not, necessarily, and the reason is the next column.
buffers and cache are not waste
Linux uses memory that nothing is asking for to cache disk contents. That’s not the system hoarding, it’s the system doing its job, and it makes your disk reads dramatically cheaper. The moment an application genuinely needs that memory, the kernel drops cache pages and hands it over. Free RAM sitting idle is RAM you paid for and aren’t using.
So available is the column that matters. It’s the kernel’s own estimate of how much a new process could claim without pushing the system into swapping. In the output above, roughly 200 MiB of headroom on a box that’s already using swap. Not a crisis, not comfortable either.
Then find out who’s actually holding memory, in descending order:
# resident set size per process, biggest first, in kilobytes
ps -eo pid,user,rss,comm --sort=-rss | head -12
# per-service totals, which is more useful than per-process
systemd-cgtop -m --iterations=1
# how much a unit is using right now
systemctl status mysql | grep -i memory
the OOM killer, and proving it acted


When the kernel cannot satisfy an allocation and has no swap left to fall back on, it does not return an error to the process and it does not freeze. It picks a victim and kills it. The choice is scored roughly by how much memory each process is using, weighted by a tunable adjustment, so the biggest consumer usually loses. On a small web server the biggest consumer is your database, which is why the symptom is so often “MySQL keeps stopping”.
This is a rational design. The alternative is the machine locking up entirely, unreachable by SSH. Sacrificing one process keeps the box alive and leaves a written record, and here’s how to read it:
# human-readable timestamps, which dmesg does not give you by default
sudo dmesg -T | grep -i -E 'out of memory|killed process|oom'
# same thing from the journal, scoped to a window you care about
sudo journalctl -k --since "2026-08-05 01:00" --until "2026-08-05 04:00" | grep -i oom
# across reboots, if the box has persistent journals
sudo journalctl -k --list-boots
sudo journalctl -k -b -1 --grep 'Out of memory'
A hit names the process, its PID, and how much anonymous memory it held when it died. That line is the difference between guessing and knowing, which is why the kernel log deserves a look before you accept that a host’s network was at fault. Pinning a reported time to the right log window is covered in reading server logs.
adding a swapfile, step by step


Most VPS images ship with no swap at all, which means the kernel has no shock absorber. A swapfile costs you a couple of gigabytes of disk and converts a hard kill into a slow patch. On a small box that trade is worth making every time.
# check first, in case swap already exists
swapon --show
free -h
sudo fallocate -l 2G /swapfile # or: sudo dd if=/dev/zero of=/swapfile bs=1M count=2048
sudo chmod 600 /swapfile # root-only, this file holds process memory
sudo mkswap /swapfile
sudo swapon /swapfile
# make it survive a reboot
sudo cp /etc/fstab /etc/fstab.bak
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
swapon --show
The chmod 600 is not housekeeping. That file holds whatever was in memory, credentials included, so it must not be world-readable. If fallocate fails on your filesystem, the dd alternative in the comment works on ext4 and XFS, just slower. Double-check the of= path before pressing Enter, because dd overwrites whatever it’s pointed at; Btrfs needs its own swapfile procedure.
swappiness, and what it really controls
Swappiness, a value from 0 to 200 on current kernels, sets the relative cost the kernel assigns to swapping anonymous pages versus dropping file cache. The default of 60 suits a desktop. On a server where you want swap as an emergency reserve rather than a routine destination, lower it.
cat /proc/sys/vm/swappiness
# try it now
sudo sysctl vm.swappiness=10
# keep it across reboots
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf
Setting it to 0 is a popular suggestion and a bad one. It doesn’t disable swap, it makes the kernel avoid it until the last possible moment, which brings back the abrupt kills you added swap to prevent. Ten leaves swap available as a reserve without the kernel reaching for it during ordinary work.
swap on network storage is a different animal
Here’s the part worth understanding before you get enthusiastic. On many cloud providers your disk is network-attached block storage, not a drive physically in the machine. Swapping to it means every page fault becomes a network round trip. Local NVMe swap is slow compared to RAM; network-backed swap is slow compared to that.
A box thrashing in swap can be so unresponsive that SSH takes minutes to hand you a prompt, which is arguably worse than a clean restart. Treat swap as insurance against a spike, not as a way to run a workload that doesn’t fit. Constant swap use means the swapfile has stopped being a safety net and become the problem, and that trend is exactly what an on-box agent catches and an uptime check never will, as in knowing your server is down first.
sizing workers for a box this small

The default configurations for PHP-FPM, MySQL and Gunicorn assume you have room. Cutting them down is usually the fix that makes a small box stable, and the arithmetic is simple: measure one worker, multiply by the count, and make sure the total plus the database leaves headroom.
| Component | Default posture | On a 1 GB box | On a 2 GB box |
|---|---|---|---|
| PHP-FPM pm | dynamic, max_children 5 in the stock pool, often raised by tuning guides | ondemand, max_children 4 | dynamic, max_children 6 to 8 after measuring per-child memory |
| Gunicorn workers | often set to CPU count times two | 2, with a thread or two each | 3 |
| MySQL buffer pool | 128 MB and rising with tuning guides | 96 to 128 MB, no more | 256 MB |
| MySQL max_connections | 151 | 25 to 40 | 60 |
| Redis, if present | unbounded | maxmemory 64mb, allkeys-lru | 128mb |
| Swap | none on most images | 2 GB | 2 GB |
Those are starting points, not tuned truths. Find your real per-worker footprint with ps under actual traffic, then set the count so the arithmetic leaves a few hundred megabytes of headroom for the kernel, the cache and your own SSH session.
systemd memory limits as a blast-radius tool

You can cap what a service is allowed to use, which changes who dies when something misbehaves. Instead of the kernel picking your database because it’s the largest process, the leaking service hits its own ceiling first.
# sudo systemctl edit my-side-project.service
[Service]
MemoryHigh=200M
MemoryMax=280M
Restart=on-failure
RestartSec=10s
MemoryHigh is a soft limit: cross it and the service is aggressively reclaimed and throttled, but not killed. MemoryMax is the hard wall. Run sudo systemctl daemon-reload after editing, then restart the unit.
I’d put a ceiling on the services I’d rather lose: a hobby script, a feed fetcher, an indexing job written in a hurry. I wouldn’t cap the database, because one killed mid-write is worse than a slow one. The point is choosing the victim deliberately instead of letting the kernel choose by size.
when the answer is more RAM

There’s a point where tuning becomes a hobby that costs more than the upgrade. The signs: swap in constant use rather than occasional, worker counts so low that requests queue during ordinary traffic, a buffer pool too small for your working set, or you flinching before every deployment. Resize the instance. Most providers do it with a reboot and let you scale back down, and the monthly difference is small next to another 2 AM investigation. On a first server, my VPS beginner guide covers what resizing involves.
common mistakes
- Panicking at the
freecolumn and running commands to drop caches. You’re throwing away useful cache and making the machine slower for no benefit. - Setting swappiness to 0 because a forum post said so, which restores the abrupt kills you were trying to avoid.
- Copying a MySQL tuning guide written for a 16 GB server. Those buffer pool numbers will get your database killed on a 1 GB box.
- Leaving the swapfile world-readable. It holds whatever was in memory, so
chmod 600matters. - Restarting the service and moving on without reading the kernel log, guaranteeing the same failure next week at the same hour.
- Forgetting that a nightly backup, a package upgrade or an image build spikes memory. The 2 AM kill and the 2 AM cron job are rarely a coincidence.
FAQ
how much swap should i add?
On a small server, one to two times RAM, capped at about 2 GB. The old rule of twice RAM came from an era of needing to hibernate desktops and doesn’t scale sensibly upward. Two gigabytes is enough to absorb a spike or a badly behaved cron job while staying small enough that you notice when it’s being used heavily.
can i stop the OOM killer from touching my database?
You can bias it with OOMScoreAdjust in the service unit, making the kernel prefer other victims. I’d use it sparingly. Protecting the largest process means the kernel kills smaller ones that were innocent, and you end up with a confusing partial outage instead of a clear one. Reducing total demand is the real fix, and capping the services you don’t mind losing is the second-best.
Add swap, cut your worker counts, then read the kernel log after the next incident instead of just restarting the service. Two of those three take five minutes, and together they turn a box that dies every few days into one you stop thinking about.
Update history: corrected the swappiness range, the PHP-FPM default and the systemd source for OOMScoreAdjust; the fstab step now keeps a backup and skips a duplicate entry.
Sources
- Kernel virtual memory sysctl reference — what swappiness actually controls, plus the overcommit settings behind OOM behaviour.
- free(1) manual page — the definition of the available column versus free and buff/cache.
- systemd.resource-control(5) — MemoryHigh and MemoryMax semantics.
- systemd.exec(5) — OOMScoreAdjust.