Fix high swap usage on Proxmox without buying RAM
Swap at 98 percent and the web interface crawling does not mean the host is short of memory. It usually means the guests have been promised far more than the host owns. Measuring what they actually use, then right-sizing, recovered a host running twenty guests on 62 GiB.
Before you start
A Proxmox VE 8 or 9 host you can reach over SSH as root, and the ability to shut down or restart at least one guest. Nothing here needs a subscription repository. The worked example is a 16-thread host with 62 GiB of RAM running about twenty guests, but the method does not depend on the size.
Your values
Fill these in and every command below updates to match. Nothing is sent anywhere.
Run this at your own risk. These steps worked on my hardware; yours may differ. Take a backup first, read each command before running it, and see the disclaimer.
Swap sitting at 98 percent, the web interface taking seconds to paint, and consoles that freeze for a moment before accepting a keystroke. The instinct is to buy memory. Usually the host does not need any.
What has actually happened is that the guests have been promised more memory than the host physically owns. One host here had about 123 GiB assigned across its guests and 62 GiB installed -- roughly twice what existed. That is fine right up until enough of it is touched at once, and then the kernel starts paging to disk and everything slows down together.
This walks through measuring it rather than guessing, and fixing it by giving memory back. No hardware, no reinstall.
Confirm it is really swap, and not something else
free -h
swapon --show total used free shared buff/cache available
Mem: 62Gi 58Gi 1.2Gi 104Mi 3.4Gi 2.1Gi
Swap: 8.0Gi 7.8Gi 196MiTwo numbers matter. Swap used near its total means the kernel has been pushing pages to disk for a while. Available is the honest figure for how much a new process could get -- not "free", which ignores reclaimable cache. Available in the low hundreds of MiB on a 62 GiB host is the problem.
If swap is nearly empty and memory still looks full, stop here: this is not your fault. Read the ZFS step near the end instead.
Add up what you have actually promised
The web interface shows each guest separately, which is exactly the wrong view for this question. Ask the host for the total:
{ for id in $(qm list | awk "NR>1 {print \$1}"); do qm config $id; done; \
for id in $(pct list | awk "NR>1 {print \$1}"); do pct config $id; done; } \
| awk "/^memory:/ {sum += \$2} END {printf \"assigned to guests: %.1f GiB\n\", sum/1024}"assigned to guests: 123.0 GiBCompare that with what the host has:
awk "/MemTotal/ {printf \"installed: %.1f GiB\n\", \$2/1048576}" /proc/meminfoOvercommitment is not automatically wrong -- it is the point of ballooning, and guests rarely touch everything they are given. Two times is where it stops being a clever use of resources and starts being a queue for the disk.
Find the guests that do not need what they were given
Do not trust the allocation. Ask each guest what it is really using.
Proxmox reports the guest view when the QEMU guest agent is installed:
qm agent 100 ping && qm config 100 | grep -E "^(memory|balloon|agent):"Inside a Windows guest, the number to look at is Committed, in Task Manager under Performance then Memory, or in PowerShell:
Get-Counter "\Memory\Committed Bytes" | Select-Object -ExpandProperty CounterSamplesInside a Linux guest:
free -hThis is where the real finding usually is. A domain controller on this host had 16 GiB assigned. Its committed memory was 4.4 GiB, its directory database was 40 MB on disk, and the process everyone assumes is enormous -- LSASS -- was using 157 MB. It had been given four times what it had ever asked for, purely because 16 looked like a safe number when the VM was created.
Check every guest before changing any of them. The ones that surprise you are rarely the ones you suspected.
Give the memory back
Memory changes take effect at the next start, so plan a restart window.
qm set 100 --memory 6144For a server that must never have memory taken away from it underneath -- a domain controller, a database -- pin it as well:
qm set 100 --balloon 0balloon 0 does not mean no memory. It means no ballooning: the guest is given
exactly what was assigned and the host will not reclaim it. Use it where a
stall would be worse than a little waste, and leave ballooning on everywhere
else, because that is what lets sensible overcommitment work at all.
For containers the change is live, no restart needed:
pct set 100 --memory 6144Stop the lab guests from starting at boot
Half the guests on a homelab host exist for an afternoon of testing and then start themselves forever. They cost memory whether or not anyone logs in.
qm set 100 --onboot 0Then shut down the ones you are not using now:
qm shutdown 100Five test machines at 4 GiB each is 20 GiB, which on the host in the example was a third of the problem on its own. They are still there, still bootable, just not holding memory while nobody watches.
Turn KSM back on
Kernel Same-page Merging finds identical memory pages across VMs and keeps one copy. On a host running several guests of the same operating system that is a real saving, often several GiB, for no configuration.
systemctl status ksmtunedIf it is not running:
systemctl enable --now ksmtunedGive it a few minutes, then see what it found:
cat /sys/kernel/mm/ksm/pages_sharing1043201Multiply by 4096 for bytes: that figure is about 4 GiB of duplicate pages collapsed into one copy each.
Two honest caveats. KSM only merges pages between virtual machines -- LXC containers share the host kernel and are not covered. And merging memory across guests has known side-channel research against it, so on a host where guests belong to parties who should not learn anything about each other, leave it off. On a homelab where every guest is yours, it is close to free.
Drain the swap
Once memory is genuinely free, the pages already written to disk stay there and stay slow. This pushes them back into RAM:
swapoff -a && swapon -aRead the warning before running that one. swapoff has to fit everything in
swap back into RAM. If the free memory is smaller than the used swap, the
kernel starts killing processes to make room, and it does not ask which ones
matter. Check first:
free -hUsed swap must be comfortably smaller than available memory. If it is not, shut down one more guest and check again.
If memory is full but swap is empty, look at ZFS
A Proxmox host installed on ZFS gives the ARC read cache a share of RAM, and that memory shows as used. It is reclaimable, so it is rarely the real problem, but it is worth knowing about before resizing guests that were not the cause.
cat /sys/module/zfs/parameters/zfs_arc_maxZero means the built-in default, which has changed between Proxmox releases -- do not assume, check what your version does. If the host also serves storage, leave the ARC alone; cache is doing useful work. A host installed on LVM-thin, as in the worked example, has no ARC at all and this step does not apply.
Verify
free -h total used free shared buff/cache available
Mem: 62Gi 31Gi 27Gi 118Mi 4.1Gi 29Gi
Swap: 8.0Gi 0B 8.0GiSwap empty, available memory in the tens of GiB, and the web interface responding immediately again. No hardware was bought and nothing was reinstalled.
What to keep doing
Check the assigned total against installed memory whenever you add a guest. It is one command and it is the number that actually predicts this problem:
{ for id in $(qm list | awk "NR>1 {print \$1}"); do qm config $id; done; \
for id in $(pct list | awk "NR>1 {print \$1}"); do pct config $id; done; } \
| awk "/^memory:/ {sum += \$2} END {printf \"assigned: %.1f GiB\n\", sum/1024}"Size guests from what they use plus headroom, not from what looks like a round
number. And when a test machine has served its purpose, set onboot 0 the same
day, while you still remember what it was for.