Back to news
· 11 min read

Why Your VPS Runs Out of Memory — OOM Killer, Swap and How to Fix It

MySQL keeps dying or PHP returns 502? Confirm the OOM killer with free -h and dmesg, add a swap file, and size PHP-FPM and MySQL to fit your VPS RAM.

Why Your VPS Runs Out of Memory — OOM Killer, Swap and How to Fix It

Why Your VPS Runs Out of Memory — OOM Killer, Swap and How to Fix It

The site is down, MySQL is "inactive (dead)" for no visible reason, PHP returns 502 errors, or a long-running script simply ends with the word Killed. You restart the service, everything works for a few hours or days, and then it happens again.

Very often this is the Linux OOM killer at work: the server ran out of memory, and the kernel picked a process to kill so the rest of the system could survive. This guide shows how to prove it, how to read the numbers correctly, and how to fix the cause — with a swap file as a safety net and with PHP-FPM and MySQL sized for the RAM you actually have.

What the OOM killer does and why it picks MySQL

Linux lets programs reserve more memory than exists, betting that not all of it will be used at once. When the bet fails — RAM is full, swap is full or missing, and the cache cannot be shrunk any more — the kernel has two options: freeze, or kill something. It kills.

The victim is chosen by an OOM score, which mostly reflects how much memory a process uses. On a typical web server the biggest single process is the database, so mysqld or mariadbd is the usual victim, even when the real cause is twenty PHP workers or a runaway cron job. That is why "MySQL crashes randomly" is so often a memory problem, not a database problem.

Step 1 — Read free -h correctly

free -h
               total        used        free      shared  buff/cache   available
Mem:           3.8Gi       3.1Gi       120Mi        64Mi       610Mi       420Mi
Swap:             0B          0B          0B
  • free — memory nobody uses at all. A low number here is normal; Linux uses spare RAM as disk cache.
  • buff/cache — disk cache. The kernel gives it back when programs need memory.
  • available — the number that matters: how much programs can still get without swapping. If it stays under roughly 10% of total, you are close to an OOM event.
  • Swap: 0B — no safety net. Many VPS images ship without swap, so the first memory peak goes straight to the OOM killer.

To watch memory over time, run vmstat 5: the si and so columns show memory swapped in and out per second.

Step 2 — Confirm it in the kernel log

The kernel always writes down whom it killed. Search for it:

sudo dmesg -T | grep -Ei 'out of memory|oom-kill|killed process'

# The same, including previous boots (if the journal is persistent)
sudo journalctl -k --since "7 days ago" | grep -Ei 'out of memory|killed process'

A typical line looks like this:

Out of memory: Killed process 1312 (mysqld) total-vm:2398420kB, anon-rss:1104332kB, file-rss:0kB, shmem-rss:0kB, UID:110 pgtables:3120kB oom_score_adj:0

It names the process and the real memory it held (anon-rss, here about 1.1 GB). Just above it, the kernel prints a table of all processes at that moment — often showing dozens of php-fpm workers next to it.

On recent Ubuntu releases also check journalctl -u systemd-oomd: this daemon can stop services under memory pressure before the kernel does. If nothing appears anywhere, the crash has another cause — read the service's own log (for example /var/log/mysql/error.log).

Step 3 — Find out what uses the memory

# Top 15 processes by real memory (RSS, in KB)
ps aux --sort=-rss | head -n 15

# Total memory per program name, in MB
ps -eo rss,comm --no-headers | awk '{m[$2]+=$1} END {for (p in m) printf "%8.0f MB  %s\n", m[p]/1024, p}' | sort -rn | head

The second command adds up all processes with the same name, which is what you need for PHP-FPM, Apache or Node.js workers: one worker looks harmless, thirty of them do not. For a live view, htop (sort by RES with F6) is easier to read.

Typically php-fpm adds up to more than half the RAM (step 5), mysqld has a buffer pool too large for the server (step 6), or a one-off job — a backup, an import, an npm install — needs to run outside peak hours.

Step 4 — Add a swap file as a safety net

Swap is disk space the kernel uses when RAM is full. It is much slower than RAM, so it does not make a small server big — but it turns a sudden crash into a short slowdown, and it lets the kernel move rarely used memory out of the way. On NVMe storage that is a good trade.

# Check that no swap exists yet
sudo swapon --show

# Create a 2 GB swap file (Ubuntu, Debian, AlmaLinux)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# Keep it after reboot
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# Verify
sudo swapon --show
free -h

If swapon reports that the file "has holes" (this can happen on some filesystems), recreate it with sudo dd if=/dev/zero of=/swapfile bs=1M count=2048 instead of fallocate and repeat the other steps.

Then tell the kernel to prefer RAM and use swap only under pressure:

echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf
sudo sysctl --system
cat /proc/sys/vm/swappiness

Watch the si/so columns of vmstat 5 afterwards. Occasional swapping is fine; constant swapping means the workload does not fit and swap is only hiding the problem.

Step 5 — Size PHP-FPM to your RAM

Every PHP-FPM worker is a separate process with its own memory — typically 40–120 MB for WordPress or Laravel, more with heavy plugins. The default pm.max_children does not know how much RAM your server has. Measure the real average size first:

# Ubuntu/Debian: the process is named php-fpm8.3 (or 8.2, 8.1...)
ps -C php-fpm8.3 -o rss= | awk '{s+=$1; n++} END {printf "%d workers, avg %.0f MB\n", n, s/n/1024}'

# AlmaLinux: the process is named php-fpm
ps -C php-fpm -o rss= | awk '{s+=$1; n++} END {printf "%d workers, avg %.0f MB\n", n, s/n/1024}'

