Skip to content

CPU is at 40%. Why is the pod being throttled?

The Machine Had More Resources. Why Was It Slower? · Part 1 of 6 · 6 min read

Read the worked explanation ↓

Interview question. A Java API pod has a two-core CPU limit. Its dashboard shows 0.8 cores of average usage, yet p99 latency jumps from 35 ms to 220 ms during bursts. The container's CPU throttling counter increases. Explain how that can happen and decide whether removing the limit is the right fix.

An average tells you how much CPU was consumed over the measurement interval. It does not tell you when it was consumed. That is the missing piece in this incident.

Suppose the dashboard reports 0.8 CPU-seconds per second over one minute. That is 40% of the two-core limit. It could represent a fairly steady service. It could also represent many very short intervals in which the application wanted four or eight cores, followed by quiet periods. The scheduler sees the second workload very differently, even though the minute-long chart shows the same average.

A two-core cgroup exhausts CPU quota early in one period even while one-minute mean usage remains low.
A two-core cgroup exhausts CPU quota early in one period even while one-minute mean usage remains low.
Think first

Two API versions each average 0.8 cores and have a 2-core limit. Version A runs two steady workers, and version B wakes eight compute-heavy workers in occasional synchronized bursts followed by quiet intervals. The CPU quota period is 100 ms. Explain the different tail latency, identify at least four discriminating measurements, and design a safe experiment to compare CPU limit changes without harming neighboring tenants.

Take a few minutes to form an approach, then compare it with the worked explanation.

Reveal the worked explanation

What the limit actually limits

Under the Linux cgroup v2 CPU controller, cpu.max represents a quota and a period. For example, 200000 100000 means the cgroup is entitled to up to 200,000 microseconds of aggregate CPU runtime during a 100,000-microsecond period. That is two cores worth of CPU time, not a promise that precisely two threads will always run.

Imagine a request burst wakes eight runnable workers. With eight available physical CPUs, those threads can collectively burn 200 milliseconds of runtime in about 25 milliseconds of elapsed time. The quota for the current 100 ms period is now exhausted. Under ordinary bandwidth control, the runnable group cannot use more quota until the next period. Even if the rest of the machine has idle cores, those threads can wait.

A request arriving just after quota exhaustion might sit unable to execute Java code, finish a TLS handshake, or emit its response. The same request arriving early in the next period might be fast. Your p99 reflects the unlucky placement in time.

The example assumes a normal 100 ms period and enough parallel runnable work to consume two-core quota rapidly. Configured period length, quota, ancestor cgroups, and burst settings change the precise behavior. Do not claim every throttled period necessarily adds 75 ms to every request.

Find out whether throttling is actually the culprit

First correlate latency spikes with the rate of change in the cgroup's cpu.stat, especially nr_periods, nr_throttled, and throttled_usec. These counters are cumulative. An absolute total since startup is not actionable. A rising throttled-period fraction and substantial throttled time during the bad windows are much stronger evidence.

Check cpu.max in the affected cgroup rather than trusting deployment YAML alone. A Kubernetes CPU limit may be translated into cgroup quota, but admission policy, runtime configuration, and parent cgroups can affect the effective budget. Confirm the kernel's cgroup version and inspect the container and its ancestors if the observed quota seems inconsistent.

Now compare runnable thread pressure with actual CPU usage. CPU usage around 0.8 cores does not tell you whether eight workers are briefly fighting for four available CPUs or two cores of quota. Track run-queue delay, application thread-state samples, request arrival bursts, GC activity, and the concurrency of work that wakes simultaneously. CPU quota and JVM garbage collection can interact because collector threads also need CPU.

I would run a controlled test at the same offered traffic, once with the current CPU limit and once with a higher or absent limit, while retaining an appropriate CPU request and observing node contention. If tail latency improves and the cpu.stat throttling delta collapses, that supports the diagnosis. It does not make unlimited CPU safe for every tenant sharing that node.

Why simply adding replicas can disappoint

