Ahmad

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.

intermediate45 minupdated 2026-09-16ksmmemoryperformanceproxmoxtroubleshootingvirtualisationDownload script.sh

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

bash
free -h
swapon --show
expected output
               total        used        free      shared  buff/cache   available
Mem:            62Gi        58Gi       1.2Gi       104Mi       3.4Gi       2.1Gi
Swap:          8.0Gi       7.8Gi       196Mi

Two 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:

bash
{ 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}"
expected output
assigned to guests: 123.0 GiB

Compare that with what the host has:

bash
awk "/MemTotal/ {printf \"installed: %.1f GiB\n\", \$2/1048576}" /proc/meminfo

Overcommitment 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:

bash
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:

powershell
Get-Counter "\Memory\Committed Bytes" | Select-Object -ExpandProperty CounterSamples

Inside a Linux guest:

bash
free -h

This 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.

bash
qm set 100 --memory 6144

For a server that must never have memory taken away from it underneath -- a domain controller, a database -- pin it as well:

bash
qm set 100 --balloon 0

balloon 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:

bash
pct set 100 --memory 6144

Stop 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.

bash
qm set 100 --onboot 0

Then shut down the ones you are not using now:

bash
qm shutdown 100

Five 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.

bash
systemctl status ksmtuned

If it is not running:

bash
systemctl enable --now ksmtuned

Give it a few minutes, then see what it found:

bash
cat /sys/kernel/mm/ksm/pages_sharing
expected output
1043201

Multiply 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:

bash
Destructive command. Read it before running it. This can delete data or interrupt service.
swapoff -a && swapon -a

Read 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:

bash
free -h

Used 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.

bash
cat /sys/module/zfs/parameters/zfs_arc_max

Zero 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

bash
free -h
expected output
               total        used        free      shared  buff/cache   available
Mem:            62Gi        31Gi        27Gi       118Mi       4.1Gi        29Gi
Swap:          8.0Gi          0B       8.0Gi

Swap 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:

bash
{ 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.