Linux cgroups v2 Explained

Khimananda Oli 9 min read Virtualization
Linux cgroups v2 Explained

By Khimananda Oli | Last reviewed: August 2026

Resource isolation failures remain a primary cause of production outages, yet many teams still treat kernel-level controls as black boxes. Understanding how Linux cgroups v2 explained through the lens of modern orchestration is no longer optional for reliable infrastructure; it is the foundation of every container runtime and systemd unit in 2026. This guide moves past theory to show you exactly how the unified hierarchy enforces limits, prevents noisy neighbors, and integrates with the tools you already use.

How does Linux cgroups v2 architecture differ from v1?

The most critical distinction in Linux cgroups v2 explained is the shift from multiple independent hierarchies to a single, unified tree. In cgroups v1, each controller (cpu, memory, blkio) maintained its own separate hierarchy. A process could belong to one group for CPU and a completely different, unrelated group for memory. This flexibility was actually a design flaw: it created race conditions during process migration, made accounting inconsistent, and allowed configurations where resource limits contradicted each other. If you have ever debugged a container that was OOM-killed despite having "plenty" of memory because the memory limit was applied in a different hierarchy than the CPU set, you have felt this pain.

Cgroups v2 solves this by enforcing a single hierarchy rooted at /sys/fs/cgroup/. All controllers attach to this same tree structure. When you move a process into a specific cgroup directory, that movement applies atomically across all enabled controllers. There is no ambiguity about which limits apply. This unified model also introduces delegation safety, allowing non-root users (like container runtimes or systemd user sessions) to manage sub-trees without risking interference with parent controls.

cgroups v1 (Legacy)CPU HierarchyMemory HierarchyIO HierarchyGroup AGroup BGroup C⚠ Process P1 in Group A (CPU)but Group B (Memory)Inconsistent accounting & race conditionscgroups v2 (Unified)Single Unified Tree/system.slice/app.service✓ All controllers bound to same path✓ Atomic process migrationConsistent limits & safe delegation
cgroups v1 used separate hierarchies per controller causing inconsistency; cgroups v2 unifies all controllers under a single tree for atomic, predictable resource control.

In practice, this means your monitoring and enforcement are finally aligned. When you read memory.current and cpu.stat from the same directory, you are guaranteed to be looking at the same set of processes. For teams managing compliance frameworks like SOC 2 or ISO 27001, this deterministic behavior is essential for proving that resource isolation controls are functioning as documented. You can confidently map a specific service's cgroup path to its audit evidence without cross-referencing disparate mount points.

How do you configure resource limits with cgroups v2?

Configuration in cgroups v2 is file-based and intuitive, but the file names and semantics have changed from v1. The unified hierarchy exposes controller-specific files only when that controller is enabled on the cgroup. Before setting limits, verify availability by checking cgroup.controllers in the target directory.

Setting Memory Limits

Memory management is where v2 shines brightest. The key files are memory.max (hard limit) and memory.high (throttling threshold). Unlike v1's memory.limit_in_bytes, v2 distinguishes between throttling and killing. Setting memory.high triggers reclaim pressure and slows the workload down before it hits the hard ceiling, providing a graceful degradation path rather than an abrupt OOM kill.

# Create a new cgroup for a batch processing job
sudo mkdir /sys/fs/cgroup/batch-job

# Enable memory controller (if not already delegated)
echo "+memory" | sudo tee /sys/fs/cgroup/batch-job/cgroup.subtree_control

# Set soft throttle at 4GB, hard limit at 5GB
echo 4294967296 | sudo tee /sys/fs/cgroup/batch-job/memory.high
echo 5368709120 | sudo tee /sys/fs/cgroup/batch-job/memory.max

# Move current shell's PID into the cgroup
echo $$ | sudo tee /sys/fs/cgroup/batch-job/cgroup.procs

# Verify membership
cat /proc/self/cgroup

A common mistake is setting memory.max equal to memory.high. This eliminates the throttling buffer and makes the hard limit behave like v1's abrupt cutoff. Always leave headroom between high and max unless you specifically want immediate termination. For database workloads like those discussed in our PostgreSQL administration essentials, this throttling behavior prevents cache thrashing from turning into catastrophic failure during traffic spikes.

Configuring CPU Controls

CPU limiting in v2 uses cpu.max instead of the old cpu.cfs_quota_us and cpu.cfs_period_us pair. The format is $MAX $PERIOD in microseconds. To allocate 1.5 cores with a 100ms period:

# Allocate 1.5 CPUs (150000us per 100000us period)
echo "150000 100000" | sudo tee /sys/fs/cgroup/batch-job/cpu.max

# Check actual usage statistics
cat /sys/fs/cgroup/batch-job/cpu.stat

