Back to Devoured - October 07, 2026
DEVOURED
The Shift to cgroup v2 in Kubernetes: What You Need to Know

The Shift to cgroup v2 in Kubernetes: What You Need to Know

DevOps Kubernetes
Kubernetes v1.35 now mandates cgroup v2, forcing clusters on legacy cgroup v1 to migrate or face kubelet startup failures.
What: Kubernetes v1.35 defaults 'failCgroupV1' to true. Users must upgrade Linux nodes to kernel 5.8+ and configure the kubelet to use cgroup v2 before upgrading their control plane.
Why it matters: Moving to cgroup v2 is essential for modern resource management features like tiered memory protection (Memory QoS) and stable rootless container execution.
Takeaway: Check if your nodes are using v2 by running 'stat -fc %T /sys/fs/cgroup/'. If it returns 'tmpfs', you are likely still on cgroup v1.
Deep dive
  • Kubernetes v1.35 strictly enforces cgroup v2 usage by default.
  • Memory QoS, in-place vertical scaling, and improved OOM handling are locked behind cgroup v2.
  • Requires kernel 5.8 or later; 5.9+ recommended for memory-intensive workloads.
  • Container runtimes like containerd v2.0+ provide automatic driver discovery.
  • Monitoring tools that read cgroup files directly need updates due to changed hierarchy and file structures.
Decoder
  • cgroup (Control Group): A Linux kernel feature that organizes processes into hierarchical groups to limit, prioritize, and monitor system resource usage (CPU, memory, disk I/O).
  • Pressure Stall Information (PSI): A kernel feature that measures how much time tasks spend waiting for resources, helping identify contention issues.
Original article

The Shift to cgroup v2 in Kubernetes: What You Need to Know

In Linux, cgroups (control groups) are a kernel feature used for managing system resources. Kubernetes uses cgroups to allocate resources like CPU and memory to containers, ensuring that applications run smoothly without interfering with each other. With the release of Kubernetes v1.31, support for v1 cgroup management moved into maintenance mode. Support for v2 cgroup management has been stable since Kubernetes v1.25.

Compared with cgroup v1, cgroup v2 provides a single unified hierarchy, a more consistent interface, and a stronger foundation for resource isolation and modern resource-management features.

Deprecation of cgroup v1

Kubernetes has deprecated cgroup v1. Starting with Kubernetes v1.35, failCgroupV1 defaults to true, so the kubelet does not start on a cgroup v1 node by default. Administrators can temporarily set failCgroupV1: false in the kubelet configuration file, but removal will follow the Kubernetes deprecation policy. Further removal work is tracked in KEP-5573: Remove cgroup v1 support.

If you are still on a release older than v1.35, migrate every Linux node to cgroup v2 before upgrading, or plan to set the temporary failCgroupV1: false override. If you are already on v1.35 or later, confirm that every Linux node runs cgroup v2 (or that you intentionally keep the override). Under the default configuration, a remaining cgroup v1 node fails during kubelet startup.

For kubeadm-managed clusters, Kubernetes v1.35 also makes this an earlier, stricter check. The SystemVerification preflight check, provided by k8s.io/system-validators, returns an error during kubeadm init, kubeadm join, and kubeadm upgrade when it detects cgroup v1 with kubelet v1.35 or later; with an older kubelet, the check remains a warning.

The top FAQs cover three main areas: why to migrate, the benefits and drawbacks, and key points to keep in mind when using cgroup v2.

Limitations of cgroup v1 and Improvements with cgroup v2

The Linux kernel documentation describes both interfaces:

  • cgroup v1 documentation
  • cgroup v2 documentation

Let's enumerate some known issues.

active_file memory is not considered available memory

The kubelet treats active_file memory as not reclaimable. For I/O-intensive workloads, a large page cache can therefore make the kubelet report memory pressure and evict Pods. This is a known kubelet issue; migrating to cgroup v2 does not by itself change that calculation. The documented workaround is to set equal memory requests and limits for containers that perform intensive I/O, after measuring an appropriate value.

Memory QoS updates in Kubernetes v1.36

