Control groups (cgroups) are the Linux kernel feature that organises processes into groups, and limits and accounts for their resources: CPU, memory, disk I/O, and number of processes. They’re the foundation of containers (Docker, Kubernetes), and of systemd’s service management. Modern distributions use cgroups v2, a single unified hierarchy.
Check your cgroup version
stat -fc %T /sys/fs/cgroup/ # cgroup2fs = v2, tmpfs = v1/hybrid
cat /proc/self/cgroup # which cgroup your shell is in
Ubuntu 22.04+, Debian 11+, RHEL 9 and AlmaLinux 9 use v2 by default.
How systemd organises cgroups
systemd puts every service, user session and container into its own cgroup:
systemd-cgls # tree of cgroups and processes
systemd-cgtop # live CPU, memory and I/O per cgroup (like top)
systemctl status nginx # shows the service's CGroup and its processes
Slices group units: system.slice (services), user.slice (logged-in users), and machine.slice (VMs and containers).
Limit a service’s resources (systemd)
Use a drop-in override, rather than editing the packaged unit:
sudo systemctl edit myapp.service
[Service]
MemoryMax=1G # hard limit: OOM-killed above this
MemoryHigh=800M # soft limit: throttled and reclaimed above this
CPUQuota=150% # at most 1.5 CPUs worth of time
CPUWeight=50 # relative share when CPUs are busy (default 100)
IOWeight=50 # relative disk I/O share
TasksMax=512 # max processes/threads
sudo systemctl daemon-reload && sudo systemctl restart myapp
systemctl show myapp -p MemoryMax -p CPUQuotaPerSecUSec
This is a great way to stop one misbehaving app (a memory leak, a runaway worker) from taking down a whole VPS. See systemd unit files.
One-off limits with systemd-run
Run a command in a temporary, limited cgroup:
sudo systemd-run --scope -p MemoryMax=2G -p CPUQuota=100% tar czf /backups/site.tgz /var/www
systemd-run --user --scope -p MemoryMax=500M ./script.sh
That’s handy for backups and batch jobs on busy servers. Compare nice and ionice.
Docker uses cgroups too
Container limits are cgroup settings:
docker run -d --memory=512m --cpus=1.5 --pids-limit=200 nginx
docker stats
See Docker CPU and memory limits.
Read the raw values
cgroup v2 exposes files under /sys/fs/cgroup/:
cat /sys/fs/cgroup/system.slice/nginx.service/memory.current
cat /sys/fs/cgroup/system.slice/nginx.service/memory.max
cat /sys/fs/cgroup/system.slice/nginx.service/cpu.stat
cat /sys/fs/cgroup/system.slice/nginx.service/memory.events # oom, oom_kill counts
Troubleshooting: OOM kills inside a cgroup
When a service exceeds MemoryMax, the kernel kills a process in that cgroup, not elsewhere on the system:
journalctl -k | grep -i "memory cgroup out of memory"
systemctl status myapp # "Main process exited, code=killed, status=9/KILL"
Raise the limit, fix the leak, or add MemoryHigh below MemoryMax, so the app slows down before it’s killed. See Linux OOM killer.
cgroups vs namespaces
- cgroups limit and measure how much a group of processes can use.
- namespaces control what they can see (processes, network, mounts).
Containers combine both. See Linux namespaces.
Practical defaults for a small VPS
On a server that runs several services, a few limits go a long way: cap memory-hungry background jobs (backups, image processing, indexing) with MemoryMax, give the web server and database a higher CPUWeight than batch work, and set TasksMax on apps that spawn workers, so a fork bomb or runaway pool can’t exhaust the process table. Leave headroom for the system itself: if all services together are allowed more memory than the VPS has, the limits won’t prevent the global OOM killer from stepping in.
Quick reference
| Task | Command / setting |
|---|---|
| cgroup version | stat -fc %T /sys/fs/cgroup/ |
| Tree / live usage | systemd-cgls / systemd-cgtop |
| Limit a service | systemctl edit X → MemoryMax=, CPUQuota= |
| One-off limit | systemd-run --scope -p MemoryMax=… cmd |
| Docker limits | --memory, --cpus |
| OOM events | memory.events, journalctl -k |
Related: Linux capabilities and systemctl commands.