Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Linux cgroups v2 Explained

By Kokil Thapa | Last reviewed: September 2026

Linux cgroups v2 Explained starts with a simple idea: the kernel groups processes and applies resource limits to the group, not to each PID individually. On a production Ubuntu server running PHP-FPM, MySQL, Redis, and queue workers, one runaway job can starve everything else. Cgroups give you a kernel-level fence around CPU time, memory, disk I/O, and network bandwidth. If you maintain Linux servers for web applications, cgroup v2 is no longer optional background knowledge. Ubuntu 22.04 and 24.04 enable it by default, systemd owns the hierarchy, and containers depend on it. This guide walks through how v2 works, how it differs from v1, and the commands you actually use when a site slows down after deploy.

What is Linux cgroups v2 and how does it differ from v1?

Cgroups — control groups — are a Linux kernel feature for grouping tasks and applying resource policies. Version 1 shipped multiple independent hierarchies. CPU might live under /sys/fs/cgroup/cpu while memory lived under /sys/fs/cgroup/memory. The same process could appear in several trees at once. That design worked for years, but it created edge cases around delegation, accounting, and container runtimes.

Cgroup v2 replaces that with a single unified hierarchy mounted at /sys/fs/cgroup. Every process sits in exactly one cgroup. Controllers — cpu, memory, io, pids, and others — attach to nodes in that one tree. Limits compose cleanly because parent cgroups constrain children. A child cannot exceed what its ancestors allow.

cgroups v1 vs v2 Hierarchycgroups v1cpu treememory treepids treeSame PIDin 3 treescgroups v2Unified rootChild cgroupProcessOne PID, one cgroup
Linux cgroups v2 explained: one unified tree replaces v1’s separate per-controller hierarchies.

The practical differences matter on real servers. Cgroup v2 uses delegation through cgroup.subtree_control instead of ad-hoc v1 mount options. Memory accounting includes swap and kernel memory in clearer files. The io controller replaces the old blkio controller with a unified model. Pressure stall information — PSI — exposes CPU, memory, and I/O pressure at the cgroup level. That helps you spot contention before OOM kills arrive.

Featurecgroups v1cgroups v2
HierarchyMultiple independent treesSingle unified tree
Process membershipCan differ per controllerExactly one cgroup per process
DelegationComplex mount rulescgroup.subtree_control file
Default on Ubuntu 24.04Hybrid fallback possibleUnified v2 primary
systemd integrationMixed v1/v2 on older releasesNative v2 slices and scopes
Pressure metricsLimitedPSI files per resource

On older CentOS 7 or legacy VPS images, you may still see hybrid mode. Modern Rocky Linux and AlmaLinux servers follow the same v2-first path as Ubuntu. When you migrate from CentOS to Rocky Linux, expect cgroup layout changes in your monitoring scripts.

How do you enable and verify cgroup v2 on Ubuntu?

Most fresh Ubuntu 22.04 and 24.04 installs already mount cgroup v2. Verification takes thirty seconds. Run these commands on any server you manage:

mount | grep cgroup
stat -fc %T /sys/fs/cgroup/
cat /proc/filesystems | grep cgroup

If stat prints cgroup2fs, the unified hierarchy is active. You should see a single cgroup2 mount at /sys/fs/cgroup. A cgroup.controllers file at the root confirms available controllers:

cat /sys/fs/cgroup/cgroup.controllers
cat /sys/fs/cgroup/cgroup.subtree_control

Typical output lists cpuset cpu io memory pids and possibly hugetlb rdma misc. If your kernel still boots in hybrid mode, add the kernel parameter systemd.unified_cgroup_hierarchy=1 to GRUB and reboot during a maintenance window.

Check whether a specific service runs under v2

systemd exposes the cgroup path for every unit. This is the fastest way to connect abstract docs to a running PHP-FPM or MySQL service:

systemctl show php8.3-fpm.service -p ControlGroup
systemctl show mysql.service -p ControlGroup
cat /proc/self/cgroup

Under v2, /proc/self/cgroup shows a single line starting with 0:: followed by the cgroup path. Under legacy v1 you would see multiple lines with different controller prefixes.

cgroup v2 Resource ControllersProcessesin cgroupcpu.maxCPU quotamemorymemory.maxOOM limitdisk I/Oio.maxI/O throttlePSI filescpu.pressurememory.pressureio.pressure
Core cgroup v2 controllers: cpu.max, memory.max, and io.max enforce limits; PSI files report resource pressure.

