How to Fix High Memory Usage on Your Ubuntu Server
Your server is crawling, processes are getting killed, and top shows memory pegged at 98%. Here is how to find the cause and fix it properly rather than just rebooting.
A reboot will clear the symptom for a few hours. It will not tell you which process ate the RAM, and it will not stop it happening again on Friday night when nobody is watching. The whole job takes about twenty minutes.
Confirm it is actually a memory problem
Before you change anything, check whether the kernel has been killing processes. If it has, the evidence is in the kernel log:
sudo dmesg -T | grep -i -E "killed process|out of memory"
Or on a systemd box:
sudo journalctl -k --since "24 hours ago" | grep -i oom
A line naming a victim process is the OOM killer telling you exactly what it sacrificed. That is your starting point. If your database process appears in that list, it explains the "error establishing a database connection" messages your visitors have been seeing.
No OOM entries at all? Your problem may be disk I/O or CPU, not memory, and the rest of this post will not help much. Check iostat and load average before you go further.
Find what is actually using the memory
Start with the overall picture:
free -h
The line that matters is available, not free. Linux deliberately uses spare RAM for disk cache, so a low free number is normal and healthy. Cache is reclaimable - the kernel hands it back the moment an application needs it. If available is comfortably above a few hundred megabytes, you are not out of memory no matter how alarming the free column looks.
Now find the worst offenders:
ps aux --sort=-%mem | head -6
On a typical WordPress box this is usually PHP-FPM workers or MySQL.
One caveat worth knowing: the RSS figure that ps reports includes shared memory, so ten PHP workers each showing 90MB are not necessarily using 900MB between them. For a more honest per-process number, install smem:
sudo apt install smem
smem -t -k -P php-fpm
The PSS column divides shared pages fairly between the processes using them. It is the number to size your configuration against.
To count how many workers are actually running:
ps --no-headers -C php-fpm | wc -l
If that number is climbing towards your configured maximum every time traffic arrives, you have found your problem.
Add a swap file
Swap will not make a slow server fast, but it stops the kernel killing processes when you briefly run out.
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
Make it survive a reboot:
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
Confirm it took:
swapon --show
free -h
Swap on an SSD is fine. Swap on a spinning disk under heavy load will feel worse than the problem you started with.
Two things that catch people out:
- On XFS or Btrfs,
fallocatecan produce a file the kernel refuses to use as swap. Ifmkswapcomplains, build it withsudo dd if=/dev/zero of=/swapfile bs=1M count=2048instead. - Size it sensibly. On a 2GB VPS, 2GB of swap is plenty. Giving a small server 8GB of swap does not buy you headroom, it just lets a runaway process thrash the disk for longer before anything stops it.
You may also want to make the kernel less eager to swap out active pages:
sudo sysctl vm.swappiness=10
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
Swap is a safety net, not a fix. If your server is sitting in swap all day, you have not solved anything - you have just moved the bottleneck to the disk.
Cap PHP-FPM before it caps you
The usual cause of memory exhaustion on a WordPress server is too many PHP workers. Each one can hold 60-120MB.
- Work out your budget: available RAM minus MySQL and the OS
- Divide by the average worker size
- Set the max children value to that number, not higher
A worked example on a 2GB server: reserve roughly 400MB for MySQL and 300MB for the operating system, web server and everything else. That leaves about 1.3GB. At 100MB per worker, your ceiling is 13, and you would set it to 10 to leave yourself a margin.
Edit the pool config, usually at /etc/php/8.2/fpm/pool.d/www.conf:
pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
Then reload:
sudo systemctl reload php8.2-fpm
pm.max_requests is the quiet hero here. It recycles each worker after a set number of requests, which contains the slow memory leaks that some plugins produce without you ever having to identify the culprit. On a low-traffic site, pm = ondemand is often better still - workers exit when idle instead of sitting there holding memory.
Setting max children too low does not crash anything. Requests queue instead, and you will see server reached pm.max_children setting in the PHP-FPM log. That is a clear, recoverable signal. Setting it too high hands the OOM killer a loaded weapon.
Do not forget MySQL
MySQL is frequently the second half of the problem. The single most important setting is the InnoDB buffer pool:
[mysqld]
innodb_buffer_pool_size = 256M
The conventional advice is to give InnoDB 70% of system RAM, and that advice is written for dedicated database servers. On a box that is also running PHP and a web server, it is a recipe for exactly the crash you are trying to prevent. On a small VPS, 25-30% of total RAM is a much safer starting point.
While you are in there, a persistent object cache - Redis, with a plugin to match - will cut the number of database queries dramatically. Fewer queries means shorter-lived PHP workers, which means less concurrent memory in use. The fix compounds.
Keep an eye on it
Set up a check so you find out before your visitors do. Even something crude beats nothing - a cron job that logs the available memory every five minutes gives you a history to look at after the next incident:
*/5 * * * * date >> /var/log/memcheck.log && free -m >> /var/log/memcheck.log
If you are on systemd, you can also stop any single service taking the whole machine down with it by adding a hard ceiling to its unit file:
[Service]
MemoryMax=512M
That way the offending service gets restarted on its own rather than the kernel picking a victim at random, which is how you end up with a healthy web server and a dead database.
For anything more serious, Netdata or a hosted uptime monitor with a memory check will page you before the site goes down.
If you would rather not babysit this, my care plans cover monitoring and monthly checks.
Need this done on your site?
I clean up hacked WordPress sites and harden them so it does not happen again.