Skip to main content
RunBook Academy

Proxmox VEXII · High AvailabilityHA advanced

CRS and dynamic load balancing (PVE 9.2)

Advanced⏱ ~26 minha-managerpvesh

What you'll learn

  • Distinguish the basic, static and dynamic CRS scheduler modes and what each measures
  • Read and set the crs keys in datacenter.cfg, including the rebalance threshold, margin and hold duration
  • Observe a rebalance decision and attribute a migration you did not order
  • Constrain or exclude specific guests from automatic rebalancing

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.

Every time the HA stack needs to decide where a guest should run, something makes that decision. That something is the Cluster Resource Scheduler, and until recently it was easy to ignore because its default answer was crude and its scope was narrow: recovery after a node failure, and nothing else.

PVE 9.2 changes both halves. There is a dynamic scheduler mode that reads actual load, and there is an automatic rebalancer that can act on it while nothing has failed at all. If you upgrade a cluster and later find migrations in the task log that nobody ordered, this is the lesson that explains them.

The three modes

CRS is configured in /etc/pve/datacenter.cfg under a single crs key with several sub-options. The scheduler mode is ha=:

ModeDecides onBehaviour
basicGuest counts and HA rules onlyThe historical default. Spreads services numerically.
staticConfigured CPU and memory of each guestPacks by what guests were allocated, not what they use.
dynamicObserved node loadPlaces by what the nodes are actually doing.

The distinction between static and dynamic is the one that matters, and the word “static” is a little misleading. static is not a fixed placement - it is a scheduler whose inputs are static: the cores and memory numbers in each guest’s config. It does not care whether a VM configured with 32 GiB is using 2 GiB or 31 GiB.

dynamic uses observed load, so it converges on something closer to the truth

  • and it also means the scheduler’s decisions change over time on a cluster whose configuration has not changed at all.
Read-only / Safewhat is CRS set to right now
set -euo pipefail

grep -E '^crs:' /etc/pve/datacenter.cfg || echo 'crs not set - default ha=basic'

# The full parsed view, including defaults the file does not spell out.
pvesh get /cluster/options --output-format json-pretty | head -40
Cluster-wide riskchange the scheduler mode
set -euo pipefail

# Observe first. Do not enable rebalancing in the same change.
pvesh set /cluster/options --crs 'ha=dynamic'

grep -E '^crs:' /etc/pve/datacenter.cfg

Automatic rebalancing: the part that moves things

Setting ha=dynamic changes where new placements go. It does not, on its own, move a running guest. That is a separate switch, and it is off by default:

KeyDefaultWhat it controls
ha-auto-rebalance0Whether the cluster rebalances running guests at all
ha-auto-rebalance-threshold30Node imbalance percentage that triggers a rebalance
ha-auto-rebalance-margin10Minimum improvement percentage required to commit a migration
ha-auto-rebalance-hold-duration3HA rounds the condition must persist before acting
ha-auto-rebalance-methodbruteforceScoring approach: bruteforce or topsis
ha-rebalance-on-start0Use CRS when an HA service goes from stopped to started

Read those four numeric keys as one sentence: the cluster acts when nodes differ by more than 30%, the move must improve things by at least 10%, and the condition must hold for 3 consecutive HA rounds first.

Three brakes, all adjustable, all in the direction of not thrashing.

Cluster-wide riskenable automatic rebalancing, conservatively
set -euo pipefail

pvesh set /cluster/options --crs \
'ha=dynamic,ha-auto-rebalance=1,ha-auto-rebalance-threshold=40,ha-auto-rebalance-margin=15,ha-auto-rebalance-hold-duration=6'

grep -E '^crs:' /etc/pve/datacenter.cfg

Those numbers are deliberately more conservative than the defaults - a wider imbalance tolerated, a larger improvement demanded, and twice as long for the condition to persist. That is the right starting posture for a first enablement: you want to see the mechanism act a few times before you let it act often.

Constraining the scheduler

Three levels of control, from narrowest to broadest.

Exclude one guest

Every HA resource has an auto-rebalance property, documented as “HA resource may be migrated during automatic rebalancing”, defaulting to 1.

Configuration changeopt a single guest out of automatic rebalancing
set -euo pipefail
VMID=101

ha-manager set "vm:$VMID" --auto-rebalance 0
ha-manager config --type vm