How do you limit CPU and memory with cgroup v2?

Manual cgroup management teaches you what systemd automates. Create a test cgroup, enable controllers, move a process, and write limits. Never experiment on production paths without a rollback plan.

  1. Create a directory under the unified mount — the kernel creates a cgroup automatically.
  2. Enable controllers in cgroup.subtree_control on the parent.
  3. Write the target PID to cgroup.procs.
  4. Set limit files on the child cgroup.
sudo mkdir /sys/fs/cgroup/myapp
echo "+cpu +memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
echo $$ | sudo tee /sys/fs/cgroup/myapp/cgroup.procs
echo "50000 100000" | sudo tee /sys/fs/cgroup/myapp/cpu.max
echo "512M" | sudo tee /sys/fs/cgroup/myapp/memory.max

The cpu.max format is $QUOTA $PERIOD in microseconds. The example above allows 50 ms of CPU time per 100 ms wall-clock window — effectively half a CPU core. Use max as the quota for unlimited CPU within parent bounds.

Memory limits and OOM behaviour

memory.max sets a hard ceiling. When the cgroup exceeds it, the kernel triggers an OOM kill inside that group. The process dies, but sibling cgroups keep running. That isolation saved a shared EC2 host I maintain where a Laravel queue worker leaked memory after a bad deploy.

Related files worth knowing:

  • memory.high — soft throttle before hard limit
  • memory.swap.max — cap swap usage for the group
  • memory.current — live usage counter
  • memory.events — OOM and high-watermark event counts

Pair cgroup memory limits with swap and memory tuning at the host level. Cgroups fence workloads; they do not replace adequate RAM. When you diagnose high memory usage, check both memory.current and application-level metrics.

I/O limits with io.max

The v2 io controller uses io.max with a format like 8:0 rbps=1048576 wbps=524288 where 8:0 is the major:minor device number from lsblk. This protects database disks when a backup job and a web app share the same VPS — a common setup on budget Nepali hosting at Rs 1,500–3,000/month (~USD 11–22).

How does systemd use cgroup v2 on modern Linux?

On current Ubuntu and Rocky systems, systemd is the cgroup manager. You rarely write to /sys/fs/cgroup by hand. Instead you declare limits in unit files or drop-ins. systemd translates them into cgroup v2 files at service start.

Every unit lands in a slice. Slices form a tree:

  • -.slice — root of the system
  • system.slice — system services like nginx, mysql, php-fpm
  • user.slice — user sessions and user services
  • machine.slice — virtual machines and containers
systemd cgroup v2 Slice Tree-.slice (root)system.sliceuser.slicemysql.servicenginx.servicephp-fpm.serviceUnit drop-in limitsCPUQuota=50% MemoryMax=1G IOWeight=100
systemd maps services into cgroup v2 slices; unit directives become kernel limit files at start.

A typical drop-in for a memory-hungry queue worker on a Laravel app:

sudo systemctl edit laravel-worker.service
[Service]
CPUQuota=30%
MemoryMax=512M
MemoryHigh=450M
IOWeight=50

Reload and restart:

sudo systemctl daemon-reload
sudo systemctl restart laravel-worker.service
systemctl show laravel-worker.service -p CPUUsageNSec -p MemoryCurrent -p ControlGroup

This pattern appears across sister sites I deploy with GitLab CI and Deployer 7. One noisy worker must not take down PHP-FPM pools serving live traffic. Read the full systemd service management guide for timer units, socket activation, and dependency ordering alongside cgroup limits.

Scopes differ from services. A service is started by systemd; a scope wraps externally spawned processes. systemd-run creates transient scopes with limits — useful for ad-hoc artisan commands or database exports:

systemd-run --scope -p MemoryMax=256M -p CPUQuota=20% \
  php artisan queue:work --once

Learn how scopes interact with process management and signals when you stop or restart grouped tasks.

How do containers and Docker use cgroup v2?

Container runtimes create a cgroup per container under system.slice or a dedicated runtime path. Docker, containerd, and Podman all write the same v2 files — memory.max, cpu.max, pids.max — that you would write manually. The runtime just automates lifecycle.

Docker flags map directly to cgroup v2 knobs:

docker run -d --name api \
  --cpus="1.5" \
  --memory="768m" \
  --memory-swap="768m" \
  --pids-limit=100 \
  myapp:latest

Inspect the live cgroup path:

docker inspect api --format '{{.HostConfig.CgroupParent}}'
cat /sys/fs/cgroup/system.slice/docker-*.scope/memory.current

Kubernetes passes limits through the kubelet to the container runtime. On a single-node k3s VPS, misconfigured limits look like throttling, not application bugs. If you run KVM guests or Flatcar Container Linux, the same v2 rules apply inside each layer.

Production Stack cgroup IsolationUbuntu 24.04 Host — 4 GB RAMnginx128M maxphp-fpm1G maxmysql2G maxredis256M maxqueue workers512M max each — OOM stays localRunaway worker OOM — web stays up
Linux cgroups v2 explained for a typical Laravel stack: per-service limits prevent one layer from starving the rest.

Official kernel documentation for the unified hierarchy lives at docs.kernel.org cgroup v2 admin guide. systemd’s cgroup interface is documented in systemd.resource-control(5). Docker publishes cgroup driver requirements in the Docker resource constraints documentation.

How do you troubleshoot cgroup v2 problems on a live server?

Most cgroup issues show up as throttling, unexpected OOM kills, or services that refuse to start after a limit change. Work through this checklist before you reboot.

Confirm the unit landed in the expected cgroup

systemd-cgls
systemd-cgtop
cat /sys/fs/cgroup/system.slice/nginx.service/cgroup.procs

systemd-cgtop gives a live view similar to top, but grouped by cgroup. It pairs well with Netdata monitoring and alerts for long-term graphs.

Read pressure and event counters

cat /sys/fs/cgroup/system.slice/php8.3-fpm.service/memory.pressure
cat /sys/fs/cgroup/system.slice/php8.3-fpm.service/memory.events
cat /sys/fs/cgroup/system.slice/php8.3-fpm.service/cpu.stat

Rising some or full pressure in memory.psi means the cgroup waits for pages. The oom_kill counter in memory.events confirms the kernel killed a process inside the group. Check application logs immediately after.

Common mistakes

  • Setting MemoryMax too low for PHP-FPM — pools spawn multiple workers that share one cgroup
  • Forgetting daemon-reload after editing unit drop-ins
  • Mixing v1 tools like cgcreate on a pure v2 host
  • Expecting ulimit to enforce memory — ulimits and cgroups are separate layers; see sysctl and ulimits tuning
  • Running backup tar jobs without I/O or CPU caps during peak traffic hours

On shared infrastructure for projects like Adventure Third Pole Trek, cgroup limits are part of the deploy checklist alongside PHP-FPM pool sizing and MySQL buffer tuning. Treat them as performance optimization infrastructure, not emergency-only patches.

If you parse cgroup stats into JSON for dashboards, validate output with the JSON formatter tool before feeding it to Grafana or a custom admin panel. For cron-driven maintenance jobs that spike disk I/O, combine cron scheduling best practices with IOWeight limits on the service unit.

Interview prep? Expect cgroup v2 questions alongside namespaces and systemd in any Linux DevOps interview set. Know the unified hierarchy rule, three limit files, and how Docker maps flags.

Key Takeaways

  • Cgroup v2 uses one unified hierarchy — every process belongs to exactly one cgroup with all controllers attached at the same level.
  • Verify v2 with stat -fc %T /sys/fs/cgroup/; expect cgroup2fs on Ubuntu 22.04 and 24.04.
  • Set limits via systemd unit drop-ins (MemoryMax, CPUQuota, IOWeight) rather than hand-editing sysfs on production.
  • Read memory.pressure, memory.events, and cpu.stat before you blame application code for slowdowns.
  • Container runtimes and Docker use the same v2 files — --memory and --cpus map directly to kernel limits.
  • Combine cgroup fences with host-level tuning, monitoring, and sensible PHP-FPM worker counts for Laravel stacks.

People Also Ask

Is cgroup v2 enabled by default on Ubuntu 24.04?

Yes. Ubuntu 24.04 LTS boots with the unified cgroup v2 hierarchy as the primary layout. Run stat -fc %T /sys/fs/cgroup/ to confirm. Some older cloud images may still use hybrid mode until you set systemd.unified_cgroup_hierarchy=1 and reboot.

What happens when a cgroup hits its memory limit?

The kernel triggers an OOM kill inside that cgroup only. Processes in other cgroups keep running. Check memory.events for the oom_kill counter and inspect service logs for the victim process. Increase MemoryMax or fix the leak based on what you find.

Can I use cgroups v1 and v2 at the same time?

Hybrid mode mounts both, but new development targets v2 exclusively. Docker, Kubernetes, and systemd assume v2 on current releases. Avoid building new tooling against v1 paths — they will break on fresh installs.

How does cgroup v2 relate to Linux namespaces?

Namespaces isolate what a process sees — PID tables, mount points, network stacks. Cgroups limit what a process consumes — CPU, memory, I/O. Containers combine both. Namespaces provide the box; cgroups set the weight and size budget inside it.

Put cgroup v2 to work on your stack

Linux cgroups v2 Explained is not abstract kernel trivia. It is the mechanism that keeps your web stack fair under load. Start by verifying v2 on each server, add systemd drop-ins for your heaviest services, and wire PSI metrics into your monitoring. On the next OOM or CPU spike, you will know exactly which unit to inspect instead of rebooting and hoping.

Need help sizing limits for a Laravel, WordPress, or custom API deployment on Ubuntu? See the server support and maintenance service or browse the project portfolio for production examples. For a full health check — cgroups, PHP-FPM, MySQL, and deploy pipeline — contact us and describe your current host layout.

Frequently Asked Questions

Cgroups v2 is a Linux kernel feature that groups processes into one unified hierarchy and applies resource policies — CPU, memory, disk I/O, and pids — to the entire group rather than to each PID individually.

Cgroup v1 used multiple independent hierarchies, so a process could sit in different trees per controller. Cgroup v2 replaces that with a single tree mounted at /sys/fs/cgroup where every process belongs to exactly one cgroup and all controllers attach at the same level. Delegation moves to cgroup.subtree_control instead of complex v1 mount rules. Memory accounting is clearer, the io controller replaces blkio, and PSI files expose CPU, memory, and I/O pressure per cgroup. Ubuntu 22.04 and 24.04 use v2 by default; older CentOS 7 images may still run hybrid mode.

Yes. Ubuntu 24.04 LTS boots with the unified cgroup v2 hierarchy as the primary layout. Run stat -fc %T /sys/fs/cgroup/ to confirm cgroup2fs. Older cloud images may still use hybrid mode until you add systemd.unified_cgroup_hierarchy=1 to GRUB and reboot.

On a server you maintain, run mount | grep cgroup, stat -fc %T /sys/fs/cgroup/, and cat /proc/filesystems | grep cgroup. If stat prints cgroup2fs, the unified hierarchy is active with a single cgroup2 mount at /sys/fs/cgroup. Confirm available controllers with cat /sys/fs/cgroup/cgroup.controllers and cat /sys/fs/cgroup/cgroup.subtree_control — typical output lists cpuset, cpu, io, memory, and pids. To tie this to a running service, use systemctl show php8.3-fpm.service -p ControlGroup. Under v2, cat /proc/self/cgroup shows one line starting with 0:: followed by the cgroup path.

The kernel triggers an OOM kill inside that cgroup only. Processes in sibling cgroups keep running. Check memory.events for the oom_kill counter and inspect service logs immediately after to identify the victim process.

Create a directory under /sys/fs/cgroup, enable controllers on the parent via cgroup.subtree_control, move a PID into cgroup.procs, then write limits. For example, cpu.max uses quota and period in microseconds — 50000 100000 allows half a CPU core. Use max as the quota for unlimited CPU within parent bounds. memory.max sets a hard ceiling; exceeding it triggers OOM inside that group only. Related files include memory.high for soft throttling, memory.swap.max to cap swap, memory.current for live usage, and memory.events for OOM counts. Never experiment on production paths without a rollback plan.

On current Ubuntu and Rocky Linux systems, systemd is the cgroup manager. You declare limits in unit files or drop-ins, and systemd writes the corresponding v2 files at service start. Directives like CPUQuota, MemoryMax, MemoryHigh, and IOWeight map to kernel limit files. After editing a drop-in, run systemctl daemon-reload and restart the unit. Verify with systemctl show laravel-worker.service -p CPUUsageNSec -p MemoryCurrent -p ControlGroup. For ad-hoc tasks, systemd-run creates transient scopes with limits — useful for one-off artisan commands or database exports without permanently changing a unit file.

