How to stop a small VPS from running out of memory: swap, limits and the OOM killer
When a server runs out of memory, Linux kills a program to free some. Often that's your database or PHP, and the site goes down. See what uses the memory, add a small safety net, and size PHP and the database so it doesn't happen again.
Before you start
- The examples use Ubuntu 24.04 with PHP 8.3 and Debian 13 with PHP 8.4, on a server with 1 or 2 GB of memory. Change the version numbers to yours.
- The database is MariaDB. MySQL works the same way, but its settings file lives in
/etc/mysql/mysql.conf.d/. - Make the changes at a quiet time. Several steps restart PHP-FPM or the database.
1. See what uses the memory
free -h
Look at the available column, not free. Linux uses spare memory to cache files and gives it back when programs need it, so "free" is often low on a healthy server. "Available" is what programs can still get. The Swap line shows if you have swap and how much of it is in use.
To see the biggest programs, run top and press Shift+M to sort by memory. Or print a list once:
ps -eo pid,user,rss,comm --sort=-rss | head
The RSS column is the memory each program uses, in KB. PHP-FPM and the database run as many processes, so add them up:
ps -C php-fpm8.3 -o rss= | awk '{ s += $1 } END { printf "%d MB\n", s / 1024 }'
ps -C mariadbd -o rss= | awk '{ s += $1 } END { printf "%d MB\n", s / 1024 }'
On Debian 13, use php-fpm8.4. With MySQL, the process is called mysqld. These totals count shared memory more than once, so the real number is a bit lower. That's fine: it's a safe upper guess. Run them at a busy time, because PHP workers grow as they serve pages.
2. Check if the OOM killer struck
When memory runs out, the kernel's "out of memory killer" (OOM killer) picks a process and ends it. Your site then shows "502 Bad Gateway" or "Error establishing a database connection", and the process just seems gone. The kernel log says what happened:
sudo journalctl -k | grep -i -e 'out of memory' -e 'killed process'
To look at the time before the last reboot, add -b -1: sudo journalctl -k -b -1. If the journal doesn't go back far enough, sudo dmesg -T | grep -i -e oom -e 'killed process' shows what's still in the kernel's memory since the last boot.
A line like this means a process was killed:
Out of memory: Killed process 4321 (mariadbd) total-vm:1277304kB, anon-rss:612000kB, ...
The name in brackets is the program that was killed. That's not always the one that used too much: the kernel picks the biggest one it's allowed to kill. A line starting with Memory cgroup out of memory means a memory limit you or systemd set was hit (step 8), not the whole server.
systemd also writes it in the service's own log:
sudo journalctl -u mariadb -u php8.3-fpm | grep -i 'oom'
Look for A process of this unit has been killed by the OOM killer.
3. Add a swap file
Many small VPS plans come with no swap at all. Then the OOM killer acts the moment memory is full. A swap file on disk gives the kernel somewhere to put memory that hasn't been used for a while. Check first:
swapon --show
If it prints nothing, there's no swap. A good size for a 1–2 GB server is 1 or 2 GB. Check that the disk has room with df -h /, then:
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
swapon --show
fallocatemakes the file in a second. It works on ext4, the usual file system on Ubuntu and Debian.chmod 600matters: the file will hold copies of memory, which can include passwords. Only root may read it.swaponwarns about "insecure permissions" if you skip it.- If
swaponsays the file "has holes", make it the slow but sure way instead:sudo rm /swapfile, thensudo dd if=/dev/zero of=/swapfile bs=1M count=1024 status=progressand run the other commands again. - On Btrfs, a normal swap file doesn't work. It needs extra steps, which the swapon manual explains. Most VPS plans use ext4, so check with
findmnt -no FSTYPE /.
Keep it after a reboot. Add one line to /etc/fstab, then check the file:
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
sudo findmnt --verify
findmnt --verify catches typos before the next reboot does. Run swapon --show and free -h again: the Swap line now shows the new size.
4. Set swappiness, gently
vm.swappiness is often explained as "how eager the kernel is to swap". More exactly, it tells the kernel how costly swapping is compared with dropping cached files. It goes from 0 to 200, and the default is 60. Lower numbers make the kernel drop file cache first and swap later.
cat /proc/sys/vm/swappiness
The default is fine for most servers. If you want swap to be used mainly in emergencies, a modest value such as 10 to 30 is a good choice:
echo 'vm.swappiness = 20' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system
Don't set it to 0. It doesn't turn swap off, it just makes the kernel wait until things are tight, which can bring the OOM killer back. Swappiness doesn't fix a server that needs more memory than it has.
5. Or try zram
zram is swap that lives in memory, compressed, instead of on disk. Data that compresses well takes less room, and it's much faster than a disk. It still needs real memory, so it helps most when the disk is slow. Both Ubuntu and Debian have it in the zram-tools and systemd-zram-generator packages. We haven't tested them for this guide, so read the kernel's zram docs first. A plain swap file is the simpler start.
6. Know when swap is a bad sign
Some swap in use is normal: the kernel moved memory that nobody touched. The problem is memory moving in and out all the time. Watch it for half a minute:
vmstat 5 6
Look at the si (swapped in) and so (swapped out) columns. Zeros, or numbers above zero only now and then, are fine. Numbers above zero in every line mean the server keeps swapping, and pages get slow. Then don't add more swap. Use less memory (step 7) or move to a bigger plan.
7. Size PHP-FPM and the database to the memory you have
The real fix is to make sure PHP and the database can't ask for more than the server has. Make a simple budget. For example:
- 1 GB server: about 200 MB for the system, Nginx and SSH, about 250 MB for MariaDB (the default 128 MB cache), and about 400 MB for PHP. With workers of 50 MB, that's
pm.max_children = 8, shared between all your pools. - 2 GB server: about 250 MB for the system, about 500 MB for MariaDB (a 256 MB cache), and about 1,000 MB for PHP:
pm.max_children = 20with 50 MB workers.
These are only starting points. Measure your own worker size the way our OPcache and PHP-FPM speed guide shows (step 6 there), and give each site its own limit with one PHP-FPM pool per site.
The MariaDB cache is called innodb_buffer_pool_size. It's the biggest part of the database's memory. Check what it is now, in MB:
sudo mariadb -NBe 'SELECT @@innodb_buffer_pool_size / 1024 / 1024'
Both Ubuntu 24.04 and Debian 13 start with 128 MB. That's right for a 1 GB server. On a 2 GB server with a bigger database, 256 MB is a good step up. Put it in a file of your own, so package updates don't overwrite it:
printf '[mysqld]\ninnodb_buffer_pool_size = 256M\n' | sudo tee /etc/mysql/mariadb.conf.d/60-memory.cnf
sudo systemctl restart mariadb
sudo mariadb -NBe 'SELECT @@innodb_buffer_pool_size / 1024 / 1024'
Advice like "give it 70% of memory" is for servers that run only the database. On a small server that also runs PHP, it would leave no room for anything else.
8. Protect SSH and the database
Each process has an OOM score. The kernel kills the one with the highest score first. You can lower a service's score with a small systemd setting, so the kernel picks something else. Start with SSH, so you can always log in to fix things:
sudo systemctl edit ssh
In the editor that opens, add these lines between the comments at the top, then save:
[Service]
OOMScoreAdjust=-1000
-1000 means "never kill this". That's safe for SSH, because it uses very little memory. Do the same for the database with a milder value, so the kernel kills a PHP worker before MariaDB:
sudo systemctl edit mariadb
[Service]
OOMScoreAdjust=-500
Don't use -1000 for the database. If the database itself is what grows, the kernel would kill everything else first, SSH aside. Restart both and check the new values:
sudo systemctl restart ssh mariadb
cat /proc/$(pidof -s sshd)/oom_score_adj
cat /proc/$(pidof -s mariadbd)/oom_score_adj
You should see -1000 and -500. On Ubuntu 24.04, SSH starts on the first login, so log in once in a second window if pidof finds nothing. A restart of ssh keeps your open session.
9. Put a ceiling on PHP-FPM
You can also give PHP-FPM a hard memory limit, so a runaway script can only take down PHP workers and never the database or SSH. PHP's own memory_limit caps one request. This caps all workers together.
sudo systemctl edit php8.3-fpm
[Service]
MemoryMax=500M
OOMPolicy=continue
Set MemoryMax a bit above the PHP budget from step 7. Don't leave out OOMPolicy=continue. In our tests, when one PHP worker went over the limit and was killed:
- On Ubuntu 24.04, systemd stopped the whole PHP-FPM service and left it stopped. Every site on the server showed "502 Bad Gateway".
- On Debian 13, systemd stopped PHP-FPM and started it again, which cut off every request that was running.
- With
OOMPolicy=continue, only that one worker died. PHP-FPM started a new one, and the other requests carried on.
This also applies when the whole server runs out of memory and the kernel kills a PHP worker, so the setting is worth adding even without MemoryMax. Restart and check:
sudo systemctl restart php8.3-fpm
systemctl status php8.3-fpm
The Memory: line now shows the limit, for example Memory: 9.4M (max: 500.0M ...).
Don't put MemoryMax on the database. When we tried, MariaDB was killed as soon as it reached the limit. It came back after five seconds, but every open connection was lost.
If something goes wrong
- "swapon: /swapfile: insecure permissions": run
sudo chmod 600 /swapfile. - "swapon failed: Invalid argument" or "file has holes": make the file with
dd(step 3). - "swapon failed: Operation not permitted": some VPS types (container-based plans such as OpenVZ or LXC) don't let you add swap. Ask your host, or rely on steps 7 to 9.
- The server doesn't boot after editing
/etc/fstab: a typo in the new line. Use your host's rescue console, fix or remove the line, and runsudo findmnt --verify. - PHP-FPM keeps stopping with "Failed with result 'oom-kill'": add
OOMPolicy=continue(step 9), then lowerpm.max_children. - The OOM killer still strikes: add up the numbers from step 1 at a busy time. If they're more than the memory you have, lower
pm.max_childrenor the database cache, or move to a bigger plan.
Official sources
- swapon(8), with the notes on files with holes and Btrfs, and mkswap(8) (man pages)
- vm.swappiness (Linux kernel docs)
- zram (Linux kernel docs), and the zram-tools and systemd-zram-generator packages
- vmstat(8) and oom_score_adj (proc man page)
- systemd: OOMScoreAdjust (systemd.exec), MemoryMax (systemd.resource-control) and OOMPolicy (systemd.service)
- InnoDB buffer pool (MariaDB docs)
- PHP-FPM configuration (php.net), with
pm.max_children
Next steps: make sure a full disk can't take the site down either, with our guide to stopping logs from filling the disk. And work through the security checklist for a new PHP server.
ServerNest is being built to show each server's memory and swap use, and to set PHP-FPM limits per site. The product is still in development. Join early access to try it first.