Optimization

Why my VPS runs out of RAM and how to fix it

Your VPS was fine last week. Today SSH answers slowly, nginx returns 502 for a second, and dmesg shows Out of memory: Killed process. That is the OOM killer reclaiming pages, and it usually targets the process you care about most. Fixing this is not about buying more RAM first, it is about finding what actually consumed it. Most of the time the memory is not leaked, it is misconfigured: a PHP-FPM pool sized for 32 workers on a 2 GB box, a MySQL buffer pool set to 70% of RAM, or a Redis instance caching data you never read. This guide walks through diagnosing memory on Ubuntu 24.04 or Debian 12, then applying fixes that reclaim gigabytes without changing your workload.

  • Key takeaways: Measure before you tune, free -h shows cache as used but it is reclaimable.
  • Set PHP-FPM pm.max_children from a real memory-per-worker calculation, not a guess.
  • Cap MySQL/Redis caches explicitly so they cannot grow into swap.
  • Use systemd-analyze blame to find boot-time RAM spikes and swap as a safety net, not a solution.

Prerequisites

  • A VPS running Ubuntu 24.04 LTS or Debian 12 (commands match both unless noted).
  • Root access or a user with sudo privileges.
  • A terminal connection over SSH.
  • Basic familiarity with systemctl and editing files in /etc.

Why does my VPS run out of RAM so fast?

Linux does not manage memory the way Windows does. Free RAM is wasted RAM, so the kernel caches disk reads aggressively. Run free -h and you will see a low free number and a high buff/cache value. That cache is not a problem, it is reclaimed instantly when an application needs pages. The real problem is anonymous memory, private pages belonging to processes, which cannot be reclaimed without killing the process or swapping it out.

When the sum of anonymous memory exceeds physical RAM, the kernel activates the OOM killer. It scores processes by size and importance, then kills the highest scorer. This is why you lose your database or web server instead of a background cron job. The trigger is almost always one of these: PHP-FPM workers multiplied by memory per request, a database buffer pool configured for a machine three times larger, Java or Node processes with a default heap limit, or a runaway redis-server filling maxmemory to its default of zero, which means unlimited.

先测量再优化,VPS 内存问题大多来自配置不当而非真正泄漏。

Measure before you tune, most VPS memory problems come from misconfiguration, not a real leak.

Step 1 - Measure what is eating RAM

Start with the baseline. free -h shows totals, but it does not tell you which process consumes what. Run these three commands in order.

free -h
htop
systemd-analyze blame

htop gives a live view sorted by memory. Press F6 and select M to sort by resident memory. Look at the RES column, that is the physical RAM a process holds. Ignore VIRT, it includes shared libraries and mapped files that do not consume real pages. The systemd-analyze blame output reveals which services peak during boot, useful when the VPS struggles right after a reboot.

For a precise per-process breakdown, install smem, which accounts for shared memory correctly, unlike top.

sudo apt install smem
smem -rk | head -30

This lists processes by proportional set size, the most honest metric for shared libraries. Expect to see php-fpm, mysqld, or java at the top. Note the numbers, they become the basis for every fix below.

Verify: smem -rk | head -30 lists processes sorted by memory, and you can see the top consumer within the first five lines.

Step 2 - Tame PHP-FPM before it eats the box

PHP-FPM is the most common RAM hog on a web VPS. Every concurrent request spawns a worker, and each worker holds the full PHP runtime plus the framework you use. A WordPress site with a 256 MB memory limit per worker, and 20 idle workers, holds 5 GB of RAM before a single visitor arrives. Check your current pool configuration.

sudo grep -E 'pm|max_children' /etc/php/8.3/fpm/pool.d/www.conf

On Ubuntu 24.04 the path is /etc/php/8.3/fpm/pool.d/www.conf. On Debian 12 with PHP 8.2 it is /etc/php/8.2/fpm/pool.d/www.conf. The pm.max_children value is your ceiling. Calculate the right number: measure average memory per worker, then divide available RAM by that.

ps -o rss,cmd -C php-fpm8.3 | awk 'NR>1 {sum+=$1; count++} END {print sum/count/1024 " MB per worker"}'

If each worker uses 80 MB and you have 2 GB of RAM, set pm.max_children to 20, leaving headroom for MySQL and Nginx. Edit the pool file and set pm = dynamic with conservative start and idle values.

