Skip to main content
RunBook Academy

Proxmox VEXVII · Performance EngineeringHost tuning

Host tuning: governors, C-states, THP and NUMA balancing

Advanced⏱ ~30 mincpupowerturbostatnumactl

What you'll learn

  • Measure the CPU frequency governor, idle policy and achieved frequency on a running node
  • Distinguish a throughput problem from a wakeup-latency problem using PSI and turbostat
  • Decide between transparent hugepage modes for a KVM host, and set defrag correctly
  • Recognise when automatic NUMA balancing fights an explicit VM NUMA topology
  • Persist host tuning so it survives a reboot and a kernel upgrade

Prerequisites

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.

There is a class of performance ticket that survives every VM-level fix. The guest has enough vCPU. It has enough RAM. Its disk is on NVMe. The host graph is flat at 30% CPU. And the application still misses its latency target by a factor of three.

The settings in this lesson are the usual answer. They live on the host, they apply to every guest on it, and none of them show up in qm config. An operator who has only ever tuned VMs cannot find them, which is why this ticket gets escalated rather than solved.

The one idea that makes the rest make sense

The Linux course states it as a rule and it is worth repeating here because every measurement in this lesson depends on it:

Utilisation is not saturation.

A host at 30% CPU utilisation is not a host with 70% of its performance in reserve. Utilisation says what fraction of wall-clock time a CPU spent executing something. It says nothing about how fast it executed, nor about how long a task waited before it got a turn. A node parked at its lowest frequency with deep idle states enabled is at 30% utilisation and delivering perhaps a third of the work per second it is capable of.

The four levers below each attack a different version of that gap.

LeverThe gap it closesCosts
Frequency governorClock speed under bursty loadPower, heat
C-states / idle policyWakeup latency, measured in microsecondsPower, heat
Transparent hugepagesGuest memory address-translation overheadLatency spikes if defrag is wrong
NUMA balancingRemote memory access on multi-socket hostsPage-fault overhead; fights explicit pinning

Measure first: what is this node actually doing?

Read-only / Safethe four numbers, before you change anything
# Which scaling driver and governor?
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

# What is every core actually clocked at, right now?
grep 'MHz' /proc/cpuinfo | sort -u -t: -k2 -n | head

# Which idle states exist, and how deep do they go?
cpupower idle-info

# The consolidated view
cpupower frequency-info
Read-only / Safea node that is quietly capped
# cpupower frequency-info
analyzing CPU 0:
driver: intel_pstate
CPUs which run at the same hardware frequency: 0
CPUs which need to have their frequency coordinated by software: 0
maximum transition latency:  Cannot determine or is not supported.
hardware limits: 800 MHz - 3.70 GHz
available cpufreq governors: performance powersave
current policy: frequency should be within 800 MHz and 3.70 GHz.
                The governor "powersave" may decide which speed to use
                within this range.
current CPU frequency: 1.19 GHz (asserted by call to hardware)
boost state support:
  Supported: yes
  Active: yes

Illustrative output

The line that matters is the last frequency reading, not the governor name. A busy node under the powersave governor should still be near its maximum; a node sitting at 1.19 GHz while the application complains is either genuinely idle between bursts, or the frequency ramp is losing the race against a bursty workload.

Saturation, not utilisation: read PSI

Pressure Stall Information is the metric that answers “is anything waiting?” — which is the question utilisation cannot answer.

Read-only / Safeis anything actually stalled?
cat /proc/pressure/cpu
cat /proc/pressure/io
cat /proc/pressure/memory
Read-only / Safe30% utilisation, and tasks are still waiting
# cat /proc/pressure/cpu
some avg10=8.11 avg60=7.44 avg300=6.90 total=48213991

Illustrative output

Lever 1 — the frequency governor

On modern Intel and AMD server parts the relevant driver is intel_pstate or amd-pstate, and it exposes only two governor names: powersave and performance. Both are hardware-driven; powersave here is not the classic cpufreq powersave that pins the CPU to its minimum. It is a demand-following policy, and it is the right default for most hosts.

performance pins the requested P-state to the maximum. It does not change your electricity bill by a rounding error — on a dense node it is tens of watts per socket, continuously.

Configuration changeset the governor on every core
cpupower frequency-set -g performance

# verify it took, on every CPU rather than just cpu0
grep . /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor | sort -u

Lever 2 — C-states and wakeup latency

This is the lever most often needed and least often understood, because its symptom does not look like a performance problem at all.

A CPU in a deep idle state (C6, C7) has powered down parts of its cache and clock domains. Waking it takes time — tens to low hundreds of microseconds. That is invisible to a throughput benchmark, which keeps the core busy so it never idles. It is very visible to a workload made of many short, latency-sensitive wakeups: small network requests, a synchronous database commit, an NFS or Ceph client round trip.

Read-only / Safewhich idle states, and how long does exit take?
cpupower idle-info