Note that cpu.weight replaces cpu.shares. The scale has changed from 2-262144 (with 1024 default) to 1-10000 (with 100 default). This normalized scale makes proportional sharing far more intuitive when configuring multi-tenant systems.

How does systemd integrate with cgroups v2 for service management?

You rarely interact with raw cgroup files in production because systemd acts as the cgroup v2 manager. Every unit file creates a corresponding cgroup slice automatically. Understanding this mapping is crucial for effective Ubuntu server monitoring and troubleshooting. When you set MemoryMax= or CPUQuota= in a unit file, systemd translates these directives into the appropriate cgroup v2 file writes.

Systemd organizes services into slices: system.slice for system services, user.slice for user sessions, and machine.slice for VMs/containers. This default hierarchy provides baseline isolation without manual intervention. You can inspect any service's live cgroup state directly:

# View the cgroup path for nginx
systemctl show nginx.service -p ControlGroup

# Inspect live memory usage via systemd
systemctl status nginx.service

# Or read the cgroup file directly
cat /sys/fs/cgroup/system.slice/nginx.service/memory.current

# Override limits temporarily without editing unit files
systemctl set-property nginx.service MemoryMax=2G CPUQuota=80%

For teams running Kubernetes, understanding this systemd layer is vital. Kubelet relies on systemd (or cgroupfs driver) to enforce pod limits. Misalignment between systemd slice configuration and kubelet cgroup driver settings causes some of the most perplexing node-level resource issues. If you are debugging CrashLoopBackOff errors that occur only under load, always verify whether the pod's cgroup limits match what the kernel actually enforces.

systemd Unit File[Service]MemoryMax=4GCPUQuota=200%IOWeight=800TasksMax=512systemdtranslatescgroups v2 Kernel/sys/fs/cgroup/system.slice/app.service/memory.max = 4294967296cpu.max = 200000 100000io.weight = 800pids.max = 512Observable EffectOOM kill at 4GB(not before)CPU throttled to 2 cores(even on 64-core host)Fork bomb contained(host stays responsive)
Systemd unit directives like MemoryMax and CPUQuota are translated into cgroups v2 kernel files, producing predictable resource enforcement observable at runtime.

What are the key differences between cgroups v1 and v2 for container runtimes?

Container runtimes (containerd, CRI-O, Podman) were the primary drivers behind cgroups v2 adoption. If you operate Kubernetes clusters or standalone containers in 2026, you are almost certainly using v2. The differences extend beyond hierarchy unification into functionality that directly impacts reliability and security.

Featurecgroups v1cgroups v2
HierarchyMultiple independent treesSingle unified tree
Memory ThrottlingNo (only hard limit + OOM)memory.high enables pressure-based reclaim
CPU Limit Syntaxcpu.cfs_quota_us + cpu.cfs_period_uscpu.max (single file, space-separated)
I/O Controlblkio (limited, no writeback support)io (full read/write bandwidth + IOPS)
Pressure MonitoringNot availablePSI (pressure.*) for CPU/memory/IO stall detection
Delegation SafetyUnsafe for unprivileged usersSafe subtree delegation via cgroup.subtree_control
Thread GranularityProcess-level onlyPer-thread control via cgroup.type

The Pressure Stall Information (PSI) interface deserves special attention. Files like memory.pressure expose some and full stall percentages over 10s, 60s, and 300s windows. This tells you not just whether a limit was hit, but how much time processes spent waiting due to resource scarcity. This metric is far more actionable than raw utilization for capacity planning and alerting. When integrating with observability stacks like those covered in our Prometheus metrics fundamentals guide, PSI metrics provide early warning signals that traditional saturation metrics miss entirely.

How do you migrate existing systems from cgroups v1 to v2 safely?

Migration requires planning because v1 and v2 cannot coexist with full functionality on the same system. Most modern distributions (Ubuntu 22.04+, RHEL 9+, Debian 12+) ship with v2 as default. If you are on an older system or need to verify your state, check the filesystem type:

# Check if cgroups v2 is mounted
mount | grep cgroup2

# Verify no v1 controllers are active
cat /proc/filesystems | grep cgroup

# Check systemd's cgroup driver
systemctl show --property=ControlGroups

If you must migrate manually, follow this sequence to avoid downtime:

  1. Audit current limits: Document all v1 configurations across hierarchies. Map them to equivalent v2 parameters using the comparison table above.
  2. Update systemd units: Replace deprecated directives (MemoryLimitMemoryMax, CPUSharesCPUWeight). Test in staging first.
  3. Enable v2 at boot: Add systemd.unified_cgroup_hierarchy=1 to kernel cmdline if not default. Remove any explicit v1 mount entries from fstab.
  4. Validate post-reboot: Confirm /sys/fs/cgroup is type cgroup2. Verify all services start correctly and limits apply.
  5. Update monitoring: Adjust metric collection paths. PSI metrics become available; old blkio metrics disappear.