sudo sed -i 's/pm.max_children = .*/pm.max_children = 20/' /etc/php/8.3/fpm/pool.d/www.conf
sudo sed -i 's/pm.start_servers = .*/pm.start_servers = 4/' /etc/php/8.3/fpm/pool.d/www.conf
sudo sed -i 's/pm.min_spare_servers = .*/pm.min_spare_servers = 2/' /etc/php/8.3/fpm/pool.d/www.conf
sudo sed -i 's/pm.max_spare_servers = .*/pm.max_spare_servers = 6/' /etc/php/8.3/fpm/pool.d/www.conf
sudo systemctl restart php8.3-fpm

These numbers are a starting point, adjust after a day of production traffic. The key is that max_children now caps total PHP memory at roughly 1.6 GB worst case instead of letting it climb without limit.

Verify: ps -o rss,cmd -C php-fpm8.3 | awk 'NR>1 {sum+=$1; count++} END {print sum/count/1024 " MB per worker"}' shows the average, and systemctl status php8.3-fpm reports active without restarting constantly.

Step 3 - Cap MySQL and Redis caches explicitly

Databases are designed to use all available memory. MySQL's InnoDB buffer pool defaults to 128 MB, but many control panels or setup scripts raise it to 70% of RAM. On a 2 GB VPS that leaves almost nothing for PHP. Check your current value.

mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
redis-cli CONFIG GET maxmemory

For MySQL on a 2 GB VPS, set the buffer pool to 512 MB. Use the mysqld config file, not the running variable, so it survives restarts.

sudo tee -a /etc/mysql/mysql.conf.d/memory.cnf <<EOF
[mysqld]
innodb_buffer_pool_size = 512M
innodb_log_file_size = 64M
performance_schema = OFF
EOF
sudo systemctl restart mysql

Disabling performance_schema frees 200 to 300 MB on small instances, a real win if you do not depend on its metrics. Note that this helps mysqld and mariadb the same way, MariaDB reads the same directive under the same section.

Redis is simpler. It stores data until it hits maxmemory, and the default is zero, unlimited. On a VPS shared with a web server, that is a disaster waiting to happen. Set a cap and a sensible eviction policy.

sudo tee -a /etc/redis/redis.conf <<EOF
maxmemory 256mb
maxmemory-policy allkeys-lru
EOF
sudo systemctl restart redis-server

The allkeys-lru policy evicts the least recently used keys when the limit is reached, so the cache shrinks instead of the OOM killer taking down Nginx.

Verify: mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';" returns 536870912 and redis-cli CONFIG GET maxmemory returns 268435456.

Step 4 - Set swap as a safety net

Swap does not fix a memory leak, but it prevents the OOM killer from firing during a temporary spike. A 2 GB VPS benefits from a 2 GB swap file, not more. Oversized swap on slow disk causes thrashing that makes the box unresponsive. Create and enable it with systemd.

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Tune the swapiness so the kernel prefers RAM until it actually needs pressure relief. The default of 60 is too aggressive for a VPS with NVMe storage.

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

This tells the kernel to avoid swapping out anonymous pages until memory pressure is real. Combined with the caps from Steps 2 and 3, swap becomes a safety net rather than a crutch.

Verify: free -h shows a Swap line with 2 GB total, and sysctl vm.swappiness returns 10.

Troubleshooting - how to read an OOM kill

If the OOM killer fires after your fixes, read the log before touching anything else. The kernel writes a detailed report to the journal.

sudo journalctl -k | grep -i 'out of memory' | tail -5

The log lists the killed PID, its memory footprint, and the top processes at that moment. Common patterns:

  • Rapid repeated kills of php-fpm: max_children is still too high, or a single request leaks memory inside a loop.
  • mysqld killed at night: cron jobs trigger a full table scan that doubles the buffer pool usage. Lower the pool or move the job to off-peak.
  • java or node killed: the heap default is too large. Set -Xmx512m for Java or --max-old-space-size=512 for Node.
  • Nothing in the log: the kernel froze before it could log, check dmesg | tail -20 immediately after the crash.