# how much time is actually spent in each state?
grep . /sys/devices/system/cpu/cpu0/cpuidle/state*/name
grep . /sys/devices/system/cpu/cpu0/cpuidle/state*/latency
grep . /sys/devices/system/cpu/cpu0/cpuidle/state*/time
Read-only / Safeexit latency is not a rounding error
# cpupower idle-info
CPUidle driver: intel_idle
CPUidle governor: menu
analyzing CPU 0:

Number of idle states: 4
Available idle states: POLL C1 C1E C6
POLL:
Latency: 0
C1:
Latency: 2
C1E:
Latency: 10
C6:
Latency: 133

Illustrative output

There are two places to limit idle depth, and they are not equivalent.

In firmware. The BIOS setting (often “Package C-State Limit”, “CPU Power Management” or a “Performance” system profile) is the durable answer and the one that also governs package-level states the OS cannot reach. Part II covers it as a pre-install decision, and it is the right place to make it.

On the kernel command line. intel_idle.max_cstate=1 or the generic processor.max_cstate=1 caps how deep the OS will go. This is the reversible one, which makes it the right tool for testing whether C-states are implicated before you book a firmware window.

Service impact possiblecap idle depth for a test — GRUB installs
proxmox-boot-tool status

# GRUB installs only — edit GRUB_CMDLINE_LINUX_DEFAULT, then:
update-grub
Service impact possiblecap idle depth for a test — systemd-boot installs
cat /etc/kernel/cmdline

# after editing that single line:
proxmox-boot-tool refresh

Lever 3 — transparent hugepages

A guest’s memory is backed by host pages, and every guest memory access walks two page tables — the guest’s own and the host’s EPT or NPT. Larger host pages mean shallower walks and fewer TLB entries burnt on the same amount of guest RAM, which is why THP is generally a win on a KVM host and why it is enabled by default.

The setting that goes wrong is not enabled. It is defrag.

Read-only / Safeboth THP settings, and whether they are being used
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag

grep -E 'AnonHugePages|HugePages_Total' /proc/meminfo
Read-only / SafeTHP on, and actually in use
# cat /sys/kernel/mm/transparent_hugepage/enabled /sys/kernel/mm/transparent_hugepage/defrag
[always] madvise never
always defer [defer+madvise] madvise never

Illustrative output

defrag=always means a page fault that cannot immediately find a free hugepage will synchronously compact memory to build one. On a host whose memory is fragmented — which is any host that has been up for months running VMs of assorted sizes — that compaction can stall the faulting task for milliseconds. The symptom is periodic latency spikes with no corresponding CPU or I/O event, which is exactly the kind of thing that gets blamed on the storage.

defer+madvise is the safe production setting on a hypervisor: the kernel does not stall the fault, it wakes kswapd and khugepaged to do the work in the background, and applications that explicitly asked with madvise still get synchronous treatment.

Configuration changeset defrag to defer+madvise
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag

cat /sys/kernel/mm/transparent_hugepage/defrag

Lever 4 — automatic NUMA balancing

On a multi-socket host, memory attached to the other socket costs more to reach — typically 1.5 to 2 times the local latency. Automatic NUMA balancing is the kernel periodically unmapping pages, catching the resulting faults, and migrating pages or tasks so they end up on the same node.

It is a good default for a host running ordinary VMs whose placement nobody has thought about. It is actively harmful in one specific case.

Read-only / Safetopology, current setting, and whether it is doing anything
numactl --hardware

cat /proc/sys/kernel/numa_balancing

numastat
numastat -c qemu

The harmful case: a VM that has been given an explicit NUMA topology (qm set --numa 1) and pinned with --affinity has had its placement decided deliberately. Automatic balancing does not know that, and will keep sampling, faulting and migrating pages that were already in the right place. You pay the page-fault overhead for a migration that has nothing to gain.

Configuration changedisable automatic NUMA balancing
sysctl -w kernel.numa_balancing=0

sysctl kernel.numa_balancing

Lever 5 — swappiness

The Proxmox ZFS documentation recommends lowering vm.swappiness to 10 on servers. The default of 60 tells the kernel to be fairly willing to swap anonymous pages out in favour of keeping page cache. On a hypervisor the anonymous pages in question are guest RAM, and swapping guest RAM to disk produces the worst latency in the entire stack: the guest has no idea it happened and cannot compensate.

Configuration changelower swappiness
sysctl -w vm.swappiness=10

free -h
sysctl vm.swappiness

On a ZFS-root node there is an additional interaction worth knowing: the ARC is not page cache and is not reclaimed by the same path, so a node with an unbounded ARC and low swappiness can still end up with memory pressure that looks inexplicable. That is a storage-backend concern and the per-backend tuning lesson covers it.

Persisting all of it

Everything above is lost at the next reboot unless you write it down somewhere the boot process reads. Three mechanisms, three kinds of setting.

Configuration changesysctl settings — swappiness, NUMA balancing
cat > /etc/sysctl.d/90-pve-host-tuning.conf <<'EOF'
vm.swappiness = 10
kernel.numa_balancing = 0
EOF