Reach for this on anything with a reason not to move: a latency-sensitive database whose replication would notice, a guest with device passthrough that is pinned to specific hardware, a licensing arrangement tied to a socket count.

Constrain where it may move things

Node-affinity rules bound the placement search for every scheduler mode. The scheduler optimises within the rules; it does not override them.

Configuration changekeep a tier on the nodes it belongs on
set -euo pipefail

ha-manager rules add node-affinity db-tier \
--resources 'vm:101,vm:102' \
--nodes 'pve-01:2,pve-02:1' \
--strict 1

ha-manager rules config

Negative resource-affinity is the other useful shape here: two guests that must never share a node - a database primary and its replica, two halves of a quorum - stay separated no matter what the load numbers say.

Turn rebalancing off, keep dynamic placement

Cluster-wide riskstop automatic migrations without reverting to basic scheduling
set -euo pipefail

pvesh set /cluster/options --crs 'ha=dynamic,ha-auto-rebalance=0'
grep -E '^crs:' /etc/pve/datacenter.cfg

This is the configuration most production clusters should sit at for their first months on 9.2: better recovery placement, no unattended migrations.

Observing a decision

Read-only / Safewatch the CRM decide
set -euo pipefail

# Which node is the CRM master right now.
ha-manager status | grep -i master

# Its reasoning, live.
journalctl -u pve-ha-crm -f --no-pager

# Migrations that the HA stack initiated, rather than a user.
pvesh get /cluster/tasks --output-format json-pretty \
| grep -B2 -A4 'qmigrate' | head -40
Read-only / Safewhat a rebalance looks like in the CRM log
# journalctl -u pve-ha-crm --since '30 min ago' --no-pager | tail -8
pve-ha-crm[2411]: node 'pve-01' load 0.81, 'pve-02' load 0.34, 'pve-03' load 0.39
pve-ha-crm[2411]: imbalance 47% exceeds threshold 40%, round 6 of hold 6
pve-ha-crm[2411]: crs: considering vm:104 pve-01 -> pve-02, predicted improvement 22%
pve-ha-crm[2411]: service 'vm:104': migrate to node 'pve-02'
pve-ha-crm[2411]: migrate service 'vm:104' to node 'pve-02' (running)
pve-ha-crm[2411]: service 'vm:104': state changed from 'started' to 'migrate'
pve-ha-crm[2411]: service 'vm:104': state changed from 'migrate' to 'started'

Illustrative output

Every input to the decision is in those lines: the observed loads, the imbalance against the threshold, the hold-duration counter, and the predicted improvement against the margin. When a migration surprises you, this is the record that explains it - and when the scheduler is not doing something you expected, it is where you find which brake stopped it.

Rolling it out without an incident

A staged sequence, because every step here is observable and the temptation is to do them all at once.

  1. Record the current setting and put it in the change ticket. grep crs: /etc/pve/datacenter.cfg, including the case where it is absent.
  2. Move to ha=static first if you were on basic. Placement now accounts for guest size, which is an improvement on counting, and nothing moves on its own.
  3. Move to ha=dynamic. Still nothing moves on its own. Watch recovery placement across a planned node reboot and confirm it lands sensibly.
  4. Opt out the guests that must not move with --auto-rebalance 0, and put node-affinity rules on the tiers that have placement constraints. Do this before the next step, not after.
  5. Enable ha-auto-rebalance=1 with a raised threshold and hold duration. Watch the CRM log for a week.
  6. Tighten towards the defaults only if the observed behaviour is boring.

The step people skip is 4, and it is the one that turns step 5 from a configuration change into an incident.

Knowledge check

Knowledge check · 5 questions

  1. Q1. What is the practical difference between the static and dynamic CRS scheduler modes?

  2. Q2. Which datacenter.cfg key stops a transient load spike - a nightly backup, say - from triggering a migration?

  3. Q3. A production database VM should not be moved by the automatic rebalancer, but must still be recovered by HA if its node fails. Which approaches achieve that? Select all that apply.

  4. Q4. On a cluster whose guests all sit on local storage, changing the CRS mode from basic to dynamic changes no placement outcome at all.

  5. Q5. Which service’s journal records the CRS placement decisions, including the observed loads and the predicted improvement?

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