Memory QoS was introduced as an alpha feature in Kubernetes v1.22 and updated in v1.27. It remains alpha in v1.36, but now separates memory throttling from memory reservation and adds tiered memory protection:

Memory QoS is available only on Linux nodes that use cgroup v2. It relies on the cgroup v2 memory controller: memory.high provides throttling, while memory.min and memory.low provide hard and soft protection when tiered reservation is enabled. cgroup v1 cannot provide this protection model.

  • Enabling the MemoryQoS feature gate applies memory.high throttling to Burstable containers. The threshold is derived from the request, limit, and memoryThrottlingFactor (default 0.9).
  • memoryReservationPolicy: None is the default. It does not write memory.min or memory.low.
  • memoryReservationPolicy: TieredReservation maps Guaranteed Pod memory requests to memory.min (hard protection) and Burstable Pod requests to memory.low (soft protection). BestEffort Pods receive neither protection.
  • The kubelet exposes Alpha metrics for the total memory.min and memory.low reservations on a node.
  • Kernel 5.9 or later is recommended. On older kernels, memory.high reclaim can trigger a known livelock; from v1.36 the kubelet logs a warning when Memory QoS is enabled on an affected kernel.

For example, to opt in to tiered protection:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
  MemoryQoS: true
memoryReservationPolicy: TieredReservation
memoryThrottlingFactor: 0.9

The overall Kubernetes recommendation is not to enable Alpha features in production; however, if you judge that memory QoS with tiered reservations is useful for your platform in production, make sure to test the configuration and account for hard-reserved memory before you enable the TieredReservation feature gate.

Container-aware OOM handling

On cgroup v2 nodes, the kubelet defaults singleProcessOOMKill to false. It therefore sets memory.oom.group for each container cgroup so that an OOM event kills all processes in that container together, rather than leaving a partially functioning multi-process container. Set singleProcessOOMKill: true only if you need the cgroup v1-compatible behavior where the kernel may kill one process at a time.

This behavior is scoped to a container cgroup, not the whole Pod. Also, cgroup.kill is a separate administrative interface: writing 1 to it sends SIGKILL to every process in that cgroup and its descendants; it does not configure OOM behavior. The cgroup v2 memory controller additionally provides memory.events counters that monitoring systems and userspace OOM managers can observe.

Rootless support

In cgroup v1, delegating controllers to less privileged containers may be dangerous. Unlike cgroup v1, cgroup v2 officially supports delegation. Most implementations of rootless containers rely on systemd for delegating v2 controllers to non-root users.

This delegation mechanism is separate from Kubernetes Pod user namespaces, which map container users to unprivileged host users.

What else?

  1. eBPF stories:
    • In cgroup v1, device access controls are exposed through interface files.
    • The cgroup v2 device controller has no interface files, and is implemented on top of cgroup BPF.
    • Cilium attaches BPF cgroup programs for socket-based load balancing.
  2. Pressure Stall Information (PSI) reports CPU, memory, and I/O contention at node, Pod, and container level. On supported clusters, the kubelet exposes PSI by default. PSI requires cgroup v2, Linux 4.20 or later, CONFIG_PSI=y, and a kernel not booted with psi=0.
  3. When migrating, update software that reads the cgroup filesystem directly.

CPU weight conversion in newer OCI runtimes

cgroup v1 uses cpu.shares, whereas cgroup v2 uses cpu.weight. Newer OCI runtimes use an improved non-linear conversion that preserves the default priority and gives small CPU requests more usable granularity. The change is implemented in the OCI runtime rather than Kubernetes: it is available in crun v1.23 and runc v1.3.2. After upgrading a runtime, monitoring or policy tools that predict exact cpu.weight values may need updates.

In-place resource updates

In-place Pod vertical scaling graduated to stable in Kubernetes v1.35. The kubelet coordinates changes between the Pod-level and container cgroups so that increases create headroom before container limits grow, while decreases constrain containers before shrinking the Pod-level boundary. Accurate aggregate enforcement for this feature requires cgroup v2.

Adopting cgroup version 2

Requirements

  • You need at least one Linux node.
  • Your OS install must run with cgroup v2 enabled.
  • The kernel version must be 5.8 or later (5.9 or later is recommended when using memory QoS).
  • The container runtime must support cgroup v2.
  • The kubelet and the container runtime must both be configured to use the correct cgroup driver.