sysctl --system
sysctl vm.swappiness kernel.numa_balancing
Configuration changesysfs settings — THP defrag, governor
cat > /etc/systemd/system/pve-host-tuning.service <<'EOF'
[Unit]
Description=Proxmox host performance tuning
After=multi-user.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag'
ExecStart=/usr/bin/cpupower frequency-set -g performance

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now pve-host-tuning.service
systemctl status pve-host-tuning.service --no-pager

Kernel command-line settings — C-state limits, IOMMU — go in /etc/default/grub plus update-grub on GRUB installs, or /etc/kernel/cmdline plus proxmox-boot-tool refresh on systemd-boot installs. proxmox-boot-tool status tells you which one this node is.

Verification that can fail

A tuning change you cannot prove did anything is a tuning change you should not have made. Each lever has a check that returns a value you can compare against the one you recorded first.

Read-only / Safethe after-state, in one block
echo "== governor =="
grep . /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor | sort | uniq -c

echo "== achieved frequency =="
grep 'MHz' /proc/cpuinfo | sort -u -t: -k2 -n | tail -3

echo "== idle depth =="
cpupower idle-info | grep -E 'Available idle|Latency'

echo "== THP =="
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
grep AnonHugePages /proc/meminfo

echo "== numa =="
sysctl kernel.numa_balancing
numastat | head -6

echo "== pressure =="
cat /proc/pressure/cpu

The one that most often disagrees with expectation is the governor line. cpupower frequency-set applies to every online CPU, but a core that was offline at the time — or a node where the systemd unit ran before all cores were up — ends up with a mixed policy. sort | uniq -c across all CPUs catches that; reading cpu0 alone does not.

Common mistakes

  • Reading cpu0 and assuming the rest match. Governor and idle policy are per-CPU. Aggregate with sort | uniq -c across all of them.
  • Treating powersave under intel_pstate as the old cpufreq powersave. It is a demand-following policy, not a minimum-frequency pin, and it is a reasonable default.
  • Concluding the host is fine because utilisation is low. C-state exit latency and compaction stalls consume no CPU time and appear in no utilisation graph. Read PSI.
  • Leaving defrag=always on a long-uptime hypervisor. Synchronous compaction on a fragmented host produces millisecond latency spikes that get blamed on storage.
  • Disabling NUMA balancing on a node with unpinned guests. It helps there. Disable it only where explicit numa and affinity have already decided placement.
  • Confusing THP with qm set --hugepages. Different mechanisms, different costs; one is host-wide and opportunistic, the other is per-VM, pinned, and disables ballooning.
  • Tuning one node in a cluster. Live migration then makes performance depend on placement, with nothing in any log to explain it.
  • Not persisting sysfs settings. sysctl.d does not cover sysfs. The change survives until the next reboot and then quietly disappears.

Key takeaways

  • Utilisation is not saturation. A host at 30% CPU can still be the bottleneck; /proc/pressure/cpu is the metric that says so.
  • The governor request is not the achieved frequency. Bzy_MHz from turbostat is what the application experiences.
  • C-state exit latency is invisible to every utilisation graph and shows up as bad latency with fine throughput, worse when the host is less busy.
  • Cap C-states on the kernel command line to test the hypothesis; fix it in firmware once you have proved it.
  • THP is a win on KVM hosts because it shortens the two-level EPT/NPT walk. Set defrag=defer+madvise to get it without compaction stalls.
  • Disable automatic NUMA balancing only where explicit numa and affinity have already placed the guests.
  • Lower vm.swappiness to 10, but keep swap — a hypervisor with no swap reaches the OOM killer instead, and the OOM killer kills guests.
  • Persist sysctl in /etc/sysctl.d, sysfs in a systemd unit, and cmdline via the bootloader proxmox-boot-tool status names.
  • Tune per class of node and keep the class uniform, or live migration turns tuning into an unattributable performance mystery.

Knowledge check

Knowledge check · 5 questions

  1. Q1. A guest misses its latency target. Host CPU utilisation is flat at 25%, storage is idle, and the guest has ample vCPU and RAM. Throughput benchmarks inside the guest look normal. Which host-level cause best fits this evidence?

  2. Q2. On a long-uptime PVE node, guests show periodic multi-millisecond latency spikes with no matching CPU or I/O event. /sys/kernel/mm/transparent_hugepage/defrag reads "[always] defer defer+madvise madvise never". What is happening and what is the fix?

  3. Q3. Automatic NUMA balancing should be disabled on any multi-socket Proxmox host, because explicit placement is always better than the kernel guessing.

  4. Q4. Which statements about persisting host tuning on PVE 9 are correct? Select all that apply.

  5. Q5. After setting the performance governor, turbostat reports Bzy_MHz of 2.4 GHz while cpupower frequency-info reports hardware limits up to 3.7 GHz. What is the most likely explanation?

Passing score: 75%. Answers are checked in this browser.