Skip to content

Kubernetes CPU Limits Keep Losing the Same Argument

A viral repo re-proves what the kernel settled years ago, and your Helm charts still haven't noticed.

Ji-ho Choi
Ji-ho Choi
Security & Cloud Editor · Aug 14, 2026 · 5 min read
Kubernetes CPU Limits Keep Losing the Same Argument

A repo titled "For the love of god stop using CPU limits in Kubernetes" hit the Hacker News front page this week, complete with fresh benchmarks: a .NET API that starts twice as fast without a CPU limit, and p99 latency under a traffic spike that drops from 377ms to 49ms. The numbers are striking. The argument is also at least four years old — the title is lifted almost verbatim from Robusta's 2022 post that made the same case.

That's the actually interesting story here. The technical debate over CPU limits was effectively settled years ago, and the "delete them" side won. Yet the advice needs relitigating on the front page every year or two, because the ecosystem keeps quietly re-adding limits faster than engineers can remove them.

The 100ms tax

The mechanics are worth restating precisely, because the misunderstanding is where the damage comes from. A CPU limit is not an average-utilization cap. It's a hard budget enforced by the Linux CFS bandwidth controller in fixed windows — 100ms by default. limits.cpu: 500m means 50ms of CPU time per window, summed across every thread in the container. An app with eight busy threads on an eight-core node burns that budget in about 6ms of wall time, then the kernel freezes every thread until the next window opens.

That freeze happens up to ten times a second, and it's invisible at the resolution most people monitor. Your one-minute CPU graph averages out sub-second stalls, so a pod can sit at 60% of its limit on the dashboard while getting throttled in most enforcement windows. This is the classic "p99 is terrible but CPU looks fine" incident, and it routinely gets misdiagnosed as a slow dependency — because what actually surfaces is downstream timeouts, not high CPU.

The counterargument — "without limits, a runaway pod starves its neighbors" — misunderstands which mechanism does the protecting. Neighbors are protected by their own CPU requests, which translate into CFS scheduling weights. Under contention, every pod gets CPU proportional to what it requested; a limit on pod A adds nothing to pod B's guarantee. Limits only convert idle CPU your app could have used into enforced stalls.

A settled argument that won't stay settled

None of this is new. Dave Chiluk's investigation at Indeed found a genuine kernel bug — expiring per-CPU quota slices — that caused throttling far beyond what the arithmetic predicted; his fix landed in Linux 5.4 back in 2019. Even with the bug fixed, the fundamental window-and-freeze design remained, and by 2022 "requests yes, CPU limits no, memory limits always" had hardened into consensus among people who'd actually profiled it.

So why does every cluster still have limits everywhere? Three reasons, and they're organizational, not technical. Cargo-culted Helm charts ship 500m limits as defaults, and defaults are destiny. Platform teams enforce limits through LimitRange and admission policies because "every workload must have limits" is an easy rule to audit, while "requests must honestly reflect P95 usage" is not. And FinOps tooling loves limits because they make capacity math look tidy — even as throttling-driven overprovisioning quietly inflates the requests that actually drive node count.

The new repo's cost analysis makes that last point well: teams respond to throttling by inflating requests, clusters fill up with reserved-but-unused CPU, and the bill grows. Deleting limits and right-sizing requests attacks both problems at once. Its benchmark numbers (2x startup, 14% steady-state p99, 87% under spike load) come from one .NET workload and single runs, so treat the magnitudes as illustrative — but they're directionally consistent with what Robusta, Indeed, and a decade of production war stories have reported.

Where "just delete the line" bites back

The strongest surviving counterargument has nothing to do with noisy neighbors. It's thread-pool sizing. Runtimes size themselves from the CPUs they think they have, and several of them read the cgroup limit to find out. Delete the limit on a 48-core node, and a container requesting one CPU may spin up 48 GC threads and worker threads, fighting for a fraction of a core with constant context-switching as the tax.

The irony is sharp with Go: Go 1.25 finally shipped container-aware GOMAXPROCS that reads the cgroup CPU limit by default — the exact knob this repo tells you to delete. .NET does the same with ProcessorCount, which is how a 500m limit silently puts a .NET service into single-threaded GC mode.

The fix is to set concurrency explicitly instead of letting the runtime infer it from a setting you're removing: GOMAXPROCS for Go, DOTNET_PROCESSOR_COUNT for .NET, -XX:ActiveProcessorCount for the JVM, sized from your request, not the node. Bake it into the shared chart before you remove a single limit, or you'll trade throttling stalls for scheduler thrash and conclude the advice was wrong.

The other legitimate exceptions: untrusted or third-party code you can't trust to request honestly, benchmarking pods where reproducibility beats throughput, and Guaranteed-QoS pods using the static CPU manager for core pinning, where limits are structurally required.

The playbook, 2026 edition

If you run latency-sensitive services on Kubernetes, the order of operations hasn't changed much, but the tooling has improved:

  1. Measure first. Alert on the ratio of container_cpu_cfs_throttled_periods_total to container_cpu_cfs_periods_total — above ~20% on a critical service, you have a real problem today.
  2. Pin runtime concurrency explicitly, fleet-wide, before touching limits.
  3. Remove CPU limits namespace-by-namespace, least critical first. Keep memory limits — memory is incompressible, and over-committing it kills pods instead of slowing them.
  4. Right-size requests from 30-day P95 usage, then let Karpenter or the cluster autoscaler consolidate the freed capacity. This step is where the savings actually materialize; deleting limits alone frees nothing.

One genuinely new factor: in-place pod resize went GA in Kubernetes 1.35, meaning requests can now be adjusted on running pods without restarts. That makes honest, continuously-tuned requests — the load-bearing half of this whole argument — far cheaper to operate than when Robusta wrote the original post. The era of "set it at deploy time and pray" is ending.

The verdict on the repo itself: right conclusion, useful receipts, zero novelty — and that's fine. Arguments against entrenched defaults have to be won repeatedly, and every rerun of this one arrives with better evidence than the last. If your cluster still has CPU limits on first-party services in 2026, the problem isn't that nobody told you. It's that your Helm charts didn't get the memo.

Sources & further reading

  1. For the love of god stop using CPU limits in Kubernetes — github.com
  2. For the Love of God, Stop Using CPU Limits on Kubernetes (Updated) — home.robusta.dev
  3. Unthrottled: How a Valid Fix Becomes a Regression — engineering.indeedblog.com
  4. Container-aware GOMAXPROCS — go.dev
  5. Kubernetes 1.35: In-Place Pod Resize Graduates to Stable — kubernetes.io
  6. For the Love of God, Think Twice Before Dropping CPU Limits on Kubernetes — hansihe.com
Ji-ho Choi
Written by
Ji-ho Choi · Security & Cloud Editor

Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading