Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-12
○Not yet marked complete on this device.
Somebody opens a ticket saying their VM is slow. Then a second person
does. Then a third, from a different team, about an unrelated
application. All three guests happen to live on the same node.
That shape — several unrelated guests degrading together on one host — is
the signature of a noisy neighbour, and it is one of the few performance
problems where the answer really is a configuration change on a single
guest rather than a capacity purchase. Provided you can prove which guest,
which is where most attempts at this go wrong.
Step 1: prove it is contention, not the node
Before hunting a guest, establish that guests are waiting for something.
Utilisation will not tell you; pressure will.
Read-only / Safeis anything waiting, and for what?— Read-only. The three pressure files distinguish the resource under contention. 'some avg10' rising on cpu points at scheduling; on io it points at the storage path; on memory it points at reclaim.
A load average well above nproc with CPU pressure climbing is CPU
contention. I/O pressure climbing with CPU pressure flat is a storage
queue. Memory pressure means reclaim, which is a different lesson.
This matters because the containment tool differs by resource, and
applying a CPU limit to an I/O problem does nothing except make one team
believe they were throttled for no reason.
Step 2: attribute it to a guest
Proxmox puts every guest in its own cgroup, which makes attribution
mechanical rather than a guessing game.
Read-only / Safewhich guest is consuming the host?— Read-only. systemd-cgtop sorts live by cgroup, and PVE names each guest's scope qemu.slice/VMID.scope for VMs and lxc/VMID for containers, so the offender is identified by VMID directly.
systemd-cgtop --depth=3 -n 4
# the same question from the process side
pidstat -u -p ALL 2 3 | sort -k8 -rn | head -15
Read-only / SafeVMID 141 is the whole story— Illustrative. The qemu.slice scope names carry the VMID, so no correlation step is needed: 141 is using nearly seven cores while its neighbours use fractions of one.
For I/O the equivalent attribution is per-cgroup byte counters.
Read-only / Safewhich guest is generating the I/O?— Read-only. io.stat under each guest's cgroup gives cumulative read and write bytes and IOPS per block device, keyed by major:minor. Sample it twice and subtract to get a rate.
# VMs
grep . /sys/fs/cgroup/qemu.slice/*.scope/io.stat 2>/dev/null
# containers
grep . /sys/fs/cgroup/lxc/*/io.stat 2>/dev/null
# and the pressure each guest is itself experiencing
grep . /sys/fs/cgroup/qemu.slice/*.scope/io.pressure 2>/dev/null
Step 3: choose the right lever
Proxmox exposes three distinct CPU controls and they are not
interchangeable. The most common mistake in this whole subject is
reaching for a hard cap when a weight was wanted.
Option
Type
Accepted
Default
Effect
cpuunits
Relative weight
1 - 262144, clamped to 1 - 10000
100 (cgroup v2)
Share of CPU when contended. No effect on an idle host.
cpulimit
Hard cap
0 - 128
0 (none)
Total host CPU time in cores. 2.5 means 250%. Applies always.
cores / sockets
Topology
—
—
How many vCPUs exist at all. Changing it needs a guest restart.
The documentation describes cpuunits as “often called CPU shares or
CPU weight… a relative weight”, and cpulimit as “a floating point
value representing CPU time in percent, so 1.0 is equal to 100%, 2.5 to
250%”.
The cpuunits range has a wrinkle worth knowing. The qm man page
documents 1 - 262144, but the API schema adds that the value “will be
clamped to [1, 10000] in cgroup v2” — and PVE 8 and later run cgroup v2
exclusively. So a guest set to 50000 is silently running at 10000,
and two guests set to 50000 and 200000 have the same weight as each
other rather than a 4:1 ratio. Stay inside 1 - 10000 and the number you
set is the number in effect.
The difference in practice:
cpuunits costs nothing when the host is not contended. A guest
with cpuunits 50 on a quiet node runs exactly as fast as one with
cpuunits 400. The weight only decides who yields when the host runs
out of CPU. That makes it the safe, always-appropriate setting.
cpulimit applies even on an empty host. A guest capped at 2.0
cores can never exceed 200% of one core’s worth of host CPU time, at 3
a.m. on a node with 62 idle cores. That is sometimes what you want —
chargeback, a test guest, a batch job you have promised will never
interfere — and it is very often not.
Configuration changelower the weight of the noisy guest — the reversible first move— Applies live to a running VM; no restart, no interruption. Halving the weight relative to the default of 100 means this guest yields to its neighbours when the host is contended, and is unaffected when it is not.
Configuration changeor raise the weight of the victims instead— Applies live. Equivalent in effect and easier to justify politically, because no team is told their guest was throttled. Weight is relative, so raising some is the same as lowering others.
for VMID in 117 122; do
qm set "$VMID" --cpuunits 400
done
for VMID in 117 122; do
qm config "$VMID" | grep -E 'cpuunits'
done
Service impact possiblehard-cap a guest that must not exceed a share— Applies live, and applies unconditionally — including when the host is idle. Set this only when a guaranteed ceiling is the requirement. A cap below what the guest needs to complete its work will make it permanently slow rather than occasionally slow.
Configuration changethe container equivalents— Applies live to a running container. pct exposes the same cgroup controls with the same semantics as qm does for VMs.
CTID=205
pct set "$CTID" --cpuunits 50
pct set "$CTID" --cpulimit 2
pct config "$CTID" | grep -E 'cpuunits|cpulimit'
Step 4: I/O throttles
CPU weight does nothing for a guest saturating a storage backend. That
needs a per-disk throttle, set on the disk parameter rather than on the
guest as a whole.
Configuration changethrottle a single disk's bandwidth and IOPS— Applies live to a running VM. The mbps values are megabytes per second; the _rd and _wr variants set read and write independently, and the _max variants allow a burst above the sustained figure. Re-issue with the values removed to lift the throttle.
Step 5: verify from the cgroup, not the config file
qm config tells you what you asked for. The cgroup tells you what the
kernel is enforcing. These disagree more often than you would like —
after a failed live apply, after a migration, after a guest restart that
picked up a pending change you had forgotten about.
Read-only / Safewhat is the kernel actually enforcing?— Read-only. cpu.weight is the translated cpuunits value; cpu.max is 'quota period' in microseconds, where 'max' means uncapped. io.max carries the throttle. These are the authoritative values.
VMID=141
CG="/sys/fs/cgroup/qemu.slice/$VMID.scope"
cat "$CG/cpu.weight"
cat "$CG/cpu.max"
cat "$CG/io.max" 2>/dev/null
# and the effect: how much throttled time has this guest accumulated?
grep -E 'nr_throttled|throttled_usec' "$CG/cpu.stat"
Read-only / Safethe cap is real, and the guest is hitting it— Illustrative. cpu.max of '400000 100000' is 4 cores' worth per 100 ms period. nr_throttled climbing means the guest is being stopped at the end of periods, which is the evidence that the cap is binding rather than merely set.
nr_throttled is the number that closes the loop. A cap that is set but
never hit contributes nothing; a cap being hit constantly means the guest
is permanently constrained, which is a different conversation from
containing an occasional burst.
When a limit is the wrong answer
Containment is a tourniquet. It is the right response to one guest
behaving abnormally, and the wrong response to a node that is simply
full.
The distinction is whether the sum of what the guests need fits.
Read-only / Safeis this a noisy neighbour, or an overloaded node?— Read-only. Sum the configured vCPUs across running guests and compare against physical cores. Substantial oversubscription plus sustained CPU pressure is a capacity problem that no per-guest limit will solve.
echo "physical cores: $(nproc)"
qm list | awk 'NR>1 && $3=="running" {print $1}' | while read -r VMID; do
qm config "$VMID" | awk -v id="$VMID" '/^cores:/ {c=$2} /^sockets:/ {s=$2} END {print id, (c?c:1)*(s?s:1)}'
done | awk '{t+=$2} END {print "configured vCPUs on running guests:", t}'
cat /proc/pressure/cpu
If the node is oversubscribed and every guest genuinely wants its
allocation, throttling one guest just chooses a victim. The honest
answers are to migrate guests off, add a node, or right-size the guests —
and that is the xvii-performance-right-sizing conversation, informed by
the N x u / (N - 1) arithmetic from Part II.
Common mistakes
Reaching for cpulimit when cpuunits was wanted. A weight costs
nothing on an idle host; a cap applies always and turns a bursty guest
into a permanently slow one.
Trusting qm config as verification. The cgroup is what the kernel
enforces. cpu.weight, cpu.max and nr_throttled are the truth.
Throttling the victim. The culprit shows high throughput and low
pressure; the victims show low throughput and high pressure. Read
io.stat and io.pressure together.
Setting an I/O ceiling at the guest’s normal requirement. A binding
throttle produces guest task hangs and read-only remounts, not a gentle
slowdown. Set it above the working figure.
Not telling the guest’s owner. A throttled guest looks, from
inside, exactly like a slow one. The follow-up ticket gets
investigated from scratch by someone who does not know.
Containing a guest on a node that is simply full. A limit then just
chooses which team suffers. Compare configured vCPUs against physical
cores before deciding.
Assuming the consumer is a guest. Backups, scrubs, resilvers and
Ceph recovery are host-side and need entirely different levers.
Throttling Ceph recovery with osd_recovery_sleep on Squid or
Tentacle. mClock is the default scheduler there and forces those
settings to zero. Change the mClock profile instead.
Key takeaways
Establish contention with /proc/pressure/* before hunting a guest;
utilisation cannot answer the question.
Attribute with systemd-cgtop — PVE names scopes by VMID, so the
offender identifies itself.
cpuunits is a relative weight, default 100, and has no effect on an
uncontended host. It is the safe first move. Keep values inside
1 - 10000; cgroup v2 clamps anything higher.
cpulimit is a hard cap in cores, range 0 - 128, default 0, and
applies unconditionally. Use it only when a guaranteed ceiling is the
actual requirement.
I/O throttles go on the disk parameter and are enforced by QEMU, not by
the cgroup — so io.max stays empty even when they are active.
Verify from cpu.weight, cpu.max and nr_throttled, not from
qm config.
CFS quota stops every vCPU at once, so a throttled multi-vCPU guest
loses more performance than the quota fraction suggests.
Assign weights at guest creation in tiers. A cluster where everything
is at the default has no contention policy.
A limit on a full node just picks a victim. Check configured vCPUs
against physical cores first.
Knowledge check
Knowledge check · 5 questions
Q1. A guest bursts to 12 cores for ninety seconds each hour, degrading its neighbours. You want it to yield during contention but still finish quickly when the host is quiet. Which change fits?
Q2. Because I/O throttles set via mbps_rd and iops_wr on a VM disk are enforced by QEMU rather than by the cgroup controller, the guest cgroup io.max file can be empty while a throttle is fully in force.
Q3. A team reports their reporting VM is slow. The host it runs on is idle, the guest sits at 100% CPU, and there is no contention anywhere on the node. What should you check before investigating the application?
Q4. systemd-cgtop shows the host heavily loaded but no guest scope consuming anything unusual, and the pressure is on I/O. Which are plausible host-side consumers worth checking? Select all that apply.
Q5. On a Proxmox VE 9 node with Ceph Squid or Tentacle, which is the correct way to bias I/O toward client workloads while recovery is running?
Passing score: 75%. Answers are checked in this browser.