Optimization

How to reduce memory usage on a Linux VPS

Your 2GB VPS is swapping, the load average is climbing, and free -h shows 200MB free. Before you reach for the upgrade button, know this: most out-of-memory situations on a Linux VPS are misconfiguration, not missing hardware. A stock LEMP stack on Ubuntu 24.04 can easily burn 1.5GB of RAM before a single visitor arrives. This guide shows you how to reduce memory usage on a Linux VPS by finding the real consumers, tuning MySQL/MariaDB, capping PHP-FPM, and setting up swap as a safety net.

  • Check what actually uses RAM before changing anything. Guessing wastes time.
  • Size the InnoDB buffer pool to ~50-60% of available RAM, not the default 128MB or the auto-sized value on bigger boxes.
  • Cap PHP-FPM workers with the pm.max_children formula: one worker per ~40-60MB of PHP memory.
  • Add 2-4GB of swap on a 2GB VPS. It is not a fix for a leak, but it prevents the OOM killer from shooting your database.

Prerequisites

  • A Linux VPS running Ubuntu 24.04 LTS, Debian 12, or another current distro. The commands here target apt-based systems; adjust for dnf (AlmaLinux, Rocky) where noted.
  • Root or sudo access.
  • A running LEMP/stack or at least a few services to tune. If the box is fresh, install something first so there is something to measure.

Why does a fresh VPS already eat 1GB of RAM?

The short answer: defaults are written for shared servers with plenty of RAM, not for a 2GB VPS. MySQL or MariaDB alone reserves the InnoDB buffer pool, MySQL 8's default is 128MB but it grows, plus per-connection buffers. PHP-FPM spawns workers, each holding a slice of memory. Nginx is light, usually 20-30MB total, but systemd, cron, and a monitoring agent each nibble a bit. It adds up.

The other half of the story is how Linux reports memory. free -h shows "available" separately from "free" because the kernel reclaims page cache under pressure. A box showing 200MB free but 800MB available is not in trouble. Learn to read that output before you panic.

The discipline here is the same as debugging any performance issue: measure, change one thing, measure again. Do not apply every tweak in this post at once, or you will not know which one helped.

How do I find which process is using the most memory?

Run this first. It lists processes by resident memory usage, the number that actually matters for pressure:

ps aux --sort=-%mem | head -20

Look at the %MEM and RSS columns. The RSS column shows resident set size, the physical RAM a process holds. You will typically see mysqld or mariadbd at the top, then php-fpm workers, then something unexpected like a crashed process or a Java app. If you see multiple php-fpm children at 80MB each and you have 20 of them, that is your problem.

For a live view, install htop and press F6 then select PERCENT_MEM to sort by memory:

apt install htop -y

htop is better than top for this because it color-codes and lets you jump to sort by memory in one keypress. Press F5 for tree view to see parent-child relationships; a master PHP process spawning 30 children becomes obvious instantly.

Step 1 - Tune MySQL or MariaDB for low RAM

The database is the biggest consumer on almost every VPS. The single most important setting is innodb_buffer_pool_size. This is the cache for data and indexes, and its default behavior on MySQL 8 is to auto-size itself, which on a 2GB box can be too aggressive. On MariaDB 10.11+, the default is 128MB, which is safe but underutilized.

Set it to about 50-60% of your RAM. For a 2GB VPS, that means 1GB. For 4GB, 2GB. Edit the config file:

nano /etc/mysql/mysql.conf.d/mysqld.cnf

On MariaDB, the path is /etc/mysql/mariadb.conf.d/50-server.cnf. Add these under the [mysqld] section:

[mysqld]
innodb_buffer_pool_size = 1G
performance_schema = OFF
skip-innodb-doublewrite

Wait, do not enable skip-innodb-doublewrite unless you understand the tradeoff. It reduces write amplification and memory use but risks corruption on a sudden power loss. On a VPS backed by enterprise storage, many hosts disable it. I keep it OFF only on test boxes; for production I leave doublewrite ON and save memory elsewhere.

performance_schema = OFF is a safe win on a small box. It is a diagnostic feature that costs about 100-200MB of RAM on a busy server. You lose access to some performance views in SHOW ENGINE PERFORMANCE_SCHEMA, but on a 2GB VPS you cannot afford it.

