A Pod at 40 Percent CPU Can Still Freeze Every 100 Milliseconds
A Kubernetes Pod can show only 40 percent CPU usage and still suffer severe CPU throttling because the graph is averaging activity across a much larger time window than the Linux scheduler uses to enforce the limit.
That mismatch is the whole problem.
A dashboard may average CPU usage over ten seconds, thirty seconds, or one minute. Linux can enforce a container CPU quota over a period measured in milliseconds. A bursty workload can consume its entire allowance near the beginning of that short period and then be prevented from running until the quota resets.
The graph later reports a moderate average.
The application experiences a pause.
This is why a latency-sensitive service can have healthy-looking utilization and poor p99 response time at the same moment.
The discussion centered on this exact failure mode. It also exposed a broader debate about whether CPU limits should be removed, retained for predictability, or applied only in specific environments.
The useful conclusion is not “CPU limits are always bad” or “CPU limits are always necessary.” The useful conclusion is that teams need to understand the time model before they trust the percentage.
CPU percentages hide the time dimension
Several participants argued that the easiest way to understand throttling is to stop thinking in percentages and start thinking in CPU time.
A process may use an entire core for a very short period and then remain mostly idle. When that burst is averaged across a long scrape interval, the result can look tiny.
One example in the discussion described an application using one full core for 50 milliseconds and then doing nothing for almost twenty seconds. On a long graphing window, that can appear as a fraction of one percent CPU.
Yet if the container has a very low CPU limit, the burst can still exceed the available quota inside the short CFS period and be throttled.
The percentage is not false. It answers a different question.
It tells you how much CPU time was consumed across the averaging window.
It does not tell you whether the workload exhausted its quota inside one scheduler enforcement period.
That distinction is especially important for modern web services. Many request paths are bursty. They perform a short block of CPU-intensive work, then wait on storage, a database, a remote API, or network I/O.
Average utilization can remain low while request latency is dominated by short scheduler pauses.
CFS quota is enforced in short periods
The discussion repeatedly used a 100 millisecond CFS period to explain the behavior.
A CPU limit is translated into an amount of CPU time available during each period. If the workload consumes that quota early, it cannot run again until the next period begins.
For example, a limit of 100 millicores corresponds conceptually to about 10 milliseconds of CPU time per 100 millisecond period.
If the application needs 2 milliseconds of CPU to serve a request, that may sound comfortably below the limit when averaged over one second.
But timing matters.
If the application needs more CPU time than remains in the current period, the request can be delayed until quota becomes available again. A small amount of actual CPU work can therefore produce a much larger wall-clock latency.
Another example in the discussion used a workload with multiple runnable threads on a four-core node. A container with a quota equivalent to 200 milliseconds of CPU time can consume that quota in only 50 milliseconds of real time if four threads run across four cores simultaneously.
The container then waits for the rest of the period.
Parallelism accelerates quota consumption.
The quota is measured in aggregate CPU time, not simply elapsed wall time.
Low average usage and high throttling are not contradictory
One participant questioned how a Pod at 40 percent CPU could be throttled, assuming throttling should happen only near 100 percent of the limit.
The replies clarified that usage percentage and throttling do not move together in a simple linear relationship.
A workload can show low average usage and still be heavily affected by throttling if its CPU demand arrives in short bursts.
It can also show relatively high average usage without throttling if its limit is large enough and the demand fits within each quota period.
The relationship depends on:
- The CPU limit
- The CFS period
- The number of runnable threads
- How quickly CPU demand arrives
- The graph averaging window
- Whether the node has free CPU
- Whether other workloads create contention
- The difference between CPU time and wall-clock time
This is why the intuitive model of “50 percent usage means 50 percent throttling” is wrong.
Throttling begins when the cgroup attempts to consume more CPU time than its quota permits during the enforcement period.
The long-term average may still be low.
The averaging trap is worst for latency-sensitive services
The strongest practical comment came from an operator who said their team removed CPU limits from request-path Deployments and saw p99 latency fall immediately. Their nr_throttled metric also dropped to zero.
That is a compelling operational result, but it should be interpreted carefully.
Removing a CPU limit allows the workload to use spare node capacity when it needs to burst. For a latency-sensitive service, this can eliminate quota-induced pauses.
The improvement can be dramatic when the application performs brief CPU-heavy work and then waits on I/O.
The danger is assuming spare CPU will always be available.
Under light cluster load, an unlimited container may burst freely. Under node contention, neighboring workloads also compete for CPU. The service may then receive only the share implied by its request and scheduling weight.
That can cause a different performance surprise.
A service that looked fast during low utilization may slow down during a traffic event precisely because neighboring Pods are also consuming their share.
Removing limits can eliminate CFS quota throttling. It does not create guaranteed burst capacity.
Requests and limits solve different problems
The discussion repeatedly returned to the distinction between CPU requests and CPU limits.
CPU requests influence scheduling and relative CPU entitlement under contention.
CPU limits impose a hard cap enforced through cgroup quota.
These settings should not be treated as interchangeable.
A realistic request tells the scheduler how much capacity the workload needs and influences how CPU is divided when the node is busy.
A limit tells the kernel not to allow the container to consume beyond a defined ceiling, even if spare CPU exists.
This creates two different risk models.
With a low request and no limit, the workload may perform well while the node has spare capacity but lose burst headroom under contention.
With a request equal to the limit, performance becomes more predictable, but bursty work may experience quota throttling if the value is too low.
With an inflated request, the workload may receive strong protection but reduce cluster efficiency.
There is no universal value that solves all three concerns.
Why some operators remove CPU limits
Several commenters argued that CPU limits provide little value for most ordinary workloads and can create avoidable latency.
Their reasoning was practical:
- CPU is compressible
- The kernel can share CPU between workloads
- Requests already influence fair allocation
- Hard limits can waste spare node capacity
- Bursty applications are vulnerable to quota pauses
- Averaged dashboards often hide the effect
- Memory limits are more important because memory is not compressible in the same way
This approach is common for trusted application workloads in clusters with good observability and realistic requests.
The principle is to let workloads borrow unused CPU rather than forcing idle capacity to remain unusable.
That can improve utilization and latency at the same time.
It works best when the platform team understands node contention, monitors requests carefully, and can identify noisy neighbors.
It is less comfortable in environments where workloads are untrusted or cost boundaries must be strict.
Why some operators keep CPU limits
The author of the original discussion listed several cases where CPU limits still made sense:
- Multi-tenant environments
- Workloads from different teams or customers
- Untrusted code
- Protection against runaway processes
- Cost control
- Managed platform models that require explicit ceilings
Another participant argued that limits improve predictability.
Their concern was that unlimited burst behavior can deceive operators. A service may look consistently fast because it uses spare CPU that is not guaranteed. Later, when adjacent Pods become busy, that burst capacity disappears and the service slows down during the worst possible traffic event.
From this perspective, a limit exposes the true performance envelope earlier.
If the workload cannot meet its latency objective within the allocated CPU ceiling, the allocation is wrong and should be fixed before production pressure reveals it.
This is a legitimate position.
Predictability has a cost. It may require unused headroom and careful load testing.
The debate is not really about whether unused CPU should exist. It is about whether the organization wants opportunistic performance or guaranteed performance.
For latency-sensitive services, load testing is the deciding tool
One operator argued that latency-sensitive containers should be load tested to determine a reasonable maximum load with headroom, then configured with request and limit set to that value.
This creates a Guaranteed QoS-style allocation and makes performance more deterministic.
The original author agreed that this is the ideal scenario but questioned whether most teams have time to test every application that thoroughly.
That tension is real.
The technically rigorous answer requires workload-specific evidence.
Teams should test:
- Normal traffic
- Peak traffic
- Burst concurrency
- Cold starts
- Startup CPU demand
- Downstream latency
- Node contention
- Multiple replicas
- Autoscaling lag
- Different CPU request and limit combinations
- Tail latency
- Throttling counters
A simple web service may not justify an elaborate performance program.
A critical request path may.
The correct policy can therefore vary by workload class.
Platform teams can provide safer defaults while requiring deeper testing only for services with strict latency or reliability objectives.
The number of threads changes how quickly quota disappears
The discussion included a detailed question about multithreading.
If a container can use several cores at once, it can consume aggregate CPU quota faster in wall-clock time.
A workload with four runnable threads can spend four milliseconds of CPU time during one millisecond of real time if all four threads run concurrently.
That is why a quota equivalent to 200 milliseconds of CPU time can disappear in 50 milliseconds on four cores.
A workload limited to two runnable threads consumes the same quota more slowly. With only two threads, the maximum aggregate consumption over 100 milliseconds of wall time is 200 milliseconds of CPU time.
This means a 2000 millicore limit can accommodate two fully busy threads without quota exhaustion under the simplified example.
The important variable is not only the CPU limit. It is how much parallel CPU time the application can consume during the period.
Languages and runtimes with worker pools, parallel garbage collection, or many runnable threads can therefore hit quota behavior differently from single-threaded services.
kubectl top is not enough
The discussion strongly criticized relying only on averaged CPU dashboards.
kubectl top and common monitoring panels are useful for broad utilization trends. They are not sufficient for diagnosing CFS throttling.
The source recommended examining cpu.stat, especially:
nr_throttledthrottled_usec
These counters reveal whether the cgroup was prevented from running because it exhausted quota.
A rising nr_throttled value shows that throttling occurred across scheduling periods.
throttled_usec shows accumulated throttled time.
The exact interpretation requires care, especially across multiple threads, but these counters provide evidence that ordinary utilization graphs miss.
The best workflow is to correlate them with application symptoms:
- p95 and p99 latency
- Request rate
- Error rate
- Queue depth
- CPU usage
- Replica count
- Node saturation
- Garbage collection
- Runtime thread behavior
A throttling counter without latency impact may not justify immediate action.
Latency degradation without throttling may point somewhere else.
PSI detects pressure that quota metrics miss
The original post also highlighted Pressure Stall Information through cpu.pressure.
PSI answers a different question from cpu.stat.
cpu.stat can show quota throttling for a cgroup.
CPU pressure can show that tasks are waiting for CPU because the system is under contention, even when no cgroup quota was exhausted.
This distinction matters after removing CPU limits.
A container with no hard CPU limit will not experience quota throttling in the same way, but it can still wait because the node is busy.
That wait can damage latency.
PSI helps reveal system-wide or cgroup-level contention that ordinary utilization averages may not explain clearly.
A useful diagnosis separates at least three cases:
- Quota throttling caused by a CPU limit
- CPU contention caused by neighboring workloads
- Hypervisor delay caused by the virtual machine not receiving physical CPU time
Each requires a different fix.
Hypervisor steal time is another invisible failure mode
The post also mentioned hypervisor steal time, commonly observed as %st.
Steal time represents CPU time the virtual machine wanted but the hypervisor did not provide because the physical host was running something else.
This is not Kubernetes CFS throttling.
It can still look like unexplained application slowness.
A Pod may have reasonable requests, no CPU limit, no concerning throttling counters, and still experience poor performance because the underlying VM is not receiving expected CPU time.
That is why cpu.stat is necessary but not sufficient.
The diagnosis should extend down the stack:
- Application latency
- Container throttling
- cgroup pressure
- Node CPU contention
- Hypervisor steal
- Instance type behavior
- Burstable VM credits where applicable
Kubernetes performance is often a layered scheduling problem.
The application runtime, Linux kernel, cgroups, Kubernetes scheduler, and cloud hypervisor can all delay execution for different reasons.
QoS class does not eliminate bad CPU sizing
A participant asked how Kubernetes QoS classes fit into the discussion.
The thread did not provide a complete answer, but it exposed the relevant principle.
Setting requests equal to limits can place a workload into a more predictable resource configuration, but that does not make the numbers correct.
A Guaranteed-style allocation with a CPU limit that is too low can still throttle badly.
A Burstable workload with a realistic request and no CPU limit may perform better under light load but vary more under contention.
QoS class is therefore not a substitute for capacity testing.
It influences eviction behavior and resource treatment, but it does not automatically determine whether the application can meet its latency objective.
The values still need to match the workload.
CPU limits do not protect a service from bad requests
One commenter pointed out that a CPU limit would not save an important service if its request were too low and neighboring Pods consumed the remaining CPU.
That correction is important.
A limit protects the node and other workloads from one container consuming beyond its ceiling.
It does not reserve additional CPU for the limited container.
The request is the important value for scheduling and guaranteed share under contention.
If a latency-sensitive service depends on spare CPU but has a tiny request, removing the limit may improve performance in quiet periods while leaving it exposed during pressure.
A robust design therefore begins with realistic requests.
The request should reflect the workload’s real minimum capacity requirement, not a long-term average chosen only to improve cluster density.
Accurate requests are harder than they look
The discussion argued that most teams set CPU requests incorrectly in one of two directions.
They set them too high for safety.
Or they set them near a long-term average that is too low for bursts.
Both are understandable.
High requests waste schedulable capacity.
Low requests create a false sense of performance when the node is quiet and unpredictable latency when contention arrives.
This is why requests should be informed by:
- Load tests
- Traffic profiles
- High-percentile usage
- Concurrency
- Threading behavior
- Latency objectives
- Autoscaling behavior
- Node contention tests
- Growth expectations
Average CPU alone is a weak sizing signal.
For latency-sensitive workloads, CPU time per request and concurrency can be more useful.
Queueing theory explains why predictability costs money
One operator summarized the tradeoff bluntly: teams cannot have perfect efficiency and perfect predictability at the same time.
If incoming demand is not fully predictable, the system needs spare capacity to absorb variation.
That is a queueing problem.
High utilization increases the probability that new work must wait.
This applies at the application thread pool, container CPU allocation, node capacity, and cluster level.
Removing CPU limits may improve opportunistic use of spare capacity, but it does not eliminate the need for headroom.
Setting limits and requests to a tested maximum can improve predictability, but the cluster may run with more unused CPU.
The correct choice depends on the business value of latency stability versus infrastructure efficiency.
The mistake is pretending the tradeoff does not exist.
A practical troubleshooting path starts below the dashboard
When a service shows unexplained latency despite moderate CPU graphs, the investigation should move through the stack deliberately.
First, confirm whether a CPU limit exists.
Then inspect the workload’s request and limit values.
Check cpu.stat for throttling counters.
Correlate those counters with latency.
Inspect CPU pressure to identify contention.
Check node-level saturation and neighboring workloads.
Review hypervisor steal time.
Examine thread count and runtime behavior.
Test the service under a controlled load.
Finally, repeat the test with adjusted limits or no limit, depending on the environment.
The goal is to identify the specific delay mechanism before applying a generic policy.
The right CPU policy is workload-specific
The discussion produced several competing recommendations:
- Remove CPU limits for latency-sensitive request paths
- Keep realistic requests
- Use limits in multi-tenant or untrusted environments
- Set request equal to limit after load testing
- Treat predictable performance as worth the unused capacity
- Prefer no limits for ordinary trusted workloads
- Monitor memory requests and limits carefully
- Use direct cgroup metrics instead of dashboard averages
These positions are not fully compatible as universal rules.
They become compatible when applied to different workload classes.
A platform policy can distinguish:
- Trusted internal services
- Public request-path services
- Batch workloads
- Multi-tenant workloads
- Untrusted code
- Strict-cost workloads
- Safety-critical workloads
- Development environments
Each class can have a different default and testing requirement.
That is more mature than enforcing one CPU rule across the entire cluster.
FAQ
Can a Pod be throttled while showing only 40 percent CPU?
Yes. The graph may average CPU over seconds or minutes while CFS enforces quota over a much shorter period. A burst can exhaust the quota and cause a pause that disappears inside the average.
What causes CFS CPU throttling?
A container is throttled when it consumes the CPU time allowed by its cgroup quota during the current enforcement period.
Why do multiple threads matter?
Multiple runnable threads can consume CPU time across several cores simultaneously, exhausting aggregate quota faster in wall-clock time.
Which metrics show actual throttling?
The discussion recommends inspecting nr_throttled and throttled_usec in cpu.stat and correlating them with application latency.
What does CPU pressure show?
PSI through cpu.pressure can reveal time tasks spend waiting for CPU because of contention, including cases where no CPU quota is configured.
Should latency-sensitive Pods have CPU limits?
The discussion did not reach one universal answer. Some operators remove limits to eliminate quota pauses, while others prefer tested request-equals-limit settings for predictability.
When are CPU limits most useful?
The source discussion identified multi-tenant clusters, untrusted workloads, runaway-process protection, cost control, and managed platform environments as common cases.
Is a realistic CPU request still important without a limit?
Yes. Requests influence scheduling and CPU share under contention. A low request can leave a service vulnerable when neighboring workloads become busy.
CTA
CPU performance problems often hide below averaged dashboards, across cgroups, node pressure, and virtualization layers. Request an online trial and explore how Sensaka helps teams correlate infrastructure signals with real application behavior.
Top comments (0)