Optimizing memory and killing SWAP exhaustion on Linux servers with aaPanel
Back to blog

Optimizing memory and killing SWAP exhaustion on Linux servers with aaPanel

Updated 9/20/2026 · Published 9/17/2026 · 4 min · Linux

Managing a VPS using web control panels like aaPanel seems straightforward initially: a few clicks, installed stacks, and you are good to go. However, once you deploy in a real production environment (on a VM running Rocky Linux 8.10 with 11 GB of RAM), default template configurations quickly lead to I/O latency and resource exhaustion.

Recently, I encountered a typical memory bottleneck: the operating system reported plenty of free and available RAM, yet the SWAP partition was nearly maxed out (1.76 GB used out of 2.0 GB). This resulted in continuous disk I/O churn and noticeable latency in HTTP response times.

Below is the technical breakdown, the individual service analysis, and the practical steps taken to drop active memory consumption from 3.29 GB down to 2.1 GB while completely clearing SWAP usage.

1. The diagnosis: pinpointing the bottleneck#

The initial step was inspecting memory and swap utilization using standard Linux diagnostics:

free -h
vmstat 1 5

The output from free -h highlighted the issue:

               total        used        free      shared  buff/cache   available
Mem:            11Gi       3.29Gi       4.82Gi       184Mi       3.01Gi       7.65Gi
Swap:          2.0Gi       1.76Gi       244Mi

Despite having over 7.6 GB of available physical RAM, the kernel was actively paging anonymous memory to swap. To identify which processes were consuming resident set size (RSS) and forcing pages to disk, I ran:

ps aux --sort=-%mem | awk 'NR<=15{print $2, $4, $6/1024 " MB", $11}'

The primary consumers were:

  1. mysqld / mariadbd: Allocating over 1.4 GB for the InnoDB buffer pool, which exceeded actual requirements for the current traffic load.
  2. PHP-FPM (multiple versions): Multiple pools configured in dynamic mode, keeping dozens of idle worker processes resident in memory.
  3. aaPanel Background Daemon (BT-Task and Python): Monitoring tasks retaining memory cache.
  4. Redis: Lacking a strict maxmemory cap, retaining expired keys in RAM.

2. Tuning MySQL / MariaDB (innodb buffer pool)#

In aaPanel, MySQL defaults are often computed based on total server RAM detected at setup time, which fails when the machine shares resources across multiple distinct stacks.

Edit the MySQL configuration file at /etc/my.cnf:

[mysqld]
# Tuned for realistic VM workload (previously set to 1536M)
innodb_buffer_pool_size = 512M
innodb_log_buffer_size = 16M
max_connections = 120
key_buffer_size = 32M
table_open_cache = 400

Save the file, restart the daemon, and check memory consumption:

systemctl restart mysqld
systemctl status mysqld --no-pager

This adjustment immediately freed nearly 900 MB of resident memory without impacting query response times.

3. Taming idle PHP-FPM pools#

When running multiple PHP versions simultaneously (e.g., PHP 7.4 for legacy applications and PHP 8.2 or 8.3 for newer apps), aaPanel maintains separate master daemons for each runtime.

Navigate to the PHP configuration directory in aaPanel (e.g., /www/server/php/82/etc/php-fpm.d/www.conf) and update the process manager configuration:

[www]
pm = ondemand
pm.max_children = 20
pm.process_idle_timeout = 15s
pm.max_requests = 500

Setting pm = ondemand ensures the PHP-FPM master spawns workers only when actual HTTP requests arrive and terminates them after 15 seconds of inactivity, returning RAM directly to the OS.

Reload the PHP daemons:

/etc/init.d/php-fpm-82 reload
/etc/init.d/php-fpm-74 reload

4. Setting memory limits and eviction policies in Redis#

When Redis runs without explicit memory caps, it can consume RAM unchecked. Edit /www/server/redis/redis.conf:

maxmemory 256mb
maxmemory-policy allkeys-lru

This ensures cache memory does not exceed 256 MB and evicts the least recently used keys when the threshold is reached.

/etc/init.d/redis restart

5. Kernel tuning: swappiness and cache pressure#

With application memory reined in, the next step was configuring the Linux kernel swap trigger thresholds. In Rocky Linux 8 (RHEL family), the default swappiness is 30, whereas in Debian/Ubuntu it is 60. In both cases, this remains too aggressive when ample RAM is free, making 10 a well-balanced target.

Create /etc/sysctl.d/99-memory-tuning.conf:

# Reduce swap aggressiveness (swap only under genuine RAM exhaustion)
vm.swappiness = 10

# Retain directory and inode caches in RAM longer
vm.vfs_cache_pressure = 50

# Prevent excessive dirty page accumulation before disk flushing
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10

Apply settings dynamically without rebooting:

sysctl --system

6. Safely flushing and cycling SWAP#

With more than 8 GB of available physical RAM, the swap space still held 1.76 GB of cold pages on disk. To safely cycle the swap space and bring relevant pages back to RAM:

# Disable swap (pages are moved back to free physical RAM)
swapoff -a

# Re-enable swap with zero utilization
swapon -a

Note: Only execute swapoff -a if free -h shows that available memory is substantially higher than current swap usage; otherwise, the kernel OOM killer will terminate active processes.

Production takeaways#

Following these adjustments, the resource profile in free -h stabilized at:

               total        used        free      shared  buff/cache   available
Mem:            11Gi       2.10Gi       6.15Gi       110Mi       2.87Gi       8.85Gi
Swap:          2.0Gi          0B        2.0Gi

By profiling and tuning services individually (MySQL, PHP-FPM, Redis) rather than relying on control panel defaults, swap-induced I/O latency disappeared, leaving over 8.8 GB of clean headroom to handle traffic spikes.

If you operate Linux servers with similar management layers (such as CloudPanel, cPanel, or aaPanel), auditing PHP pool lifecycle and kernel swappiness remains the most impactful operational fix for silent performance degradation.


Conclusion#

In this post, I walked through the step-by-step optimization that brought active VM memory usage down from 3.29 GB to 2.1 GB while completely clearing swap: right-sizing the MySQL buffer pool, switching PHP-FPM pools to ondemand, constraining Redis with maxmemory, and tuning the kernel to page only under genuine memory pressure.

Was this article helpful?

Leave a quick reaction to help prioritize future technical guides:

CC BY-NC

This post is licensed under CC BY-NC.

Comments

Join the discussion below.

0 comments