Bali, Indonesia
Insights

VPS Swap Keeps Filling Up: Memory Leak, Undersized Plan, or a Cryptominer?

October 26, 2026
0:00
Your VPS feels sluggish, free -h shows swap almost full, and a reboot fixes it for a few days before it creeps back. The usual question is whether to buy more RAM. Before you do, it helps to know there are three common causes that look identical from the outside: an application slowly leaking memory, a plan that is simply too small for what runs on it, or a cryptominer someone installed through a vulnerable site or a weak password. Each one leaves different fingerprints, and you can find them with tools already on the server. This is also one of the more common misreads at a support desk. A customer sees high swap and assumes the server is broken or oversold, when the answer is sitting in a few lines of /proc.
Swap is disk space the kernel uses to park memory pages that have not been touched recently. Linux will happily move idle pages there and leave them, even when plenty of RAM is free later. So a swap number that is high but stable is not, by itself, a problem. What hurts performance is the kernel constantly moving pages in and out, because every page read back from disk is far slower than reading RAM. That movement is what you want to measure first. vmstat shows it directly in the si (swapped in) and so (swapped out) columns, in kilobytes per second.
BASH
# Totals: RAM, swap, and the 'available' column (memory usable without swapping)
free -h

# Which swap devices or files exist and how full each one is
swapon --show

# Sample every 5 seconds, 12 times. Watch the si and so columns.
vmstat 5 12

# How eager the kernel is to swap (higher means more willing)
cat /proc/sys/vm/swappiness
Ignore the first line of vmstat; it is an average since boot. If si and so sit near zero across the run, the swap is mostly cold pages and the slowdown is probably coming from somewhere else, such as CPU or disk I/O. If they stay non-zero while the site is under normal load, the server genuinely does not have enough RAM for its current working set, and the next step is finding out who that working set belongs to. Save this output; you will compare against it after any fix.
Every process exposes its swapped-out memory as VmSwap in /proc/PID/status. Most top setups do not show it by default, so it is easier to read it directly and sort.
BASH
# Top 15 processes by swapped-out memory (kB). Kernel threads have no VmSwap line and are skipped.
for f in /proc/[0-9]*/status; do
  awk '/^Name:/{n=2} /^VmSwap:/{print2, n, FILENAME}' $f 2>/dev/null
done | sort -rn | head -15

# To confirm a suspected leak, record one process over time (replace 1234 with the real PID)
PID=1234
while true; do
  date '+%F %T'
  grep -E '^(VmRSS|VmSwap):' /proc/$PID/status
  sleep 300
done
The first command gives you a ranked list with the process name and its /proc path, which contains the PID. On a typical web VPS you expect to see names like mysqld or mariadbd, php-fpm, lsphp, httpd, nginx, or a Node or Java process near the top. That alone is useful: it tells you which service to look at instead of guessing. If the numbers here add up to noticeably less than what free reports as swap used, that is not necessarily a bug in your math. Pages from shared memory and tmpfs mounts such as /dev/shm can be swapped out without being charged to any single process. A large unexplained gap is worth a look at what is stored in /dev/shm, which also happens to be a favorite place for malware to drop files.
The second loop in the block above is the leak test. Let it run across a few hours of normal traffic. VmRSS is the part of the process in RAM, VmSwap the part on disk, and together they approximate how much memory the process is holding. A leak looks like that sum climbing steadily and never coming back down, even through quiet periods, then dropping to a normal level when you restart only that service. Long-running workers in PHP-FPM, application servers, and some caching daemons are typical places to see it. If restarting one service gives you days of relief and nothing else changes, that is strong evidence. An undersized plan looks different. No single process grows without limit. Instead, memory rises with traffic and falls back when traffic drops, and the total working set simply does not fit. The usual culprits are configuration values that were never sized for the plan: too many PHP-FPM children, a database buffer pool set larger than the RAM can really spare, or several sites each with their own cache and worker pool. Multiply the per-worker memory you saw in the ranked list by the maximum worker count in your config, add the database, and compare that against total RAM. If the math does not fit, tuning or resizing is the honest answer. Swappiness is the knob people reach for first, and it rarely solves either case. Lowering it with sysctl vm.swappiness=10 (temporary until reboot) makes the kernel prefer dropping file cache before swapping out application memory. That can smooth out a server that is only slightly short on RAM. It does nothing for a leak, and on a server that is truly undersized it can push you toward the OOM killer instead of a slow swap. Change it only after the measurements, and re-run vmstat to see whether si and so actually fell.
Most cryptominers are CPU-heavy first. Swap pressure enters the picture because some mining algorithms want a lot of RAM: RandomX, used for Monero, needs roughly 2 GB for its dataset in fast mode. On a small VPS that alone can push your real services into swap. So the pattern to watch for is high swap plus sustained high CPU from a process you cannot account for. Miners also try not to look like miners. Common tricks are renaming the process to something that blends in (names resembling kernel threads, systemd helpers, or php-fpm), running from writable temp directories, deleting the binary after launch, and reinstalling themselves from cron. These checks are all read-only:
BASH
# Heaviest CPU users with owner, parent, runtime, and full command line
ps -eo pid,ppid,user,%cpu,%mem,etime,args --sort=-%cpu | head -15

