Kubernetes v1.35 refuses to start the kubelet on cgroup v1 nodes
Kubernetes v1.35 defaults failCgroupV1 to true, so the kubelet will not start on a cgroup v1 node unless an operator sets a temporary override. Nodes that make the move get GA swap, worth up to 3x pod density in published tests until active memory spills to disk.
Written for builders.

Bar chart of four values. Headless Chrome pods: 80 per node without swap, 160 per node with swap. Python sandboxes: 80 per node without swap, 240 per node with swap.
gVisor pods per node in the swap benchmarks, without vs with swap In pods per node
| Item | Value | Claim |
|---|---|---|
| Headless Chrome, no swap | 80 pods per node | 22 |
| Headless Chrome, with swap | 160 pods per node | 22 |
| Python sandboxes, no swap | 80 pods per node | 22 |
| Python sandboxes, with swap | 240 pods per node | 22 |
What happened
- 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.
- 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.
- For kubeadm-managed clusters, 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.
- Under the default configuration, a remaining cgroup v1 node fails during kubelet startup. Clusters on a release older than v1.35 should migrate every Linux node to cgroup v2 before upgrading, or plan to set the temporary failCgroupV1: false override.
Why it matters
Kubernetes now has two checks that stop a cgroup v1 node, and they run at different moments. On kubeadm-managed clusters the first is the SystemVerification preflight check, which comes from k8s.io/system-validators [4]. It returns an error during `kubeadm init`, `kubeadm join` and `kubeadm upgrade` when it detects cgroup v1 alongside a kubelet at v1.35 or later [4]. With an older kubelet the same check only warns [4]. The second check is in the kubelet's own startup, where `failCgroupV1` now defaults to true [1].
Clusters built without kubeadm hit only the second check [4]. The kubernetes.io FAQ is blunt about it: under the default configuration, a remaining cgroup v1 node fails during kubelet startup [5]. For anyone still below v1.35, it says to migrate every Linux node to cgroup v2 before upgrading, or plan to set the temporary override [5]. Chris Short, summarising the FAQ in his DevOps'ish newsletter, wrote: "Find those old nodes before your next upgrade does." [27]
The override is temporary
The override is one line in the kubelet configuration file [2]. With the config header from the FAQ's own examples [29], it looks like this:
```yaml apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration failCgroupV1: false ```
The FAQ calls the setting temporary. It says removal will follow the Kubernetes deprecation policy and points to KEP-5573 for the removal work [2]. The notice period has been long. cgroup v2 support has been stable since v1.25, and v1 support moved into maintenance mode in v1.31 [3]. v1.35 arrives ten minor releases after v2 went stable [30].
The FAQ describes the kubeadm preflight error and the kubelet override as separate checks [4][2]. It does not say whether `failCgroupV1: false` also clears the preflight error. We would prove that on a test cluster before an upgrade window depends on it.
Four floors, four components
Running the unified v2 hierarchy is the first floor. DevOps'ish's summary of the FAQ lists three more: kernel 5.8 or later; containerd 1.4 or later, or CRI-O 1.20 or later; and matching cgroup drivers on the kubelet and the container runtime [6]. Each floor belongs to a different component [6]. A node image that passes the kernel check still has to pass the runtime check. A current runtime still fails the list if its cgroup driver disagrees with the kubelet's [6].
There is a second, higher kernel line. For Memory QoS, the FAQ recommends kernel 5.9 or later, because on older kernels `memory.high` reclaim can trigger a known livelock [7]. From v1.36 the kubelet logs a warning when Memory QoS is enabled on an affected kernel [7]. A node on 5.8 clears the listed floor and still sits in the livelock range [34]. For fleets rebuilding node images anyway, we think 5.9 is the version to standardise on.
What the migration leaves alone
One known eviction problem survives the move. The kubelet treats `active_file` memory as not reclaimable, so an I/O-heavy workload with a large page cache can make the kubelet report memory pressure and evict Pods [8]. This is kubernetes/kubernetes#43916, and the FAQ says migrating to cgroup v2 does not by itself change that calculation [8]. The documented workaround is to measure first, then set equal memory requests and limits on the containers doing the heavy I/O [8].
v2 does add a memory protection model, one the FAQ says cgroup v1 cannot provide [9]. Memory QoS runs only on cgroup v2 nodes [9]. Enabling the `MemoryQoS` gate applies `memory.high` throttling to Burstable containers. The threshold comes from request, limit and `memoryThrottlingFactor`, which defaults to 0.9 [10]. The reservation policy defaults to `None`, which writes neither `memory.min` nor `memory.low` [11]. Setting `TieredReservation` maps Guaranteed Pod requests to `memory.min` as hard protection and Burstable requests to `memory.low` as soft protection. BestEffort Pods get neither [11]. The FAQ's opt-in example [29]:
```yaml apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration featureGates: MemoryQoS: true memoryReservationPolicy: TieredReservation memoryThrottlingFactor: 0.9 ```
The feature arrived as alpha in v1.22, was updated in v1.27, and is still alpha in v1.36 [9]. The project's general advice is to keep alpha features out of production. Teams that enable tiered reservation anyway are told to test it and account for hard-reserved memory first [12]. DevOps'ish also lists whole-container OOM kills, PSI metrics on by default and in-place Pod-level resizing among the v2 gains [13].
Swap needed separate accounting
Node swap reached General Availability in v1.34 [14]. A kubernetes.io post that benchmarks it gives two historical reasons swap was discouraged. Under cgroup v1, memory and swap shared one combined limit. Operators could not cap disk swap on its own, so a container's real memory use was hard to predict or isolate [15]. Kubernetes' swap support relies on cgroup v2's separate swap accounting [15]. The second objection was the latency of paging to spinning disks, which the authors say NVMe Local SSDs largely eliminate [16].
The v1.35 cutoff and the swap feature depend on the same kernel interface. A node that fails the first check could never have given swap an independent limit [15]. DevOps'ish describes the setup as a few lines of KubeletConfiguration with `LimitedSwap` and Burstable QoS on the pods [17].
The kernel build below 300 MB
The clearest test in the post is a Linux 6.1.1 kernel build [18]. Without swap, the smallest memory limit that avoided an OOM crash was 600 MB [18]. With swap routed to a Local SSD, the limit fell by half to 300 MB, and the build finished in 374 seconds against the baseline's 433 [19]. The gap is 59 seconds, about 14% faster on half the memory [31]. A constrained run beating its own baseline is a result we would want to reproduce before it goes into a capacity plan.
Then the limit went to 200 MB. The active working set moved into swap, I/O waits grew long, and execution time rose by over 40% [20]. If the reference is the 433-second baseline, the run took more than 606 seconds [32]. The step from 300 MB to 200 MB removed one third of the limit [33]. The post's authors drew the conclusion themselves: "This reinforces that swap serves as an insurance policy for burst memory, not a replacement for active RAM." [21] Short wrote it more plainly: "Swap is insurance for idle and bursty memory. It's not more RAM." [26]
The headline figures come from density. In gVisor sandboxes, headless Chrome went from 80 to 160 pods per node and Python sandboxes from 80 to 240 [22]. Those are gains of 2x and 3x [35]. The post reports density gains of up to 3x, "often with little or no latency cost" [23].
What would have to be true elsewhere
These are measurements of someone else's workloads on someone else's hardware. For them to carry over, a large share of resident memory has to be cold. Both tested workload types have that property. The post says earlier compiled objects sit inactive in memory while a CI job runs [24]. The agent sandboxes need a large footprint to start and run untrusted code, then sit idle waiting for the next prompt [25].
The swap device also has to be fast. The benchmarks ran on NVMe Local SSDs on GKE [17], and slow-disk latency was one of the two original reasons swap was discouraged [16]. The memory limit also has to stay above the active working set. The 200 MB kernel build shows what happens below it [20]. We'd expect a service whose resident memory is mostly hot to see little density gain and reach that slowdown almost at once.
The QoS class now drives more behaviour
Several of these features key off the same Pod fields. The swap setup DevOps'ish describes puts pods in Burstable QoS [17]. Memory QoS throttles Burstable containers at `memory.high` and, under tiered reservation, gives their requests only soft `memory.low` protection [10][11]. The page-cache workaround for #43916 edits the same request and limit fields [8]. For an I/O-heavy pod that also wants swap, we think the QoS class has to be chosen deliberately and tested against swap, throttling and eviction together. For sizing hard reservations, the kubelet exposes alpha metrics for a node's total `memory.min` and `memory.low` reservations [28].
What to watch
- KEP-5573 landing in a release that removes cgroup v1 support outright, taking the failCgroupV1: false override with it.
- Memory QoS and TieredReservation leaving alpha, which would turn the 5.9 kernel recommendation into a practical floor for fleets that want memory.min and memory.low protection.
- A fix for kubernetes/kubernetes#43916 changing how the kubelet counts active_file page cache, which would retire the equal-requests-and-limits workaround for I/O-heavy pods.
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
Starting with Kubernetes v1.35, failCgroupV1 defaults to true, so the kubelet does not start on a cgroup v1 node by default.
- [2]
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.
- [3]
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.
- [4]
For kubeadm-managed clusters, 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.
- [5]
Under the default configuration, a remaining cgroup v1 node fails during kubelet startup. Clusters on a release older than v1.35 should migrate every Linux node to cgroup v2 before upgrading, or plan to set the temporary failCgroupV1: false override.
- [6]
DevOps'ish's summary of the cgroup v2 FAQ lists requirements: kernel 5.8 or later, containerd 1.4+ or CRI-O 1.20+, and matching cgroup drivers on the kubelet and runtime.
- [7]
For Memory QoS, 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.
- [8]
The kubelet treats active_file memory as not reclaimable; for I/O-intensive workloads a large page cache can make the kubelet report memory pressure and evict Pods. This is known issue kubernetes/kubernetes#43916; 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.
- [9]
Memory QoS was introduced as alpha in Kubernetes v1.22, updated in v1.27, and remains alpha in v1.36. It is available only on Linux nodes that use cgroup v2; cgroup v1 cannot provide this protection model.
- [10]
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).
- [11]
memoryReservationPolicy: None is the default and 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.
- [12]
The overall Kubernetes recommendation is not to enable Alpha features in production; teams that judge tiered-reservation Memory QoS useful in production should test the configuration and account for hard-reserved memory before enabling it.
- [13]
DevOps'ish lists what cgroup v2 brings: Memory QoS with tiered memory.min/memory.low protection, whole-container OOM kills, PSI metrics on by default, and in-place Pod-level resizing.
- [14]
Kubernetes support for running nodes with swap enabled reached General Availability in v1.34.
- [15]
Under cgroup v1, the controls treated memory and swap as a single combined limit rather than letting operators set an independent limit for disk swap, which made a container's real memory usage unpredictable and hard to isolate. Kubernetes' swap support relies on cgroup v2, whose separate swap accounting tracks disk swap on its own.
- [16]
The second historical reason swap was discouraged was the latency penalty of paging to slow spinning disks, which fast NVMe Local SSDs largely eliminate.
- [17]
The node swap benchmarks were backed by NVMe Local SSDs on GKE; the setup is a few lines of KubeletConfiguration with LimitedSwap and Burstable QoS on the pods.
- [18]
Swap was validated with a complete Linux 6.1.1 kernel build; on a baseline node without swap, the minimum memory limit to prevent an OOM crash during compilation was 600 MB.
- [19]
Routing swap to a Local SSD cut the container memory limit by 50% to 300 MB without execution slowdown; it ran in 374s vs the baseline 433s.
- [20]
Compressing the limit further to 200 MB forced the active working set into swap, causing long I/O wait times and increasing execution time by over 40%.
- [21]
"This reinforces that swap serves as an insurance policy for burst memory, not a replacement for active RAM."
- [22]
In the swap benchmarks, gVisor headless Chrome pods doubled from 80 to 160 per node, and gVisor Python sandboxes tripled from 80 to 240.
- [23]
The benchmarks across three workloads found density gains of up to 3x, often with little or no latency cost.
- [24]
Because earlier compiled objects sit inactive in memory while the pipeline progresses, CI/CD jobs frequently hoard unused physical RAM.
- [25]
Agentic sandbox pods require large memory footprints to initialize and execute untrusted code, then typically enter long-tail idle phases waiting for user prompts.
- [26]
"Swap is insurance for idle and bursty memory. It's not more RAM."
- [27]
"Find those old nodes before your next upgrade does."
- [28]
The kubelet exposes Alpha metrics for the total memory.min and memory.low reservations on a node.
- [29]
The FAQ's example opt-in to tiered protection is a KubeletConfiguration with apiVersion kubelet.config.k8s.io/v1beta1, featureGates MemoryQoS: true, memoryReservationPolicy: TieredReservation and memoryThrottlingFactor: 0.9.
- [30]
v1.35, where cgroup v1 nodes fail by default, comes ten minor releases after cgroup v2 support went stable in v1.25.
- [31]
The 300 MB swap-backed kernel build finished 59 seconds, about 14%, faster than the 433-second baseline.
- [32]
If the over-40% slowdown is measured against the 433-second baseline, the 200 MB run took more than 606 seconds.
- [33]
Cutting the limit from 300 MB to 200 MB removed one third of the limit.
- [34]
A node on kernel 5.8 meets the listed 5.8 floor but is below the 5.9 recommendation for Memory QoS, inside the range where memory.high reclaim can livelock.
- [35]
The gVisor density results are a 2x gain for headless Chrome and a 3x gain for Python sandboxes.
Sources & coverage · 2 publishers
The reporting this story was synthesized from, earliest first. Every link goes to the original.
- kubernetes.io5d agoScaling Kubernetes Workloads with Node Swap
- kubernetes.io5d agoThe Shift to cgroup v2 in Kubernetes: What You Need to Know