For now, you can opt back in to use cgroup v1; the Kubernetes project recommends using cgroup v2, but in Kubernetes 1.36 the cgroup v1 option remains supported as a fallback. That fallback is scheduled for removal in Kubernetes v1.38.

kernel updates around cgroup v2

  • In Linux 4.5, the cgroup v2 io, memory, and pids controllers were supported.
  • Linux 4.15 added support for the cgroup v2 cpu controller.
  • Pressure Stall Information (PSI) support began with Linux 4.20.
  • The Kubernetes project does not recommend using cgroup v2 with a Linux kernel older than 5.2 due to lack of cgroup-level task freezer support.
  • Kubernetes documents 5.8 as the minimum kernel version for cgroup v2; the root cgroup's system-level cpu.stat file was added in Linux 5.8.
  • The memory.high livelock fix used by Memory QoS is present in Linux 5.9 and later.
  • memory.peak was added in Linux 5.19.

cgroup driver configuration

If you use kubeadm to manage your cluster, Kubernetes recommends that you use the systemd cgroup driver. If you can pick either option, I recommend using the systemd driver.

Whatever tooling you've chosen, the kubelet automatically tries to detect the runtime's recommended cgroup driver. If you're using a container runtime that supports cgroup v2 but doesn't support automatic cgroup driver detection, you can manually configure an override by editing the kubelet configuration file.

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd

Tools and commands for troubleshooting

  • stat -fc %T /sys/fs/cgroup/: Check whether cgroup v2 is enabled.
  • systemctl list-units 'kube*' --type=slice or --type=scope: List Kubernetes-related units.
  • bpftool cgroup list /sys/fs/cgroup/*: List all programs attached to the cgroup CGROUP.
  • systemd-cgls /sys/fs/cgroup/*: Recursively show control group contents.
  • systemd-cgtop: Show top control groups by their resource usage.
  • tree -L 2 -d /sys/fs/cgroup/kubepods.slice: Show Pods' related cgroups directories.

How to check if a Pod CPU or memory limit is successfully applied to the cgroup file?

Work from the API object down to the node. Identify the node, then compare desired resources in the spec with the enacted values in status.

kubectl get pod <pod-name> -n <namespace> \
  -o jsonpath='{.spec.nodeName}{"\n"}'

kubectl get pod <pod-name> -n <namespace> \
  -o jsonpath='{range .spec.containers[*]}{.name}{" spec\t"}{.resources}{"\n"}{end}'

kubectl get pod <pod-name> -n <namespace> \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{" status\t"}{.resources}{"\n"}{end}'

On that node, as root, with the systemd cgroup driver and cgroup v2:

CONTAINER_ID=$(crictl ps \
  --label io.kubernetes.pod.namespace=<namespace> \
  --label io.kubernetes.pod.name=<pod-name> \
  --name <container-name> -q | head -n1)

# OCI view
crictl inspect "$CONTAINER_ID" | jq '.info.runtimeSpec.linux.resources'

# Kernel cgroup path for this container
PID=$(crictl inspect "$CONTAINER_ID" | jq -r '.info.pid')
CGROUP="/sys/fs/cgroup$(awk -F: '$1=="0"{print $3}' /proc/$PID/cgroup)"

cat "$CGROUP/cpu.weight"   # request.cpu → shares → weight
cat "$CGROUP/cpu.max"      # limit.cpu
cat "$CGROUP/memory.max"   # limit.memory

# Pod-level cgroup
cat "$(dirname "$CGROUP")/cpu.max" "$(dirname "$CGROUP")/memory.max"

# Present when MemoryQoS is enabled
cat "$CGROUP/memory.high" "$CGROUP/memory.min" "$CGROUP/memory.low"

SCOPE=$(basename "$CGROUP")
systemctl show "$SCOPE" \
  -p CPUWeight -p CPUQuotaPerSecUSec -p CPUQuotaPeriodUSec -p MemoryMax
Explore all 55 articles in today's digest

Fresh dev news, AI-digested daily. Never miss a trend at devoured.fyi