# Where does a suspicious PID really run from? (replace 1234)
ls -l /proc/1234/exe
tr '\0' ' ' < /proc/1234/cmdline; echo

# Any process whose binary was deleted or lives in a temp directory
ls -l /proc/[0-9]*/exe 2>/dev/null | grep -E 'deleted|/tmp/|/var/tmp/|/dev/shm/'

# Established outbound TCP connections with owning process (run as root)
ss -tnp state established

# Common persistence spots
for u in (cut -d: -f1 /etc/passwd); do crontab -l -uu 2>/dev/null | sed 's/^/'$u': /'; done
ls -la /etc/cron.d /var/spool/cron 2>/dev/null
systemctl list-timers --all
A process name can lie, but /proc/PID/exe usually cannot. If something calling itself a kernel thread has an exe link pointing to /tmp/.x/something, it is not a kernel thread; real kernel threads have no exe target at all. Show (deleted) results some care, though: a legitimate service that was not restarted after a package upgrade also shows its old binary as deleted. Check its path and your package history before drawing conclusions. In the ss output, look for long-lived connections from processes owned by your web user (www-data, apache, nobody, or a cPanel account) to IPs and ports your applications have no reason to contact. Web server workers normally answer inbound connections; they rarely hold outbound sessions open for hours. If ps shows nothing odd but CPU stays pegged and memory does not add up, the process listing itself may be lying. Rootkits that hook process tools through the dynamic linker are covered in Checking /etc/ld.so.preload to Confirm Hidden Malware Processes, and that check belongs here before you trust any of the output above.
Once you have the measurements, the decision usually falls out of them.
What you measuredLikely causeNext step
One process's RSS + swap climbs steadily and resets on restartMemory leakFix or update the application; schedule worker recycling as a stopgap
Memory follows traffic, no single grower, worker math exceeds RAMUndersized plan or oversized configLower worker and buffer limits to fit, or resize the plan
Unknown high-CPU process, temp or deleted binary, odd outbound connectionsCompromiseIncident response, not tuning
The third row deserves a clear warning. Killing the miner process feels like a fix, but it almost never is. Whatever got it onto the server (an outdated plugin, a weak SSH or panel password, an exposed admin service) is still there, and the persistence entries you found in cron or systemd will start it again. Before removing anything, record the evidence: the ps, ss, and /proc/PID/exe output, the file paths, and the cron entries. Then find the entry point, rotate every credential the attacker could have seen, and seriously consider rebuilding from a known-clean backup rather than cleaning in place. If the miner ran as root, assume the whole server is untrustworthy. Whichever path you take, finish where you started. Run the same free, vmstat, and per-process swap commands again under similar load and compare them to the baseline you saved. If si and so are back near zero and no process is creeping upward, the fix worked. If not, the numbers will tell you which of the three causes you have not ruled out yet. The practical rule: do not upgrade RAM or touch swappiness until you have watched si/so, ranked processes by VmSwap, and spent two minutes checking for a process that should not exist. Resizing a compromised server only gives the miner more room.