Run it under normal traffic, not right after a restart. Then calculate:

pm.max_children = (RAM you can give to PHP) / (average worker size)

Example, 4 GB VPS with MySQL on the same server:
  4096 MB total
 - 1500 MB MySQL (1 GB buffer pool + overhead)
 -  400 MB system, Nginx, cron, SSH
 -  500 MB headroom
 = ~1700 MB for PHP / 80 MB per worker = 21  ->  use 20

Set it in the pool file — /etc/php/8.3/fpm/pool.d/www.conf on Ubuntu/Debian (adjust the version), /etc/php-fpm.d/www.conf on AlmaLinux:

pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500

pm.max_requests recycles each worker after 500 requests, which stops slow memory leaks in plugins from growing forever. On a server with little traffic, pm = ondemand with pm.process_idle_timeout = 10s keeps almost no workers in memory between visits. Check and reload:

sudo php-fpm8.3 -t && sudo systemctl reload php8.3-fpm     # Ubuntu/Debian
sudo php-fpm -t && sudo systemctl reload php-fpm           # AlmaLinux

When all workers are busy, new requests wait briefly in the queue — far better than a killed server. If the PHP-FPM log often shows "server reached pm.max_children", add page caching first, then more RAM.

Step 6 — Size MySQL or MariaDB

The database uses memory in two ways: a fixed part, mostly the InnoDB buffer pool, and a part per connection (sort, join and read buffers). Check the current values:

sudo mysql -e "SELECT @@innodb_buffer_pool_size/1024/1024 AS buffer_pool_mb, @@max_connections;"
sudo mysql -e "SHOW GLOBAL STATUS LIKE 'Max_used_connections';"

Guidelines for a server that also runs the web server and PHP:

  • innodb_buffer_pool_size — about 25–30% of RAM on a shared web server; it only needs to hold your active data. A database of 300 MB does not need a 2 GB buffer pool.
  • max_connections — the default 151 is far more than a small site uses. Set it a bit above Max_used_connections and above pm.max_children, for example 50.

Put the settings in the server's config file — /etc/mysql/mysql.conf.d/mysqld.cnf (MySQL on Ubuntu), /etc/mysql/mariadb.conf.d/50-server.cnf (MariaDB on Debian/Ubuntu) or a new file in /etc/my.cnf.d/ (AlmaLinux), under the [mysqld] section:

[mysqld]
innodb_buffer_pool_size = 1G
max_connections         = 50
sudo systemctl restart mysql      # or: mariadb / mysqld, depending on the system
sudo mysql -e "SELECT @@innodb_buffer_pool_size/1024/1024, @@max_connections;"

A memory budget for common VPS sizes

Starting points for a LEMP or LAMP server running one or a few PHP sites with the database on the same VPS. Measure your own worker size and adjust.

RAMSwapinnodb_buffer_pool_sizemax_connectionspm.max_children (at ~80 MB)
2 GB2 GB384M308–10
4 GB2 GB1G5018–20
8 GB2–4 GB2G8040–50

Step 7 — Protect the database and restart it automatically

Even with good sizing, it is worth telling the kernel that the database is the last thing it should kill, and making sure it comes back if it ever is killed. Use a systemd drop-in, which survives package updates:

sudo systemctl edit mysql     # or mariadb / mysqld
[Service]
OOMScoreAdjust=-500
Restart=on-failure
RestartSec=5s
sudo systemctl daemon-reload
sudo systemctl restart mysql
systemctl show mysql -p OOMScoreAdjust -p Restart

Do not set -1000 (never kill): then the kernel will kill SSH or the web server instead, and you may lose access to the machine.

For more general tuning — caching, Nginx and kernel settings — see our guide on how to optimize a VPS for speed.

When the real fix is more RAM

Swap and tuning remove waste; they cannot create memory. It is time to upgrade when, after the steps above:

  • available in free -h is regularly under 10% at normal traffic;
  • vmstat shows swap activity (si/so) all the time, not only during backups;
  • PHP-FPM keeps reaching pm.max_children even with page caching enabled.

Upgrades within the same VPS line are done from the client area and billed prorated. Afterwards, raise pm.max_children and the buffer pool to use the new memory.

Frequently Asked Questions

How do I know if the OOM killer stopped my MySQL?

Run sudo dmesg -T | grep -i 'killed process' or sudo journalctl -k | grep -i 'out of memory'. A line with mysqld or mariadbd confirms it. If nothing is there, check the database's own error log.

Is swap bad for a VPS on NVMe?

No. A moderate swap file with vm.swappiness=10 is a safety net that prevents sudden crashes. It only becomes a problem when the server swaps constantly — that means it needs less load or more RAM.

How much swap should I add?

For 2–4 GB of RAM, a 2 GB swap file is a good start; for 8 GB or more, 2–4 GB. You do not need "twice the RAM" on a server that does not hibernate.

Why is "free" memory almost zero even when the server is idle?

Linux uses unused RAM as disk cache and releases it when needed. Look at the available column instead of free; that is the memory programs can really still use.

Should I just lower pm.max_children as far as possible?

No. Too few workers make requests queue and pages load slowly. Calculate it from the RAM you can give PHP divided by the measured worker size, then check the PHP-FPM log for "reached pm.max_children" warnings.


At Liga Hosting, our Standard VPS plans start at 2 GB of RAM and go up to 128 GB, and our Performance VPS plans combine Ryzen 9 9950X cores with DDR5 memory — all on NVMe with full root access, so you can set swap, PHP-FPM and MySQL exactly as in this guide. Upgrades within the same line are self-service from the client area.