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
Option
Range
What it does
Enforced how
cores
1 - N
How many CPUs the container sees
cpuset - which host CPUs its tasks may run on
cpulimit
0 - 8192, default 0
Ceiling on total CPU time consumed
cgroup CPU bandwidth quota
cpuunits
0 - 500000, default 100 under cgroup v2
Relative share when CPUs are contended
cgroup 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— Changes take effect immediately for cpulimit and cpuunits; a change to cores takes effect on restart for applications that read the count at startup.
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— Lowering a memory limit below current usage on a running container can trigger reclaim or an immediate OOM kill inside it. Raise freely; lower during a maintenance window.
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— Appends a raw LXC setting to the container config. Requires a restart. Set it well above the container's steady-state process count.
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.
Resource
Bounded per container?
Notes
CPU time
Yes
cpulimit (quota), cpuunits (weight)
Memory
Yes
memory, swap - hard limits, OOM at the boundary
Processes
Yes
via raw lxc.cgroup2.pids.max
Block I/O
Partially
cgroup v2 io controller; PVE has no pct option for it
Network bandwidth
Yes, per interface
rate= on the net[n] option
Kernel memory structures
No
dentries, inodes, socket buffers are largely shared
Filesystem cache
No
Page cache is global; a container doing heavy I/O evicts others’ cache
Kernel time
No
A 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— Read-only. Comparing the host's cgroup view against the container's own view is how you catch an lxcfs problem or a limit that did not apply.
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
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?
Q2. One process in a container was killed and the rest kept running. Where is the authoritative evidence?
Q3. Which are accurate statements about container resource limits? Select all that apply.
Q4. Setting cpuunits low is an effective way to stop a container consuming most of the host’s CPU.
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.