An Introduction to cgroups
Learn how cgroups v2 work, how systemd maps services onto the cgroup tree, and how to set CPU, memory, and I/O limits for processes and services.
Before you start
- ▸Root or sudo access on the target machine
- ▸Linux kernel 5.2 or later (for stable cgroups v2 support)
- ▸systemd 245 or later (ships with Ubuntu 20.04+, Fedora 31+, Debian 11+)
Control groups (cgroups) are a Linux kernel feature that partition processes into hierarchical groups and enforce resource constraints on them. Every modern Linux distribution running systemd already relies on cgroups v2 for service isolation, CPU scheduling, memory limits, and I/O throttling — even if you have never touched them directly. Understanding cgroups lets you tune system behaviour beyond what high-level tools expose, diagnose runaway processes, and build your own resource-controlled execution environments.
cgroups v1 vs v2: What Changed and Why It Matters
cgroups v1 (circa 2008) allowed individual controllers — memory, cpu, blkio, etc. — to each maintain their own independent hierarchy. This created coordination nightmares: a process could appear in different positions across different controller trees. cgroups v2 (merged in kernel 4.5, stable by 5.2) enforces a single unified hierarchy. All controllers attach to the same tree, making delegation and accounting consistent.
Check whether your kernel has cgroups v2 mounted:
mount | grep cgroup
You should see a single cgroup2 line at /sys/fs/cgroup. If you see cgroup (v1) entries alongside it, your system is running in hybrid mode — common on older Ubuntu LTS kernels. Fedora and Arch have shipped pure v2 for several years. Ubuntu 22.04+ defaults to pure v2.
Force pure v2 on systems still in hybrid mode by adding systemd.unified_cgroup_hierarchy=1 to your kernel command line in /etc/default/grub, then running update-grub and rebooting.
The cgroup v2 Filesystem Layout
The cgroup hierarchy is exposed as a virtual filesystem rooted at /sys/fs/cgroup. Each directory is a cgroup; files inside it are the interface to the kernel.
ls /sys/fs/cgroup
Key files you will find in any cgroup directory:
- cgroup.controllers — controllers available to this cgroup
- cgroup.subtree_control — controllers delegated to child cgroups
- cgroup.procs — PIDs of processes directly in this cgroup
- memory.current — current memory usage in bytes
- cpu.stat — CPU accounting statistics
- io.stat — block I/O accounting per device
How systemd Uses cgroups v2
systemd maps its unit hierarchy directly onto the cgroup tree. Each service, slice, and scope gets its own cgroup directory. The layout is:
systemd-cgls
Typical output (abbreviated):
Control group /:
└─system.slice
├─nginx.service
│ └─1234 nginx: master process
└─postgresql.service
└─5678 postgres: checkpointer
Three built-in slices matter most:
- system.slice — system daemons managed by systemd
- user.slice — per-user login sessions
- machine.slice — containers and VMs via systemd-machined
You can inspect a service's exact cgroup path with:
systemctl status nginx.service | grep CGroup
Setting Resource Limits Through systemd
The recommended way to set cgroup limits for system services is through systemd unit directives — either in the unit file itself or via a drop-in override. This survives reboots and service restarts.
Creating a drop-in override
sudo systemctl edit nginx.service
This opens an editor for a drop-in file at /etc/systemd/system/nginx.service.d/override.conf. Add your resource directives in the [Service] section:
[Service]
# Limit to 2 CPUs worth of time (200000 out of 1000000 period)
CPUQuota=200%
# Hard memory limit — process is OOM-killed if exceeded
MemoryMax=512M
# Soft memory limit — kernel reclaims from here first
MemoryHigh=400M
# I/O weight (default 100, range 1-10000)
IOWeight=50
Reload and restart to apply:
sudo systemctl daemon-reload
sudo systemctl restart nginx.service
Key systemd resource directives (cgroup v2)
| Directive | cgroup v2 file | Effect |
|---|---|---|
| CPUQuota= | cpu.max | Cap CPU time as percentage of one core |
| CPUWeight= | cpu.weight | Relative CPU share (1–10000, default 100) |
| MemoryMax= | memory.max | Hard limit; OOM kill on breach |
| MemoryHigh= | memory.high | Soft limit; throttle and reclaim |
| MemorySwapMax= | memory.swap.max | Cap swap usage |
| IOWeight= | io.weight | Relative block I/O share |
| IOReadBandwidthMax= | io.max | Hard read throughput cap per device |
| TasksMax= | pids.max | Cap number of tasks (threads + processes) |
Writing Directly to the cgroup Filesystem
For experimentation or containerisation tooling, you can write directly to /sys/fs/cgroup without systemd. This is transient — it does not survive reboots.
Create a cgroup and apply limits
# Create a new cgroup (as root)
mkdir /sys/fs/cgroup/demo
# Enable memory and CPU controllers for its children
echo "+memory +cpu" > /sys/fs/cgroup/demo/cgroup.subtree_control
# Set a 256 MB hard memory limit
echo 268435456 > /sys/fs/cgroup/demo/memory.max
# Set CPU quota: 100000 / 1000000 = 10% of one core
echo "100000 1000000" > /sys/fs/cgroup/demo/cpu.max
# Move the current shell into this cgroup
echo $$ > /sys/fs/cgroup/demo/cgroup.procs
Any process spawned from that shell inherits the cgroup. To verify:
cat /proc/$$/cgroup
Delegate a cgroup to a non-root user
Delegation lets unprivileged processes manage their own sub-cgroups. systemd handles this automatically for user sessions, but you can do it manually:
# Allow user 'alice' to manage the demo cgroup
chown alice /sys/fs/cgroup/demo
chown alice /sys/fs/cgroup/demo/cgroup.procs
chown alice /sys/fs/cgroup/demo/cgroup.subtree_control
Verifying and Monitoring Resource Usage
systemd surfaces per-service resource usage through systemctl status and systemd-cgtop:
systemd-cgtop
This shows a live view sorted by CPU, memory, or I/O. For a specific service, read the raw kernel files:
# Memory usage for nginx
cat /sys/fs/cgroup/system.slice/nginx.service/memory.current
# CPU statistics
cat /sys/fs/cgroup/system.slice/nginx.service/cpu.stat
cpu.stat reports usage_usec (total CPU microseconds consumed), user_usec, and system_usec. Divide by elapsed real time to get utilisation.
Check whether any limits were ever hit (PSI — Pressure Stall Information, available kernel 4.20+):
cat /sys/fs/cgroup/system.slice/nginx.service/memory.pressure
Non-zero values in some or full fields indicate the service has stalled waiting for memory.
Troubleshooting
Limits not applying
- Confirm the controller is listed in
cgroup.controllersof the target cgroup. If not, the parent must delegate it viacgroup.subtree_control. - Run
systemctl daemon-reloadafter editing unit files. Missing this is the most common mistake. - On hybrid-mode systems, some controllers may only be active on the v1 hierarchy. Switch to pure v2 mode.
Service immediately OOM-killed after applying MemoryMax
Set MemoryHigh as a softer intermediate limit first. Watch memory.current under realistic load before tightening MemoryMax. Also check whether the service's baseline RSS (visible in systemctl status) already exceeds your limit.
CPUQuota has no effect
Verify the cpu controller is enabled: cat /sys/fs/cgroup/cgroup.controllers should include cpu. On some minimal kernels it may be compiled out — check with grep CONFIG_CFS_BANDWIDTH /boot/config-$(uname -r).
Cannot write to /sys/fs/cgroup as a regular user
Only the root-level cgroup directory is owned by root. For unprivileged cgroup management, use systemd's user session slice (systemctl --user) or ensure proper delegation as described above. The systemd-run --user --scope command is a convenient way to launch a process in a delegated user cgroup.
Frequently asked questions
- Do I need to configure cgroups manually if I just use systemd services?
- No. systemd automatically places every service in its own cgroup. You only need to write manually to /sys/fs/cgroup when building custom tooling, running experiments, or working outside of systemd's unit model.
- What is the difference between MemoryHigh and MemoryMax?
- MemoryHigh is a soft limit: when crossed, the kernel begins aggressively reclaiming pages from the cgroup and throttles allocation, but the process is not killed. MemoryMax is a hard ceiling — exceeding it triggers the OOM killer for processes in that cgroup.
- Can unprivileged users create and manage their own cgroups?
- Yes, through delegation. systemd automatically delegates a sub-tree under user.slice to each logged-in user, allowing 'systemctl --user' to manage units with cgroup limits without root. Manual delegation requires a root process to chown the cgroup directory to the target user.
- Will my cgroup limits survive a reboot?
- Limits written directly to /sys/fs/cgroup are lost on reboot because the filesystem is virtual. Limits set via systemd unit files or drop-ins (in /etc/systemd/system) persist across reboots.
- How do I check if a process was killed because it exceeded a cgroup memory limit?
- Look in 'journalctl -u <service>' for 'oom-kill' or 'Memory cgroup out of memory' messages. You can also read memory.events inside the service's cgroup directory — the 'oom_kill' counter increments each time a process in that cgroup is OOM-killed.
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.