Also cap the number of connections. Each connection eats a few MB of buffers:

[mysqld]
max_connections = 50

After editing, verify the syntax and restart:

mysqld --validate-config
systemctl restart mysql

Or mariadbd --validate-config if you run MariaDB. Then confirm the buffer pool took effect:

mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"

Expected output: +-------------------------+-----------+ | Variable_name | Value | +-------------------------+-----------+ | innodb_buffer_pool_size | 1073741824 | +-------------------------+-----------+

The value is in bytes: 1073741824 is exactly 1GB. If you see 134217728 (128MB), the setting did not get picked up, which usually means you edited the wrong file or another config overrides it. Run my_print_defaults mysqld to see which files are read in order.

Step 2 - Cap PHP-FPM workers so they stop multiplying

PHP-FPM is the second biggest consumer. The default pm = dynamic setting lets the pool grow up to pm.max_children, and many distros set that number too high for a small VPS. The formula is simple: divide your available RAM by the average PHP process size. If a WordPress worker uses 60MB and you have 1GB left after the database, that is roughly 16 workers.

Find the pool config, usually /etc/php/8.3/fpm/pool.d/www.conf:

nano /etc/php/8.3/fpm/pool.d/www.conf

Change these values:

pm = ondemand
pm.max_children = 12
pm.process_idle_timeout = 30s

Set pm = ondemand instead of dynamic. This is the biggest single win for a low-RAM VPS. Ondemand spawns workers only when a request arrives and kills them after process_idle_timeout. A quiet site uses near-zero PHP memory. A busy site still scales up to max_children.

The tradeoff: ondemand spends a few milliseconds spawning a worker on a cold request, and with opcache disabled that means recompiling PHP scripts each time. Keep opcache on with a reasonable memory limit:

nano /etc/php/8.3/cli/conf.d/10-opcache.ini
opcache.memory_consumption = 64
opcache.max_accelerated_files = 4000

Then restart PHP-FPM:

systemctl restart php8.3-fpm

Verify worker count on an idle site:

ps aux | grep php-fpm | grep -v grep | wc -l

Expected output: 2 or 3 (the master plus maybe one idle child). If your site had traffic, a value under 12 means the cap is working. Before this change you might have seen 20-30 workers.

Step 3 - Trim Nginx, Redis, and other services

Nginx is usually a minor consumer, 20-40MB total. But its worker processes are tied to CPU cores, and each holds some memory for connection pools. If you are on a 1-core VPS and see 4 workers, that is unnecessary:

nano /etc/nginx/nginx.conf
worker_processes 1;

Set it to auto if you are not sure how many cores you have, or set it explicitly to the number of vCPUs. Nginx is event-driven; one worker handles thousands of connections. Four workers on one core just wastes a few MB.

If you run Redis purely as a cache, it can be tuned to stay lean. The default maxmemory is 0, meaning it can grow until the OS kills it. Set a cap and a sensible eviction policy:

nano /etc/redis/redis.conf
maxmemory 128mb
maxmemory-policy allkeys-lru

allkeys-lru evicts the least-recently-used keys when the limit is hit, which is what you want for a cache. Never use noeviction on a cache; it errors out instead of evicting, and your app starts throwing OOM exceptions. Restart Redis and check its actual usage:

systemctl restart redis-server
redis-cli INFO memory | grep used_memory_human

Expected output: used_memory_human:1.50M or similar. The point is not the exact number, it is that you capped the ceiling so it can never climb past 128MB.

Step 4 - Add swap and tune swappiness

Swap is not a substitute for enough RAM, but it is a critical safety net. When memory spikes, swap absorbs the burst and the OOM killer stays quiet. The OOM killer can shoot your database or PHP master, and that is a much worse failure mode than a few seconds of disk I/O.

Create a 2GB swap file on a 2GB VPS:

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile swap swap defaults 0 0' >> /etc/fstab

Check it is live:

swapon --show

Expected output: NAME TYPE SIZE USED PRIO /swapfile file 2G 0B -2

Then tune swappiness. The default of 60 means the kernel swaps fairly eagerly. On a VPS with NVMe storage, the disk is fast enough that some swapping is tolerable. For a database server, keep swappiness around 10 so the kernel prefers keeping hot data in RAM:

