On August 26, 2026, Kubernetes v1.37 goes GA, and a kubelet flag most operators have never touched flips its default value: FailCgroupV1 goes from false to true. The practical effect is blunt. A node still running cgroup v1 doesn't get a deprecation warning or a degraded mode — the kubelet refuses to start. The node goes NotReady, its pods get evicted or rescheduled elsewhere, and if enough of your fleet is still on v1, you don't have a warning to heed, you have an outage to explain.
This hits self-managed fleets hardest — bare metal, older base images, anything provisioned before Ubuntu 22.04 or Debian 11 made cgroup v2 the default. If you're running a self-hosted PaaS that enforces per-tenant CPU and memory limits directly at the cgroup layer — not through a managed control plane that abstracts it away — this isn't a someday migration. It's a fleet-wide audit with a hard deadline attached, and the interface your limit enforcement depends on is changing underneath you at the same time.
The timeline: from "maintenance mode" to "won't boot"
"cgroup v1 is going away" has been true for years in the abstract sense that all new kernel and container-runtime work targets v2. What changed in 2026 is that Kubernetes stopped treating that as a suggestion:
| Release | Date | What changed |
|---|---|---|
| v1.31 | August 2024 | cgroup v1 support enters maintenance mode — still works, still gets critical fixes, but gets no new features (KEP-4569) |
| v1.35 | December 2025 | FailCgroupV1 kubelet flag reaches beta, still defaulting to false (permissive) |
| v1.37 | August 26, 2026 | FailCgroupV1 defaults to true; kubelet refuses to start on a cgroup v1 host unless explicitly overridden. Also raises the minimum container runtime to containerd 2.0 |
| v1.38+ | No earlier than late 2026 | Target for removing cgroup v1 code from Kubernetes entirely — no override flag will save you at that point |
The KEP-5573 removal proposal is explicit that there's no firm date for full removal yet, but the direction is committed: FailCgroupV1=false is a temporary escape hatch, not a supported long-term configuration. Kubernetes isn't acting alone here, either — systemd began obsoleting cgroup v1 in v256, RHEL 10 and Fedora 43 no longer boot into it, and Amazon Linux 2023 has moved on. The kernel ecosystem converged on this well before Kubernetes made it a boot-time failure.
Check what your fleet is actually running — right now
Don't assume based on when you provisioned a node. Mixed fleets — some nodes built two years ago, some rebuilt last month — are the common case, and "maintenance mode" gave everyone years to not notice which is which. Two commands settle it definitively on any node:
# The authoritative check: filesystem type of the cgroup mount
stat -fc %T /sys/fs/cgroup/
# cgroup2fs → cgroup v2 (unified)
# tmpfs → cgroup v1 (per-controller hierarchy)
# Confirms which controllers are active under v2
cat /sys/fs/cgroup/cgroup.controllersWhether a node lands on v1 or v2 by default comes down to its base image, not its cloud provider:
| Base OS | Default cgroup version |
|---|---|
| Ubuntu 22.04+ / 24.04 | v2 |
| Debian 11+ / 12 | v2 |
| Talos Linux 1.3+ | v2 (v1 support removed outright in Talos 1.10) |
| RHEL 9.4+ | v2 (RHEL 10 drops v1 entirely) |
| Ubuntu 20.04, Debian 10, CentOS 7-era images | v1 |
A Hetzner fleet built fresh today on stock Ubuntu 24.04 or Talos images is already fine. The risk is entirely in nodes that predate that default switch and were never rebuilt — exactly the machines a "maintenance mode" release note gave you no urgency to touch.
What actually breaks: the enforcement semantics, not just the file paths
If your PaaS enforces tenant CPU/memory limits by writing cgroup files directly — rather than going through a container runtime abstraction that papers over the difference — the v1→v2 migration isn't a path rename. The enforcement model itself changes for CPU, memory, and I/O:
| Resource | cgroup v1 | cgroup v2 | What's different |
|---|---|---|---|
| Memory limit | memory.limit_in_bytes — one hard ceiling | memory.max (hard) + memory.high (soft) | v2 gives you a two-tier model: memory.high throttles and forces reclaim before the process is killed; v1 has no equivalent back-pressure step, it's reclaim-then-OOM at one number |
| CPU limit | cpu.cfs_quota_us / cpu.cfs_period_us | cpu.max (single file, quota and period in one line) | Same underlying CFS bandwidth mechanism, different file — a straight rename, low risk |
| I/O limit | blkio.throttle.* under a separate blkio hierarchy | io.max under the unified hierarchy | v1's blkio lived in its own controller tree; v2 folds it into the same per-cgroup directory as CPU and memory |
| Pressure visibility | None per-cgroup — only system-wide /proc/pressure/* | cpu.pressure, memory.pressure, io.pressure per cgroup | This one doesn't have a v1 equivalent at all. PSI accounting scoped to an individual tenant's cgroup is a v2-only capability |
The memory case is the one that bites in practice. A limit-enforcement path written against memory.limit_in_bytes and treating "OOM" as the only failure mode has no way to express memory.high's throttle-before-kill behavior — which means porting it naively to v2 either ignores the soft limit entirely (losing the back-pressure v2 was designed to add) or misreads throttling events as if they were OOMs. And the last row matters beyond migration: if you want to alert on a tenant's cgroup approaching memory contention before the OOM killer fires — not the whole node's aggregate pressure, but that one tenant's — memory.pressure is the only interface that exists. It has no v1 counterpart to fall back to.
A worked example: a noisy tenant under v1 vs v2
Say a tenant's build process leaks memory and climbs toward its 2GB limit. Under cgroup v1, there is exactly one signal to watch and one outcome to plan for:
memory.usage_in_bytes climbs toward memory.limit_in_bytes (2147483648)
→ kernel reclaims what it can
→ usage still exceeds the limit
→ cgroup-scoped OOM killer fires, a process in the tenant's cgroup diesEnforcement code has one job: alert (or intervene) as usage approaches the hard ceiling, because there's no intermediate state — reclaim and OOM happen back-to-back with no throttling step in between.
Under cgroup v2, the same tenant's climb toward its limit passes through a state v1 simply doesn't have. With both memory.high (say, 1.6GB, an 80% soft threshold) and memory.max (2GB, the hard ceiling) configured:
memory.current climbs past memory.high (1717986918)
→ kernel throttles the cgroup and forces reclaim, no process dies yet
→ memory.pressure's "full" value for this cgroup rises — visible per-tenant, in real time
→ if usage keeps climbing anyway, it eventually crosses memory.max
→ only then does the cgroup-scoped OOM killer fireA v1-style enforcement path that only watches for the terminal OOM event will correctly report a kill when one happens — but it has nothing to say about the tenant that spent ten minutes throttled and degraded before recovering on its own, because that intermediate state doesn't exist in v1's model at all. A tenant-facing dashboard built only against v1 semantics can't surface "your build was throttled, not killed" as a distinct condition, and an SRE debugging a slow-but-not-crashed tenant workload has no memory.pressure file to point at until the fleet is actually on v2.
Migration checklist for a Cluster API / Hetzner fleet
- Audit every node image, control-plane and worker, with the
statcheck above — not a spot check on one node per pool, since mixed-age fleets are exactly where this fails silently until 1.37 lands. - Standardize on a v2-default base image. For CAPH-managed fleets, that's a current Ubuntu 24.04 or Talos 1.3+ image. If you're pinned to an older image for other reasons, cgroup v2 can be forced via the
systemd.unified_cgroup_hierarchy=1kernel boot parameter — but plan to replace the image anyway rather than carry that flag indefinitely. - Bump the container runtime to containerd 2.0+ before touching Kubernetes 1.37. The runtime requirement lands in the same release as the
FailCgroupV1flip; miss it and you can fail upgrades on two independent axes at once. - Audit your own limit-enforcement code against the mapping table above, not just the cluster's boot capability. A node happily running cgroup v2 doesn't help if your control plane is still writing to
memory.limit_in_bytespaths that no longer exist. - Don't lean on
FailCgroupV1=falseas the fix. It buys time on a single upgrade, not a strategy — KEP-5573's stated direction is removing the v1 code path outright, at which point the override stops being an option at all.
For a platform that's already committed to running its own fleet on owned hardware instead of leasing an abstraction from a managed vendor, this kind of interface change is the tradeoff made explicit: you get the control, and you also own the audit when the interface underneath you moves. Bex.co is built for exactly that model — an open-source, AI-native Render alternative that runs tenant workloads on Cluster API–managed Hetzner machines you actually own, with resource enforcement that's inspectable rather than hidden behind someone else's control plane. Check out the project on GitHub if you're planning this kind of fleet audit yourself.
Sources
- Kubernetes 1.31: Moving cgroup v1 Support into Maintenance Mode
- KEP-4569: cgroup v1 Maintenance Mode
- KEP-5573: Remove cgroup v1 support
- Nodes with cgroup v1 fail to start by default in Kubernetes v1.35+ (SUSE)
- Kubernetes v1.37: containerd 2.0, cgroup v2, and What to Fix Before August 26
- Control Group v2 — The Linux Kernel documentation
- PSI Pressure Metrics (cgroup2)
- Talos Linux v1.9 Cgroups Resource Analysis.



