Use cgroup v2 for Resource Limits
Learn to use cgroup v2 with systemd slices, scopes, MemoryMax, CPUWeight, and IOWeight to enforce precise resource limits on Linux services and processes.
Before you start
- ▸Linux kernel 5.8 or later (ships with Ubuntu 22.04+, Fedora 33+, Debian 11+)
- ▸systemd 247 or later for full cgroup v2 delegation support
- ▸Root or sudo access to edit unit files and write to sysfs
- ▸Basic familiarity with systemd unit files and journalctl
Control groups version 2 (cgroup v2) is the unified hierarchy that replaced the fragmented cgroup v1 model. Since Linux kernel 5.2 and systemd 243, cgroup v2 is the default on major distributions. It gives you a single, coherent tree to govern memory, CPU, and I/O for any process—whether a system service, a user session, or an ad-hoc group of processes. This guide walks through the practical mechanics: verifying your setup, writing resource constraints directly in unit files, building custom slices, and applying limits to transient scopes.
Verify cgroup v2 Is Active
Before touching any limits, confirm the kernel is running the unified hierarchy.
mount | grep cgroup
You should see a single line like cgroup2 on /sys/fs/cgroup type cgroup2. If you also see cgroup (v1) mounts, the system is in hybrid or legacy mode. Modern Ubuntu 22.04+, Fedora 31+, Debian 11+, and Arch all default to the unified hierarchy.
cat /sys/fs/cgroup/cgroup.controllers
Output should list at least cpuset cpu io memory pids. If a controller is missing, it may not be delegated to the subtree you want to use.
Understand the systemd cgroup Tree
systemd maps its unit hierarchy directly onto the cgroup tree. The three key node types are:
- Slice – a grouping container; slices nest (e.g.,
user.sliceinside-.slice). You set aggregate limits here. - Service/Scope – leaves that hold actual processes. Services are managed units; scopes wrap externally-started processes.
- Transient unit – created on the fly with
systemd-run, ideal for testing limits without editing files.
Inspect the live tree at any time:
systemd-cgls
Key Resource Directives
MemoryMax and MemoryHigh
MemoryMax is a hard ceiling—the kernel OOM-kills processes in the cgroup if they exceed it. MemoryHigh is a soft ceiling—the kernel throttles allocation and reclaims aggressively but does not kill. Use both together: set MemoryHigh slightly below MemoryMax to get throttling before a hard kill.
CPUWeight
Replaces the old cpu.shares. Range is 1–10000; default is 100. A service with weight 400 gets four times the CPU time of one with weight 100 when the system is contended. It has no effect when CPU is idle.
IOWeight
Controls proportional I/O bandwidth via the BFQ or kyber scheduler. Same 1–10000 scale as CPUWeight. Requires that your block devices support BFQ (check with cat /sys/block/sda/queue/scheduler).
Apply Limits to an Existing Service
The cleanest method is a systemd drop-in file, which survives package updates.
sudo systemctl edit postgresql.service
This opens an editor for a drop-in at /etc/systemd/system/postgresql.service.d/override.conf. Add:
[Service]
MemoryHigh=1G
MemoryMax=1200M
CPUWeight=80
IOWeight=50
Reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart postgresql.service
Verify the live cgroup values:
systemctl status postgresql.service
cat /sys/fs/cgroup/system.slice/postgresql.service/memory.max
Create a Custom Slice
A slice lets you cap an entire category of services—say, all your web-tier daemons—without touching each unit individually.
sudo tee /etc/systemd/system/webtier.slice <<'EOF'
[Unit]
Description=Web Tier Slice
Before=slices.target
[Slice]
MemoryMax=4G
CPUWeight=200
IOWeight=200
EOF
sudo systemctl daemon-reload
sudo systemctl start webtier.slice
Now assign services to it by adding Slice=webtier.slice in their [Service] section (or a drop-in):
sudo systemctl edit nginx.service
[Service]
Slice=webtier.slice
The slice's MemoryMax=4G is the combined ceiling for every service inside it. Individual services can have tighter limits on top.
Use Transient Units for Testing
systemd-run creates a scope or service on the fly. This is invaluable for validating limits before committing them to unit files.
sudo systemd-run \
--scope \
--slice=webtier.slice \
-p MemoryMax=512M \
-p CPUWeight=50 \
-- stress-ng --vm 2 --vm-bytes 400M --timeout 30s
Watch the cgroup in real time from another terminal:
watch -n1 'cat /sys/fs/cgroup/webtier.slice/run-*.scope/memory.current'
If stress-ng tries to exceed 512 MB, the OOM killer fires inside the scope and leaves the rest of the system untouched.
Delegate cgroup Control to Unprivileged Users
By default only root can write to cgroup controllers. You can delegate a subtree to a user session—useful for containers or user-managed services.
sudo mkdir -p /etc/systemd/system/[email protected]
sudo tee /etc/systemd/system/[email protected]/delegate.conf <<'EOF'
[Service]
Delegate=cpu memory io pids
EOF
sudo systemctl daemon-reload
After this, a user's lingering systemd instance (systemctl --user) can set MemoryMax in user-scoped units without root. Enable lingering for the account first:
sudo loginctl enable-linger alice
Verify Limits Are Enforced
systemctl show postgresql.service \
-p MemoryMax -p MemoryHigh -p CPUWeight -p IOWeight
Expected output (values will match what you set):
MemoryMax=1258291200
MemoryHigh=1073741824
CPUWeight=80
IOWeight=50
Values are shown in bytes for memory. Cross-check with the raw cgroup interface:
cat /sys/fs/cgroup/system.slice/postgresql.service/cpu.weight
cat /sys/fs/cgroup/system.slice/postgresql.service/io.weight
Troubleshooting
IOWeight has no effect
The io controller requires BFQ or another weight-aware scheduler on the underlying block device. Check and set it:
cat /sys/block/sda/queue/scheduler
echo bfq | sudo tee /sys/block/sda/queue/scheduler
Make it permanent via a udev rule or a kernel parameter (elevator=bfq) in your bootloader config. NVMe devices sometimes need nvme_core.default_ps_max_latency_us=0 to expose BFQ properly.
Controller not available in subtree
If a controller shows in /sys/fs/cgroup/cgroup.controllers but not in a subdirectory's cgroup.controllers, the parent hasn't delegated it. Write the controller name to the parent's cgroup.subtree_control:
echo '+memory +cpu +io' | sudo tee /sys/fs/cgroup/cgroup.subtree_control
In practice, let systemd manage this automatically by reloading after adding Delegate= lines.
MemoryMax is ignored on swap-heavy systems
MemoryMax counts anonymous memory plus file-backed pages but, by default, does not include swap. Add MemorySwapMax=0 to prevent the cgroup from swapping out beyond the memory limit, which otherwise lets processes silently exceed the intent of your constraint.
[Service]
MemoryHigh=1G
MemoryMax=1200M
MemorySwapMax=0Frequently asked questions
- What is the difference between MemoryHigh and MemoryMax?
- MemoryHigh is a soft limit that triggers aggressive reclaim and throttling but does not kill processes. MemoryMax is a hard ceiling; exceeding it causes the kernel OOM killer to terminate processes in the cgroup.
- Does CPUWeight guarantee CPU time even when the system is idle?
- No. CPUWeight is proportional and only matters under CPU contention. When CPUs are idle, a low-weight cgroup can use as much CPU as it needs.
- Why does IOWeight seem to have no effect on my NVMe drive?
- IOWeight requires a weight-aware I/O scheduler such as BFQ. Many NVMe devices default to none or mq-deadline. Set the scheduler to bfq via sysfs or a udev rule and reload your services.
- Can I mix cgroup v1 and v2 on the same system?
- The kernel supports a hybrid mode, but mixing v1 and v2 controllers is complex and deprecated in practice. Stick with the pure v2 unified hierarchy; all modern distributions have completed the migration.
- Do container runtimes like Docker or Podman work with these cgroup v2 limits?
- Yes. Docker 20.10+ and Podman 3.0+ both support cgroup v2. If you run containers inside a slice, the slice's limits apply as an outer envelope, and per-container limits set by the runtime apply inside.
Related guides
AI and Artificial-Life Tools on Linux
Set up open-source AI/ML and artificial-life toolkits on Linux: PyTorch, JAX, DEAP, Avida, NetLogo, and RL environments with GPU driver guidance.
Assembly Language on Linux: A Starter Guide
Write x86-64 assembly on Linux from scratch: install NASM and GAS, learn syscalls, assemble and link a working program, then inspect and debug it.
How to Benchmark Disk Performance with fio
Learn to benchmark Linux disk performance with fio: writing job files, testing latency and throughput, and interpreting IOPS and percentile output correctly.
The Linux Boot Process Explained
Trace the full Linux boot sequence from UEFI firmware through GRUB2, the kernel, initramfs, and systemd to your login prompt — with diagnostics at each stage.