If the OOM killer still targets a process you cannot shrink, the honest answer is more RAM. A Linux VPS with 4 GB instead of 2 GB changes the calculus for MySQL, PHP, and Node workloads. On a budget, move the database to a separate VPS, that usually halves peak memory on the web server. If your workload is already at 8 GB and still growing, a dedicated server gives you full control over memory and CPU without virtualization overhead.

Step 5 - Monitor memory over time

One htop snapshot is not a diagnosis. Memory usage drifts with traffic, cron jobs, and user behavior. Install munin for a lightweight graph of memory, swap, and process counts. It polls every five minutes and stores a month of history.

sudo apt install munin munin-node
sudo systemctl enable --now munin-node

Munin graphs are plain HTML files viewable over SSH with a quick Python server, no web panel needed.

cd /var/cache/munin/www && python3 -m http.server 8080
# Point your browser at http://your-vps-ip:8080/vps.example.com/

Watch the memory graph for a sawtooth pattern, up during peak, down after cache reclaim. A steady upward trend that never drops points to a leak in one process. Cross-reference the processes graph to find which one grows monotonically. That is your next debugging target, and it is a code fix, not a config fix.

If graphs show regular weekly peaks, schedule a check with a cron job that alerts you before the OOM killer acts.

*/5 * * * * /usr/bin/free -m | /usr/bin/awk '/Mem:/ {if ($4 < 100) system("/usr/bin/logger -t memalert \"Low memory: " $4 " MB free\"")}'

This logs a warning whenever free memory drops below 100 MB, giving you a chance to react before a process dies. Pair it with the monitoring approach you already run for uptime, the Self-hosted VPS monitoring with Prometheus and Grafana guide shows a more detailed setup if you need per-process alerts.

Verify: systemctl status munin-node shows active, and the memory graph in your browser updates within ten minutes.

FAQ

Why does my VPS show high memory usage but no process uses it?

That is the page cache. free -h reports disk cache under buff/cache, and it looks like usage but the kernel reclaims it instantly. Check free -h and look at the available column, it is the real number of MB an application can request without swapping.

What is the OOM killer and why does it kill MySQL?

The OOM killer is a kernel mechanism that terminates processes when anonymous memory exceeds physical RAM plus swap. It scores processes by memory footprint, so MySQL and PHP-FPM, the largest consumers, are the first targets. Capping their memory as shown above prevents these kills.

Is swap on a VPS with NVMe actually useful?

Yes, but only as a safety net. NVMe is fast enough for occasional swap-ins, but continual swapping will wear the disk and slow every request. Set swap to roughly the size of your RAM, keep swappiness low, and treat swap usage as a signal to fix the underlying allocation.

How much should I set innodb_buffer_pool_size on a 2 GB VPS?

Start at 512 MB plus 64 MB for the log file. That leaves enough for PHP-FPM, Nginx, and the OS. If your database is the only workload, 1 GB is safe. Measure SHOW ENGINE INNODB STATUS and check the buffer pool hit ratio, above 99% means you are fine.

Can I run Redis and MySQL on a 2 GB VPS?

You can, with strict caps. Set Redis maxmemory to 128 MB and MySQL buffer pool to 384 MB, then budget the rest for the web server. If both need more, move one to a second VPS, it is cheaper than buying a 4 GB box and then still hitting limits during peaks.

Related articles

The pattern here applies to any Linux box, but a cheap Linux VPS with 2 GB RAM is where this matters most. Measure first, cap the big three (PHP-FPM, MySQL, Redis), then add swap as insurance. That order keeps the OOM killer quiet without buying hardware you may not need. If monitoring shows you are consistently at 90% usage after tuning, it is time to scale up, not tune down further.

VPS 内存耗尽排查与修复要点

VPS 内存耗尽通常不是泄漏,而是 PHP-FPM、MySQL 和 Redis 的默认配置过大。先用 free、smem 和 systemd-analyze 测量,再根据每个进程的实际占用设置上限,最后配置 2GB 交换文件作为安全网。OOM killer 会在内存耗尽时终止最大进程,因此务必先限制缓冲池和 worker 数量。若优化后仍持续达到 90% 使用率,应升级到更大内存的 VPS。

Note: This guide is for general reference. Every system and infrastructure has its own specifics, so test each step in a safe environment and consult a qualified engineer before applying it in production.