Skip to main content
RunBook Academy

Proxmox VEX · LXC ContainersConfiguration

Container resource limits under cgroup v2

Intermediate⏱ ~24 minpct

What you'll learn

  • Distinguish cores, cpulimit and cpuunits and say which one throttles and which one only prioritises
  • Predict what happens when a container reaches its memory limit, and where the evidence appears
  • Explain why free and top inside a container report what they do
  • Set limits that fail visibly rather than mysteriously

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.

A VM with 4 GiB of RAM has 4 GiB of RAM. Its kernel manages that memory, runs out of it in the ordinary way, and its own OOM killer resolves the situation inside the guest where the guest’s logs record it.

A container with memory: 4096 has a cgroup limit of 4 GiB against the host’s memory. Everything about how it behaves at the boundary is different, and so is where you look afterwards.

PVE 9 runs pure cgroup v2 - the unified hierarchy, no v1 fallback - so this lesson describes one mechanism rather than two.

CPU: three knobs that are not the same knob

OptionRangeWhat it doesEnforced how
cores1 - NHow many CPUs the container seescpuset - which host CPUs its tasks may run on
cpulimit0 - 8192, default 0Ceiling on total CPU time consumedcgroup CPU bandwidth quota
cpuunits0 - 500000, default 100 under cgroup v2Relative share when CPUs are contendedcgroup CPU weight

The distinction that matters in practice:

  • cpulimit is a hard ceiling and applies always. cpulimit: 2 means the container gets at most two CPUs’ worth of time per second even if the host is completely idle.
  • cpuunits is a weight and applies only under contention. A container with cpuunits: 50 against one with cpuunits: 200 gets a quarter of the contested time - but if nothing else wants CPU, it can use the whole machine up to its cpulimit.
  • cores changes what the workload believes. Applications size their thread pools, worker counts and GC threads from the CPU count they observe. A container with cores: 2 and cpulimit: 8 will still start two workers.
Configuration changeset CPU limits on a running container
set -euo pipefail
CTID=200

pct set "$CTID" --cores 4 --cpulimit 2 --cpuunits 100

pct config "$CTID" | grep -E '^(cores|cpulimit|cpuunits):'

Memory: the limit is a wall, not a warning

memory sets the cgroup memory maximum in MiB - documented range 16 to N, default 512. swap sets an additional swap allowance, default 512 MiB.

Configuration changeset memory and swap
set -euo pipefail
CTID=200

pct set "$CTID" --memory 4096 --swap 512

# Find the container's cgroup on the host. The parent directory name has
# differed between LXC versions, so discover it rather than hard-coding it.
CG=$(find /sys/fs/cgroup -maxdepth 2 -type d -name "$CTID" \
     -not -path '*monitor*' | head -1)
printf 'cgroup: %s\n' "$CG"

# Read back what the cgroup actually enforces, in bytes.
cat "$CG/memory.max" "$CG/memory.swap.max" "$CG/memory.current"
Read-only / Safethe counter that settles the argument
# cat /sys/fs/cgroup/lxc/200/memory.events   # path discovered as above
low 0
high 0
max 4127
oom 3
oom_kill 3

Illustrative output

Read that as: the container hit its hard limit 4127 times, went into OOM three times, and three processes were killed. A max count in the thousands with oom_kill at zero is also worth knowing about - it means the container is constantly reclaiming at its ceiling, which is a performance problem disguised as a working system.

PID limits, and the fork bomb question

A container that can create unlimited processes can exhaust the host’s global PID space, which affects every other container and the host itself. The cgroup v2 pids controller bounds this.

PVE does not expose a pids.max option on pct set, so it is set through the raw LXC configuration:

Configuration changecap the number of processes a container may create
set -euo pipefail
CTID=200

CG=$(find /sys/fs/cgroup -maxdepth 2 -type d -name "$CTID" \
     -not -path '*monitor*' | head -1)

# Look at the steady state first - the limit must clear it comfortably.
cat "$CG/pids.current"

printf 'lxc.cgroup2.pids.max: 2048\n' >> "/etc/pve/lxc/$CTID.conf"
pct reboot "$CTID"

cat "$CG/pids.max"

What a container limit cannot do

Being explicit about the gaps is more useful than listing the knobs.

ResourceBounded per container?Notes
CPU timeYescpulimit (quota), cpuunits (weight)
MemoryYesmemory, swap - hard limits, OOM at the boundary
ProcessesYesvia raw lxc.cgroup2.pids.max
Block I/OPartiallycgroup v2 io controller; PVE has no pct option for it
Network bandwidthYes, per interfacerate= on the net[n] option
Kernel memory structuresNodentries, inodes, socket buffers are largely shared
Filesystem cacheNoPage cache is global; a container doing heavy I/O evicts others’ cache
Kernel timeNoA container issuing pathological syscalls costs host CPU in ways cpulimit accounts for poorly

That last block is the honest limit of the model. cgroups bound what a container consumes; they do not bound what a container costs. A container within every one of its limits can still degrade its neighbours through the page cache and the shared kernel, and no amount of limit-setting changes that. When a workload’s noisy-neighbour behaviour has to be bounded rather than merely discouraged, that is an argument for a VM.

Verification

Read-only / Safewhat the container is actually limited to, from both sides
set -euo pipefail
CTID=200
CG=$(find /sys/fs/cgroup -maxdepth 2 -type d -name "$CTID" \
     -not -path '*monitor*' | head -1)

echo '--- enforced by the host ---'
printf 'memory.max   %s\n' "$(cat "$CG/memory.max")"
printf 'memory.current %s\n' "$(cat "$CG/memory.current")"
printf 'cpu.max      %s\n' "$(cat "$CG/cpu.max")"
printf 'cpu.weight   %s\n' "$(cat "$CG/cpu.weight")"
printf 'pids.max     %s\n' "$(cat "$CG/pids.max")"
grep oom_kill "$CG/memory.events"

echo '--- believed by the container ---'
pct exec "$CTID" -- sh -c 'nproc; free -m | head -2'

The two halves should agree. nproc reporting the host’s CPU count while cpu.max shows a quota is the signature of an lxcfs problem, and it means every application inside that container has sized itself for a machine it does not have.

Knowledge check

Knowledge check · 5 questions

  1. Q1. A container is configured with cores: 4, cpuunits: 100 and no cpulimit. The host is otherwise idle. How much CPU time can the container consume?

  2. Q2. One process in a container was killed and the rest kept running. Where is the authoritative evidence?

  3. Q3. Which are accurate statements about container resource limits? Select all that apply.

  4. Q4. Setting cpuunits low is an effective way to stop a container consuming most of the host’s CPU.

  5. Q5. Which cgroup v2 file records how many times a container’s processes have been OOM-killed?

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