A container requesting a full CPU used to get a fair share of the machine. Then the node moved to cgroup v2, and that same container's CPU priority silently dropped to less than 40 percent of the default — with no change to any YAML you wrote. Here is the table up front, because this is the whole post in five rows:
| Request | cpu.shares (v1) | cpu.weight, old linear formula | cpu.weight, new quadratic formula |
|---|---|---|---|
| 100m | 102 | 4 | 17 |
| 250m | 256 | 10 | 35 |
| 500m | 512 | 20 | 59 |
| 1000m (1 CPU) | 1024 | 39 | 100 |
| 2000m (2 CPU) | 2048 | 79 | 174 |
Read the middle column against cgroup v2's default weight of 100: under the old conversion, a pod requesting an entire CPU ranked below every system daemon running outside Kubernetes. The January 2026 fix — a new quadratic conversion announced on the Kubernetes blog on January 30 — restores 1 CPU to weight 100 and gives small requests usable granularity again. The rest of this post explains how the math works, where the old formula actually hurt, and the four checks a self-hosted fleet should run to find out which side of the fix its nodes are on.
How milliCPU becomes a weight: two conversions, three anchor points
Kubernetes never writes cgroup values directly from your pod spec. It runs two conversions in series. First, milliCPU becomes v1-style shares: cpu.shares = milliCPU × 1024 / 1000, where 1024 is cgroup v1's default shares value and has nothing to do with millicores. That is why 100m becomes 102 shares, 250m becomes 256, and 1000m lands exactly on 1024. Second, those shares are converted to a v2 weight — and that second step is where the trouble lived.
The ranges explain why a naive mapping was doomed. Cgroup v1 shares span 2 to 262144 (2¹ to 2¹⁸, a logarithmic scale), while v2 weights span 1 to 10000 (10⁰ to 10⁴, a decimal scale). KEP-2254, which carried Kubernetes onto cgroup v2, bridged them with the simplest thing that could work: a straight line, cpu.weight = 1 + ((cpu.shares − 2) × 9999) / 262142. Linear across the full span, and wrong exactly where it mattered.
The January 2026 replacement keeps the same inputs but fits a quadratic in log space: cpu.weight = ceil(10^(L²/612 + 125L/612 − 7/34)) with L = log₂(cpu.shares). The formula looks intimidating until you see it was designed to cross exactly three points: (2, 1) the minimums, (1024, 100) the defaults, and (262144, 10000) the maximums. Pinning the defaults together is the entire fix in one decision — a 1-CPU container is back at parity with system processes instead of at 39 percent of their priority. Everything else in the curve is "close to linear" interpolation between those anchors.
One architectural detail matters for planning: this change was implemented at the OCI runtime layer, not in Kubernetes itself. The kubelet still computes shares exactly as before; runc (from v1.3.2) and crun (from v1.23) apply the new math when writing the weight. Your cluster does not get the fix by upgrading Kubernetes — it gets it when the container runtime on each node is new enough.
Where the old formula actually hurt
The upstream post names two failure modes, and both land harder on a multi-tenant PaaS than on a single-team cluster.
Deprioritization against everything outside Kubernetes. On a v1 node, a 1-CPU container at 1024 shares stood equal with host daemons at their default. On a v2 node under the linear formula, that container weighed 39 against the same daemons' 100. In ordinary operation nobody notices — weights only matter under contention — but contention is precisely when a platform's promise is tested: the node is saturated, the kubelet is throttling, and your paying tenant's workload is the cgroup the scheduler sheds first, losing to log shippers and monitoring agents that happen to live outside the Kubernetes cgroup subtree. For a fleet packing several tenants per node, "loses every contention event to the host" is a systematic bias, not a rounding error.
Granularity collapse at the small end. A 100m request converted to weight 4. Four units of weight cannot be subdivided, which kills any plan to run sub-cgroups inside the container with distinct priorities — the exact direction Kubernetes is heading with writable cgroups for unprivileged containers (KEP-5474). The new formula gives the same request weight 17: still small, but divisible enough to express "this process group matters more than that one" inside a sandbox. If your roadmap includes agent sandboxes with internal process tiers, the old floor of 4 would have been a wall.
A third casualty the post warns about explicitly: your own tooling. Anything that assumed the linear formula — capacity models, cost-allocation math that converts requests to weights, dashboards that predict throttle behavior — now computes wrong numbers on runtimes carrying the fix. The failure is silent in both directions: old tooling on new runtimes, or new expectations on nodes nobody upgraded.
What did not change: where the conversion is transparent
Not every CPU control flows through shares-to-weight, and naming the unaffected parts keeps the audit scoped. CPU limits never touched this code path: cpu.cfs_quota_us under v1 and cpu.max under v2 are absolute bandwidth caps, and a pod capped at half a core is capped at half a core on either cgroup version. Scheduler bin-packing is equally untouched — the scheduler places pods using milliCPU requests from etcd, long before any cgroup file exists, so your overcommit ratios and node-fullness math are byte-identical. Guaranteed pods (limits equal requests) under steady state behave the same too, since throttling there is quota-driven, not weight-driven.
The conversion only bites under contention between cgroups at the same level — exactly the oversubscribed, burstable-heavy node pools where a PaaS extracts its margin. If a pool runs Guaranteed workloads with headroom to spare, the new formula changes nothing observable, and you can note that pool as verified-by-construction. Everything else — Burstable tenants sharing nodes near capacity, Batch and BestEffort pools, any node where host daemons compete with tenants — goes through the four checks below.
The four-check audit for your fleet
Run these in order; each takes minutes, and together they tell you whether "fair share" still means what you tuned it to mean.
1. Find out which cgroup version each node pool actually runs. The migration is inherited silently — a node image refresh, a kernel default flip, or a fresh machine provisioned by your Cluster API provider can move a pool to v2 without any workload change. Check the kubelet's cgroup driver and confirm per pool, not per cluster, since mixed fleets are the norm during rollouts.
2. Check the OCI runtime version on every node image. The new formula arrives with runc 1.3.2+ or crun 1.23+. A node on cgroup v2 with an older runtime still applies the linear math — the worst combination, because dashboards may assume the fix while kernels enforce the old weights. Pin minimum runtime versions in your machine images alongside the Kubernetes version, and treat a runtime below the cutoff as a distinct, known-bad configuration rather than "slightly old."
3. Re-validate one fair-share ratio by hand. Pick a representative contention pair from your fleet — say a 500m tenant workload beside a 1-CPU one — and compute the ratio under both formulas: 20:39 old versus 59:100 new. If any autoscaling threshold, overcommit ratio, or noisy-neighbor policy was tuned against the old proportions, re-derive it. The absolute numbers changed, but more importantly the relative spacing changed, so limits tuned years ago against v1 behavior deserve a second look rather than assumed parity.
4. Grep your observability and cost tooling for the old constant. Search for 262142, 9999, and any shares-to-weight helper. Anything matching needs the quadratic replacement or an explicit pin to runtime version. This is also the moment to stop hand-rolling the conversion: derive expected weights from the runtime's own behavior (a canary pod reading back its cpu.weight) instead of reimplementing the formula in a spreadsheet that rots at the next upstream change.
The pattern worth keeping
Step back and this is a familiar story wearing kernel clothes: a platform writes policy against an abstraction (milliCPU requests), the abstraction compiles down through two lossy translations, and one translation changes meaning under an "invisible" infrastructure migration. The tenants most exposed are the ones sharing nodes most densely — which is to say, everyone on a bin-packed PaaS.
The good news is that the fix is already shipped and the audit is small: cgroup version per pool, runtime version per image, one ratio re-derived, one grep through tooling. Do it before the next node-image rollout does it for you.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