A critical gotcha: Docker versions before 20.10 require the --cgroup-parent flag adjusted for v2 paths. Containerd and CRI-O handle this automatically in recent releases. Always test container lifecycle operations after migration, not just service startup. Some applications detect cgroup version at runtime and may need updates or configuration changes.

Start Migration AssessmentOS supports v2 natively?NoUpgrade OS FirstYesProceedAudit & map all v1 limitsUpdate systemd units & testEnable v2 & rebootValidate + Update MonitoringFailureRollback & Fix
Safe cgroups v1 to v2 migration requires OS validation, limit auditing, staged testing, and a defined rollback path before enabling unified hierarchy in production.

Applying Linux cgroups v2 explained principles to production reliability

Mastering Linux cgroups v2 explained concepts transforms how you approach resource governance from reactive firefighting to proactive engineering. The unified hierarchy eliminates an entire class of isolation bugs that plagued v1 deployments for years. By leveraging memory.high for graceful degradation, PSI metrics for predictive alerting, and systemd's native integration for declarative configuration, you build systems that fail predictably rather than catastrophically. Start by auditing your current cgroup version and mapping existing limits to v2 equivalents. Then implement PSI-based monitoring alongside your traditional metrics. These incremental steps compound into significantly higher reliability without requiring application changes. For teams needing hands-on implementation support or audit-ready resource governance validation, reach out to discuss your infrastructure.

Frequently Asked Questions

Cgroups v2 uses a unified hierarchy where each process belongs to exactly one control group per controller, eliminating the complex multiple hierarchies of v1. This simplifies management and prevents conflicting resource limits across different subsystems in modern Linux kernels.

Run mount and look for cgroup2 mounted at /sys/fs/cgroup. If you see cgroup instead of cgroup2, your system runs v1. Most 2026 distributions default to v2, but legacy containers may still force v1 mode.

No, Docker requires kernel 5.8 or newer for full cgroups v2 support. Older kernels only support v1. Upgrade your host kernel or use a distribution backporting cgroups v2 features before migrating container workloads to the unified hierarchy.

Write bytes to memory.max in the target cgroup directory under /sys/fs/cgroup. For example, echo 536870912 > memory.max sets a 512MB hard limit. Use memory.high for throttling without OOM kills in production environments.

Yes, systemd creates and manages cgroups v2 slices for services, scopes, and user sessions. Configure resource limits via unit file directives like MemoryMax= or CPUQuota= instead of manipulating /sys/fs/cgroup directly to avoid conflicts with systemd ownership.

Ensure cpu controller is enabled in cgroup.subtree_control. Write +cpu to that file before setting cpu.max. Also verify your workload generates enough load to trigger throttling, as idle processes won't show limit enforcement in monitoring tools.

Nested containers inherit parent cgroup limits through the unified hierarchy. Child cgroups cannot exceed parent allocations. Enable delegation by writing to cgroup.subtree_control and chowning the child cgroup directory so inner runtimes can create subgroups safely.

The io controller replaces blkio in v2, offering unified weight-based and absolute bandwidth limiting via io.weight and io.max files. It supports both read/write directions and provides better proportional sharing than the deprecated blkio interface used in v1.

Not directly. You must reboot into a v2-only kernel or use systemd.unified_cgroup_hierarchy=1 kernel parameter. Running processes stay in their original hierarchy until restart. Plan maintenance windows for migration since live switching between versions is unsupported.

Read metrics from files like memory.current, cpu.stat, and io.stat under each cgroup directory. Tools like cadvisor, bpftrace, or custom eBPF programs parse these interfaces efficiently. Avoid polling too frequently as excessive reads add kernel overhead.

Yes, memory.max triggers OOM killer within that cgroup scope first. Set memory.oom.group=1 to kill all tasks in the cgroup together rather than individual processes. This prevents partial failures in microservices where orphaned threads cause data corruption.

Write +pids to cgroup.subtree_control in the parent cgroup. Then set pids.max in child groups to limit fork bombs or runaway thread creation. Default systemd configurations often enable this globally, but custom hierarchies require explicit activation.

Yes, the unified hierarchy reduces attack surface by preventing controllers from being mounted separately. Delegation model allows safe unprivileged container management. Combined with namespaces and seccomp, v2 provides stronger isolation boundaries than v1's fragmented permission model.

Failure means a child cgroup already exists or another process holds references. Remove existing children first or check lsof /sys/fs/cgroup for open handles. Controllers cannot be enabled while descendants exist, enforcing strict hierarchical ordering rules.

No, v1 tools like cgcreate or libcgroup fail on v2 systems. Use systemd-run, cgexec replacements, or direct sysfs writes. Update automation scripts and CI pipelines to use v2 interfaces before upgrading hosts to avoid deployment breakage.