Adding replicas may spread requests but only if the balancer and traffic patterns actually spread those bursts. A single request that uses a pool of eight CPU-heavy workers can still exhaust a two-core pod's quota. A synchronized refresh or scheduled job might trigger all pods at once. Conversely, removing the limit may improve latency for this workload while making unrelated pods miss their performance budgets through noisy-neighbor contention.

I would determine the workload's burst envelope before choosing a setting. A request with 20 ms of CPU work spread over four parallel phases has a different scheduling profile than a long computation that needs two whole cores continuously. Thread counts, executor limits, and the amount of work performed on the critical path matter. Restricting concurrency can sometimes make p99 better by avoiding bursts and queue growth, but it can also lower throughput, so measure both.

Consider an autoscaler, too. If it scales from minute-level CPU utilization while the real problem occurs in 25 ms bursts, it can miss the signal. Scale decisions may need request concurrency, queue delay, or a latency-relevant saturation metric, with safeguards against oscillation. Longer CPU windows do not erase a short scheduling cliff.

What I would change

For a latency-sensitive dedicated API, I would test a CPU request that reflects actual resource needs and reconsider an unnecessarily tight CPU limit, subject to cluster policy and fairness requirements. In an environment with strict quotas, I might increase the quota, reduce per-pod burst parallelism, or shift batch work to separate pools. Each option addresses a different part of the problem.

The interview answer should end with evidence. Show that throttling and p99 are correlated, establish a controlled causal improvement, and check the blast radius. The lesson is not that CPU limits are bad. It is that average utilization is the wrong measurement of a scheduler enforcing a budget over much shorter intervals.

Go deeper

The cgroup v2 interface cpu.max defaults to a 100,000 µs period if only the quota is changed. cpu.stat includes CPU usage and, when bandwidth control is enabled, throttling statistics. The kernel documents that some throttling counters describe the group's own limit rather than inherited throttling. Consult both local and ancestor groups where needed. Kubernetes CPU requests primarily influence scheduling and CPU shares/weights under contention, whereas CPU limits set a hard ceiling through quota on supported Linux configurations. Burstable quota configurations and runtime defaults are implementation-specific. JVM thread pools and GC worker threads contribute to the same cgroup CPU budget.

Check your reasoning

  • Explain two-core aggregate runtime budget within a short quota period.
  • Distinguish average CPU utilization from within-period runnable bursts.
  • Correlate request tails with throttling deltas and effective cgroup/ancestor limits.
  • Run a controlled load comparison with changed quota and unchanged offered traffic.
  • Account for fair-node usage, CPU requests and per-pod work concurrency.

Common mistake: A pod averaging 0.8 CPU cannot be throttled when its quota is two cores.

A pod has low minute-average CPU but rising throttling counters and burst-time latency. Which next step best tests the diagnosis?

Read the answer explanation

Compare latency with cgroup throttling deltas and test quota or concurrency at equal load — Kernel quota is enforced over much shorter periods, and short bursts can exhaust aggregate runtime.

Apply it elsewhere

A machine-learning inference server shares a Kubernetes node with background training. Interactive predictions occasionally exceed their 75 ms deadline despite a low five-minute CPU average. Choose a resource-isolation and load-shedding policy and explain how to prove it protects interactive work.

Compare your approach

Correct aggregate quota arithmetic, short-period runnable bursts, `cpu.stat` deltas, ancestor cgroups, controlled intervention, fair-node impact, and distinguish CPU request from CPU limit.

One more constraint

Does a CPU chart averaging below the configured core limit prove the kernel never throttled the container?

Quick revision

A Linux CPU limit is typically a runtime quota over a short period, not a cap on minute-average utilization. A two-core quota over 100 ms can be consumed by eight threads in roughly 25 ms, leaving a throttling gap even if the minute-average is 0.8 cores. Compare p99 windows with changes in cpu.stat, inspect effective cgroup quotas and runnable-thread pressure, then test quota or concurrency changes at the same load. Removing a limit may help this pod but can hurt others on a shared node.

References