$linuxjunkies
>

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.

AdvancedUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

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.slice inside -.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=0
tested on:Ubuntu 24.04Fedora 40Debian 12Arch rolling-2025-04

Frequently 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