Every systemd unit lands in a slice that forms a tree under the root -.slice. system.slice holds system services like nginx, mysql, and php-fpm. user.slice holds user sessions and user services. machine.slice holds virtual machines and containers. Parent cgroups constrain children — a child cannot exceed what its ancestors allow. This is why a noisy Laravel queue worker in its own service unit cannot consume resources allocated to PHP-FPM pools serving live traffic, provided you set sensible MemoryMax and CPUQuota drop-ins on each unit.

Create a systemd drop-in with systemctl edit laravel-worker.service and add CPUQuota=30%, MemoryMax=512M, MemoryHigh=450M, and IOWeight=50 under the Service section. Run systemctl daemon-reload, then systemctl restart laravel-worker.service. Confirm the unit landed in the expected cgroup with systemctl show laravel-worker.service -p ControlGroup. On shared hosts I deploy with GitLab CI and Deployer 7, this pattern stops one leaky worker from taking down PHP-FPM after a bad deploy. Pair cgroup limits with sensible worker counts and host-level RAM — cgroups fence workloads but do not replace adequate memory.

Docker, containerd, and Podman create a cgroup per container and write the same v2 files you would set manually. Flags map directly: --cpus="1.5" sets cpu.max, --memory="768m" sets memory.max, --memory-swap="768m" caps swap, and --pids-limit=100 sets pids.max. Inspect the live path with docker inspect api --format '{{.HostConfig.CgroupParent}}' and read memory.current under the container scope. On a single-node k3s VPS, misconfigured Kubernetes limits passed through the kubelet look like throttling rather than application bugs — always check cgroup stats before blaming code.

PSI exposes CPU, memory, and I/O pressure at the cgroup level through files like memory.pressure under each service path. Rising some or full pressure in memory.psi means processes in that cgroup wait for memory pages rather than getting CPU time. This helps you spot contention before OOM kills arrive. Read memory.pressure alongside memory.events and cpu.stat when a site slows after deploy. I pair systemd-cgtop with Netdata alerts for long-term graphs, but PSI files give you immediate kernel-level signal about which unit is under resource stress.

The v2 io controller uses io.max with a format like 8:0 rbps=1048576 wbps=524288, where 8:0 is the major:minor device number from lsblk. This caps read and write bytes per second for processes in that cgroup. On budget VPS hosting at Rs 1,500–3,000 per month (~USD 11–22), a backup tar job and a web app often share the same disk — io.max protects database I/O when maintenance jobs run during peak hours. Combine io.max caps with IOWeight on systemd unit drop-ins for finer control between competing services on the same host.

Work through a checklist before rebooting. Confirm the unit landed in the expected cgroup with systemd-cgls, systemd-cgtop, and cat on the service cgroup.procs file. Read memory.pressure, memory.events, and cpu.stat for the affected unit — an rising oom_kill counter in memory.events confirms the kernel killed a process inside the group. Common mistakes include setting MemoryMax too low for PHP-FPM pools with multiple workers, forgetting daemon-reload after editing drop-ins, mixing v1 tools like cgcreate on a pure v2 host, and expecting ulimit to enforce memory when ulimits and cgroups are separate layers. Check application logs immediately after any OOM event.

Hybrid mode mounts both hierarchies, but new development targets v2 exclusively. Docker, Kubernetes, and systemd on current Ubuntu and Rocky Linux releases assume v2. Avoid building new tooling against v1 paths — they break on fresh installs. If your kernel boots in hybrid mode, add systemd.unified_cgroup_hierarchy=1 to GRUB and reboot during a maintenance window to move fully to the unified hierarchy.

Namespaces and cgroups solve different isolation problems and containers combine both. Namespaces isolate what a process sees — PID tables, mount points, network stacks. Cgroups limit what a process consumes — CPU time, memory, disk I/O, and network bandwidth. Namespaces provide the box; cgroups set the weight and size budget inside it. On a production Ubuntu server running PHP-FPM, MySQL, Redis, and queue workers, namespaces do not stop one runaway job from starving everything else — that is the cgroup layer's job. Expect cgroup v2 questions alongside namespaces and systemd in Linux DevOps interview prep.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: