Technical

Proxmox HA Load Balancing: It Doesn’t Rebalance, and Here’s the Script to Do It

Proxmox HA Load Balancing: It Doesn’t Rebalance, and Here’s the Script to Do It

Proxmox HA load balancing is not dynamic. The cluster resource scheduler (CRS) places VMs when they start or fail over, but it never rebalances running workloads. If a node gets overloaded, nothing moves automatically.

Networking equipment with connected cables, showcasing modern technology infrastructure.

What the CRS Actually Does

The CRS picks a node for a VM when it starts or when HA recovers it. It uses a scoring system based on CPU, memory, and sometimes other factors. But once the VM is running, the CRS doesn’t touch it. No live migration, no balancing. It’s a placement algorithm, not a balancer.

This trips people up. You set up HA, see “load balancing” in the docs, and assume it’ll spread VMs out. Nope. If you add a new node, existing VMs stay put. If one node gets slammed, nothing moves.

Why It Doesn’t Rebalance

Rebalancing means live-migrating VMs, which is risky and disruptive. Proxmox’s design philosophy is conservative: don’t move things unless you have to. The CRS only runs when a VM needs a new home. That’s it.

So if you want dynamic load balancing, you have to script it yourself.

A complex network of cables in a data center with a monitor in the foreground.

A Script to Force Rebalancing

Here’s a basic approach. It checks each node’s CPU load and migrates the VM with the highest CPU usage off the most loaded node. You’d run this on a schedule, like every 15 minutes via cron.

#!/bin/bash
# Simple Proxmox HA load balancer
THRESHOLD=80  # CPU load percentage to trigger migration

# Get the node with highest CPU load
OVERLOADED_NODE=$(pvesh get /nodes --output-format json | jq -r '.[].node' | while read node; do
  LOAD=$(pvesh get /nodes/$node/status --output-format json | jq -r '.data.cpu')
  echo "$node $LOAD"
done | sort -k2 -n | tail -1 | awk '{print $1}')

LOAD=$(pvesh get /nodes/$OVERLOADED_NODE/status --output-format json | jq -r '.data.cpu')
if [ $(echo "$LOAD > $THRESHOLD" | bc) -eq 1 ]; then
  # Find the VM with highest CPU usage on that node
  VMID=$(pvesh get /nodes/$OVERLOADED_NODE/qemu --output-format json | jq -r '.[].vmid' | while read vmid; do
    CPU=$(pvesh get /nodes/$OVERLOADED_NODE/qemu/$vmid/status/current --output-format json | jq -r '.data.cpu')
    echo "$vmid $CPU"
  done | sort -k2 -n | tail -1 | awk '{print $1}')

  # Find the least loaded node (excluding the current one)
  TARGET_NODE=$(pvesh get /nodes --output-format json | jq -r '.[].node' | grep -v $OVERLOADED_NODE | while read node; do
    LOAD=$(pvesh get /nodes/$node/status --output-format json | jq -r '.data.cpu')
    echo "$node $LOAD"
  done | sort -k2 -n | head -1 | awk '{print $1}')

  # Migrate the VM
  qm migrate $VMID $TARGET_NODE --online
fi

This is rough. It doesn’t check memory, doesn’t handle HA groups, and could cause a migration storm if loads fluctuate. You’d want to add cooldowns and better scoring. But it’s a starting point.

Should You Even Bother?

For most homelabs, dynamic rebalancing is overkill. If you have a few VMs and a couple of nodes, manual migration when you notice a problem is fine. The script above adds complexity and risk. Live migration can fail, and if it does, your VM might be stuck.

If you really need automatic balancing, consider using HA groups with static assignments. You can pin VMs to preferred nodes, and if a node fails, they’ll move to the fallback. That’s not dynamic, but it’s predictable. And predictability is worth a lot.

For more on HA and failover, check out Falling Over to a ZFS Replica: The Manual Proxmox Failover That Actually Works.

Leave a comment

Comments are reviewed before they appear. Your email is never published.