echo 'vm.swappiness=10' >> /etc/sysctl.d/99-swap.conf
sysctl -p /etc/sysctl.d/99-swap.conf

Verify:

cat /proc/sys/vm/swappiness

Expected output: 10. A note on vfs_cache_pressure: leave it at the default unless you have a specific reason. Setting it to 50 can keep more directory inode cache in RAM, which helps file-heavy workloads, but it also means less RAM for your database buffer pool. Test before adopting it.

Step 5 - Kill the CPU hogs and memory leaks

Some memory consumers are not services you configured; they are processes that went wrong. A classic example on a 2GB VPS is WordPress cron spawning hundreds of wp-cron.php processes, or a PHP script with a memory leak. The kernel eventually invokes the OOM killer, but it might kill the wrong process.

Before the OOM killer acts, set up a quicker tripwire. earlyoom is a daemon that watches memory pressure and kills the biggest consumer before the kernel's OOM killer does:

apt install earlyoom -y
systemctl enable --now earlyoom

This is not a fix; it is insurance. The real fix is finding the leak. Use journalctl -u php8.3-fpm to see if workers are being killed and restarting in a loop. If a single process grows over hours, watch it:

watch -n 60 'ps -o pid,rss,cmd -p $(pgrep -f your-process)'

If RSS climbs steadily, you have a leak. Fix the code, cap the process with a supervisor limit, or restart it on a schedule with systemd timers.

Troubleshooting

The server is still swapping after all these changes. Check if you actually restarted everything. systemctl restart mysql php8.3-fpm nginx covers the main stack. Then watch live pressure: vmstat 1 5 shows si and so columns; if they are non-zero, swapping is active. If it still swaps, your working set genuinely exceeds RAM, and it is time to scale up. That is not a failure of this guide; it is data.

MySQL refuses to start after changing the buffer pool. Check the error log: journalctl -u mysql -n 50 or /var/log/mysql/error.log. The most common cause is setting innodb_buffer_pool_size larger than available RAM, so the allocation fails at startup. Lower it to 512M and retry.

PHP-FPM workers are still spawning under ondemand. Verify the pool file you edited is the one being read. Run php-fpm8.3 -tt to validate the syntax, and check the listening socket: ss -tlnp | grep php. If your site uses a different pool (e.g. www2.conf), edit that one instead.

FAQ

How much swap should I add to a 2GB VPS?

Add 2GB of swap. That doubles the memory headroom for bursts, and on modern NVMe storage the performance penalty is acceptable. If you see heavy swap usage constantly, that is a signal to add RAM, not swap.

Is it better to use swappiness 10 or 60?

Use 10 for a database server or any workload where stealing memory from the InnoDB buffer pool hurts. Use 60 if you run many short-lived processes and prefer the kernel to reclaim page cache aggressively. There is no universal best; test both under load.

Why does my VPS show low free memory even after optimization?

Linux uses free RAM for page cache by design. Run free -h and look at the "available" column, not "free". If "available" is high, the box is healthy. If both are low, you have a real shortage.

Does disabling performance_schema break MySQL monitoring tools?

Some newer monitoring tools read performance_schema tables. If you rely on them, keep it on and save memory elsewhere, for example by lowering max_connections or reducing the buffer pool by 128MB.

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

Yes, but only for light workloads. Set the InnoDB buffer pool to 256MB, cap Redis at 128MB, and keep PHP on ondemand. A 1GB box running WordPress, MySQL, Redis, and Nginx is tight but workable for a low-traffic site.

Related articles

Linux VPS 内存优化与降低占用方法

本文介绍如何在 Linux VPS 上降低内存占用:先用 ps 和 htop 找出占用最高的进程,然后调整 MySQL 的 innodb_buffer_pool_size 到内存的 50-60%,将 PHP-FPM 改为 ondemand 模式并限制 max_children,同时为 Redis 设置 maxmemory 上限。最后建议添加 2GB swap 并将 swappiness 调低到 10,作为内存峰值时的安全网。如果调优后仍然频繁交换,说明工作集确实超出内存,应考虑升级配置。

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.