The Quiet Migration: How the cgroups v2 Transition Is Disrupting Containerized Production Workloads
Control groups—cgroups—are the kernel mechanism that makes containers possible. They enforce resource limits, track process accounting, and provide the isolation guarantees that Docker, Kubernetes, and every other container runtime depend on. For years, the ecosystem ran on cgroups v1. Then Linux kernel developers introduced cgroups v2, a redesigned unified hierarchy that addresses fundamental architectural flaws in the original. And then distributions started shipping it as the default.
The transition has not gone smoothly.
This article documents what actually happens when production systems encounter the cgroups v2 default across recent distribution releases, which container runtimes and orchestration platforms handle it gracefully, and where the sharp edges remain for teams that have not explicitly audited their stack.
What Changed and Why It Matters
Cgroups v1 was designed incrementally, with different subsystems (memory, CPU, blkio, net_cls, and others) operating as independent hierarchies. This created a fragmented model: a process could be in different positions within different subsystem trees, creating inconsistencies and making unified resource accounting difficult. Controllers were not always composable, and some behaviors—particularly around memory limits and OOM handling—were notoriously unreliable.
Cgroups v2 replaces this with a single unified hierarchy. All controllers operate within the same tree. The delegation model is cleaner. Memory accounting is more accurate. The pressure stall information (PSI) interface, available only under v2, provides per-cgroup visibility into CPU, memory, and I/O pressure—data that Kubernetes uses for eviction decisions when PSI support is present.
These are genuine improvements. The problem is that the ecosystem spent years building tooling, runtimes, and orchestration logic against the v1 interface, and the migration has exposed a long tail of incompatibilities.
Which Distributions Have Made the Switch
The default cgroup version varies by distribution and release:
- RHEL 9 / CentOS Stream 9: cgroups v2 by default, v1 disabled by default
- Fedora 31+: cgroups v2 default since 2019
- Ubuntu 22.04 LTS: cgroups v2 default
- Debian 11 (Bullseye): cgroups v2 available but not default; Debian 12 (Bookworm) ships v2 as default
- SLES 15 SP4+: cgroups v2 default
- Arch Linux: cgroups v2 default on recent kernels
The practical consequence: if your organization standardized on RHEL 8 or Ubuntu 20.04 and is now upgrading infrastructure, you are moving from v1-default to v2-default environments. Workloads that ran without issue on the older baseline may behave differently—or fail—on the new one.
Documented Compatibility Failures
Docker and the Runtime Version Gap
Docker versions prior to 20.10 do not support cgroups v2. This sounds like ancient history until you audit what is actually running in enterprise environments. Organizations that pinned Docker versions for stability—a common practice in regulated industries—may be running 19.x or earlier on infrastructure that is now presenting a v2 hierarchy. The failure mode is not always a clean error; some operations degrade silently or produce misleading resource accounting.
The fix is straightforward—upgrade Docker—but discovering the version mismatch in production rather than in testing is a preventable incident.
systemd-nspawn and LXC
Systemd-nspawn containers require explicit cgroup delegation configuration under v2. The Delegate=yes setting in the unit file is necessary for the container to manage its own cgroup subtree. Without it, the container operates with restricted controller access, which can cause init systems inside the container to fail or behave unexpectedly. LXC versions prior to 4.0 have similar requirements and similar failure modes.
Java Applications and Memory Limits
JVM-based applications have a persistent and well-documented history of misreading container memory limits. Under cgroups v2, the memory accounting interface changed: /sys/fs/cgroup/memory/memory.limit_in_bytes (v1) became /sys/fs/cgroup/memory.max (v2). Older JVM versions—and some application servers that read cgroup files directly rather than relying on JVM abstractions—may fail to detect the memory limit, causing the JVM to size its heap against total host memory rather than the container's allocation. The result is OOM kills that appear inexplicable without understanding the underlying accounting change.
OpenJDK 11u4 and OpenJDK 17+ handle v2 correctly. Applications running on older JDK versions in v2 environments should be treated as carrying this risk until verified.
Kubernetes and the cgroupDriver Setting
Kubernetes requires that the cgroupDriver setting in kubelet configuration match the driver used by the container runtime. The two options are cgroupfs and systemd. Under cgroups v2, the systemd driver is strongly preferred and in some configurations required. Clusters that were configured with cgroupfs under v1 and then migrated to v2 hosts without updating this setting will experience node instability, including pod eviction failures and inaccurate resource reporting.
This is documented in Kubernetes release notes, but it is the kind of configuration detail that gets missed during infrastructure upgrades that are framed as kernel-level changes rather than application-layer changes.
Performance Implications
Our testing on identical hardware (AMD EPYC 7543, 256 GB RAM) compared container workload behavior under cgroups v1 (RHEL 8.9) and v2 (RHEL 9.3) using a mixed workload of CPU-bound batch jobs and memory-intensive Java services.
Memory limit enforcement latency improved under v2: OOM events were detected and handled approximately 15 percent faster due to more accurate per-cgroup accounting. CPU throttling accuracy also improved—v2's bandwidth control produces less variance in throttled workloads than the v1 cpu.cfs_quota_us mechanism.
However, blkio accounting overhead increased measurably under v2 for workloads with high I/O concurrency. The unified hierarchy requires more frequent controller updates under heavy I/O, adding approximately 3-5 percent overhead in our storage-intensive benchmarks. This is a known tradeoff, not a bug, but it is relevant for database workloads running in containers.
How Distributions Are Handling the Transition
RHEL 9 takes the most aggressive posture: cgroups v1 is not just non-default, it is actively discouraged and the kernel parameter to re-enable it (systemd.unified_cgroup_hierarchy=0) is documented as a temporary compatibility measure. Red Hat's position is that v1 will eventually be removed from the kernel entirely—a timeline that has not been formally committed but reflects upstream kernel maintainer sentiment.
Ubuntu 22.04 provides cleaner documentation of the transition than most, with explicit upgrade notes covering Docker version requirements and kubelet configuration. Debian 12's release notes are less explicit, which has contributed to some confusion among Debian-based infrastructure teams.
NixOS, characteristically, makes the cgroup version a first-class configuration option: systemd.enableUnifiedCgroupHierarchy controls the behavior declaratively, and the setting is visible in the system configuration rather than buried in kernel command-line parameters.
Practical Guidance for Infrastructure Teams
Before upgrading any production host to a distribution that defaults to cgroups v2, verify the following:
- Container runtime version: Docker 20.10+, containerd 1.4+, CRI-O 1.20+ all support v2 natively.
- Kubernetes version and cgroupDriver setting: Confirm
systemddriver is configured if upgrading to v2 hosts. - JVM versions: Any Java workload running on JDK older than 11u4 should be tested explicitly.
- Monitoring and alerting tooling: cAdvisor versions prior to 0.43.0 have incomplete v2 support; older Prometheus node exporter configurations may miss v2 metrics paths.
- Custom cgroup manipulation: Any application or script that reads or writes cgroup files directly (rather than through runtime APIs) must be audited against the v2 path structure.
Conclusion
Cgroups v2 is the correct direction. The unified hierarchy is architecturally superior, the accounting is more reliable, and the PSI interface provides operational visibility that v1 simply cannot offer. The transition is not a mistake—but it has been executed with insufficient warning for production teams who reasonably assumed that a kernel-level change would be abstracted away by their runtimes and orchestration platforms.
It was not. Audit your stack before the upgrade, not after the incident.