CPU Throttling in Containers: Detect It and Set CPU Limits Correctly
AI generated
FROM
RUN
Docker · Performance · Linux
CPU Throttling in Containers
When low CPU usage is still slow

A dashboard shows 40 percent CPU usage, yet timeouts and latency spikes keep piling up. The contradiction almost always resolves itself once you look at cpu.stat: the container is being throttled long before its average usage comes anywhere near a limit.

16 min read cpu.stat nr_throttled CFS quota requests vs limits

1. What CPU throttling technically means

CPU throttling happens when a process requests more CPU time within a billing period than its configured quota allows, and the Linux scheduler then pauses the process entirely for the rest of that period, regardless of whether free CPU capacity is currently available on the host. That is the crucial difference from a normally overloaded CPU: even if eight out of twelve host cores sit completely idle, a container with a tight limit still gets throttled, because throttling depends exclusively on its own cgroup's quota, not on actual host utilization.

The underlying mechanism is called the Completely Fair Scheduler, CFS for short, and it works in fixed time windows, 100 milliseconds long by default. Within each window a container may consume as much CPU time as its quota allows. If the quota is already exhausted within the first 20 milliseconds of a 100-millisecond window, for example because several threads were briefly active at once, the kernel pauses the container for the remaining 80 milliseconds, entirely regardless of how urgent the request actually was.

2. cpu.stat: the file that makes throttling visible

The kernel file cpu.stat, which exists for every cgroup under /sys/fs/cgroup/.../cpu.stat, contains exactly the counters that prove throttling. The field nr_periods counts how many CFS time windows have been observed in total since the cgroup was created. nr_throttled counts in how many of those windows the container was actually throttled because its quota ran out early. throttled_usec sums the total time in microseconds the container has spent in a throttled state since the cgroup was created.

The ratio of nr_throttled to nr_periods is the single most informative value for deciding whether a CPU limit is too tight: if that ratio consistently sits above a few percent, the container is throttling regularly, even if its average CPU usage looks harmless in docker stats, because averages taken over several seconds smooth out short, hard throttling spikes and make them practically invisible.


# Read cpu.stat of a running container
CID=$(docker inspect --format '{{.Id}}' my-container)
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/cpu.stat

# Example output:
# usage_usec 48213942
# user_usec 41022103
# system_usec 7191839
# nr_periods 128841
# nr_throttled 9532
# throttled_usec 4821394

# Rough throttling ratio: 9532 / 128841 = ~7.4 percent of all periods

3. CPU requests and CPU limits are two different things

A common misunderstanding treats CPU requests and CPU limits as the same concept under different names. A request, mapped in Docker via --cpu-shares, only defines a relative priority when multiple containers compete for the same CPU time, but it sets no absolute upper bound. A container with a high share value gets allocated more CPU time than one with a lower value whenever there is competition, but as long as there is no competition, it can freely use the full available host CPU.

A limit, on the other hand, implemented through the quota in cpu.max and the Docker flag --cpus, sets a hard, absolute ceiling that applies regardless of the rest of the host's utilization. That is exactly where the most common mistake happens: a team sets a limit to make resource usage predictable, but picks a value below the application's actual peak load, and thereby produces artificial throttling in exactly the moments the application needs the most performance.

4. Typical symptoms in production

CPU throttling rarely shows up as an obvious error, it shows up as a diffuse performance problem: latency spikes that appear seemingly at random, requests that occasionally take far longer than the median with no visible relationship to overall load, and health checks that fail sporadically even though the application, according to its logs, keeps running normally. What makes it especially tricky is that classic CPU usage dashboards often hide these spikes completely, because they are averaged over seconds or minutes, while a single throttling event lasts only a few milliseconds.

In environments with synchronous request handling, for example classic web frameworks without asynchronous I/O, the problem intensifies further: a throttled thread does not just block itself, it potentially blocks the entire worker, turning a brief CFS throttling event of a few milliseconds into a noticeable double-digit millisecond latency spike, as soon as multiple requests within the same period compete for the same tight quota.

5. Measuring throttling systematically instead of guessing

Instead of immediately suspecting application code for vague latency issues, a systematic first step pays off: reading cpu.stat before and after a load spike and computing the difference in nr_throttled and throttled_usec. If both values rise noticeably during the load spike, CPU throttling is a very likely contributing factor, regardless of what application metrics say about CPU usage in percent.

For ongoing monitoring, cAdvisor is a good fit, extracting the metric container_cpu_cfs_throttled_periods_total directly from the cgroup statistics and exposing it through Prometheus as a time series. A Grafana panel that shows this metric alongside classic CPU usage reliably surfaces throttling problems that pure usage dashboards left completely invisible, because high throttling rates can well occur at low average usage.


# Compare throttling before and after a load spike
CG=/sys/fs/cgroup/system.slice/docker-${CID}.scope

echo "Before:"; grep -E 'nr_throttled|throttled_usec' $CG/cpu.stat
# ... trigger a load spike, e.g. via a load test ...
echo "After:"; grep -E 'nr_throttled|throttled_usec' $CG/cpu.stat

# Prometheus query for ongoing monitoring
# rate(container_cpu_cfs_throttled_periods_total[5m])
#   / rate(container_cpu_cfs_periods_total[5m])

6. Sizing CPU limits correctly

The basic rule for sensible CPU limits is the same as for memory limits: measure first, then limit, not the other way around. A container should first be observed without a limit, or with a very generous one, under realistic peak load, ideally with a load test that simulates short bursts rather than constant steady load, because short, intense load spikes are especially prone to throttling. Only with these measurements in hand does it make sense to set a limit that covers the actual peak load with sufficient headroom.

A concrete calculation illustrates the quota mechanics: a limit of --cpus=2 produces a quota of 200000 microseconds against a default period of 100000 microseconds. If the application runs with four parallel threads, each briefly active on different cores, the combined CPU time of all four threads can exhaust the quota within a fraction of the period, even if no single thread computes continuously. Multi-threaded applications therefore tend to need more generous limits than pure average consumption would suggest.


# Calculation example: --cpus=2 translated into cpu.max
docker run -d --name calc-test --cpus=2 nginx:alpine
CID=$(docker inspect --format '{{.Id}}' calc-test)
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/cpu.max
# Output: 200000 100000  --> 2 full CPU cores per 100ms window

# The period itself can theoretically be adjusted (rarely useful),
# it usually stays at the default of 100000 microseconds

7. Alternative strategy: shares instead of hard limits

For latency-critical applications where even occasional throttling is unacceptable, one alternative is to skip hard CPU limits entirely and rely solely on CPU shares. Without a limit, a container can use the full available host CPU at any time, while shares only kick in during actual resource competition and then ensure a fair, proportional distribution, instead of applying a hard, time-window-bound brake.

This strategy works best on dedicated or clearly overprovisioned hosts where enough combined CPU capacity exists for all containers, so that a single container could theoretically claim more CPU during a malfunction, but in practice this rarely becomes a problem for other workloads. On densely packed, heavily utilized multi-tenant hosts, on the other hand, a hard limit is usually indispensable, to prevent a single misbehaving container from crowding out every other container.

8. Common mistakes in limit configuration

A particularly widespread mistake is copying CPU limits wholesale from a Kubernetes or Docker Compose template without adapting them to the actual application, for example a generic cpus: 0.5 applied to every microservice regardless of whether that service performs compute-heavy work or just lightweight I/O forwarding. A second common mistake is setting limits once at initial deployment and never revisiting them, even though an application's actual resource needs change over months as new features and growing data volumes are added.

A third, subtler mistake involves languages and frameworks that query the number of available CPU cores themselves for sizing internal thread pools: if that query returns the host's physical core count instead of the effective core count set through cpu.max, the application sizes its own thread pools too large, which further increases the likelihood of throttling, because more threads end up competing simultaneously for the same, much smaller quota than originally intended.

9. A practical workflow from symptom to fix

A proven workflow starts with the symptom, usually unexplained latency spikes despite low average CPU usage, and moves through cpu.stat to confirm whether throttling is actually occurring. Once confirmed, the next step is measuring the real peak load without a limit, followed by resizing the limit with sufficient headroom, and finally setting up ongoing monitoring of the throttling rate to catch future regressions early, before they show up again as diffuse latency problems.

The table below compares the key metrics from cpu.stat with their respective meaning, as a quick reference for the next performance analysis of a container whose CPU usage looks harmless on the dashboard, while its users still complain about delays.

Metric Meaning Warning sign Next step
nr_periods Number of observed CFS time windows Baseline value, not an alarm by itself Basis for the ratio calculation
nr_throttled Number of throttled time windows Ratio to nr_periods above a few percent Raise the limit or analyze the load
throttled_usec Total throttled time in microseconds Noticeable rise during load spikes Correlate with latency metrics
cpu.max quota/period Configured limit Quota clearly below measured peak load Reset the limit based on peak measurement

Mironsoft

Container infrastructure, CI pipelines and deployment automation

Docker setups that hold up across the team and in production?

We review existing Dockerfiles and Compose stacks for security gaps, bloated images and fragile build pipelines, then build a container infrastructure that builds fast, runs securely and stays understandable across the team.

Dockerfile Review

Systematically optimizing multi-stage builds, layer caching and image size.

Security Audit

Hardening container isolation, secrets handling and image scanning against real attack surfaces.

CI/CD Integration

Building build pipelines, registries and deployment strategies for reproducible releases.

10. Summary

Detecting CPU Throttling: The Essentials at a Glance

Root cause

The CFS quota runs out early within a 100ms window, regardless of free host CPU.

Visible in

cpu.stat, through the fields nr_periods, nr_throttled, and throttled_usec.

Most common mistake

Confusing requests (shares) with limits (quota), guessing limits without a load test.

Fix

Measure the real peak load, set the limit with headroom, monitor the throttling rate continuously.

11. FAQ: Detecting CPU Throttling: The Essentials at a Glance

1What exactly does CPU throttling mean technically?
The Linux CFS scheduler pauses a container for the rest of a time window as soon as it has fully consumed its configured CPU quota within that window, regardless of whether free CPU capacity is currently available on the host.
2Why does low average CPU usage still show throttling?
Averages taken over several seconds almost completely smooth out short, hard throttling spikes lasting just a few milliseconds, making them practically invisible in classic usage dashboards even though they cause noticeable latency.
3What does the nr_throttled field in cpu.stat mean?
It counts in how many of the observed CFS time windows the container was actually throttled because its quota ran out early. The ratio to nr_periods shows how frequently throttling occurs relative to total runtime.
4What is the difference between CPU requests and CPU limits?
Requests, implemented via CPU shares, only define a relative priority when there is competition for resources, with no absolute ceiling. Limits, implemented via the quota in cpu.max, set a hard ceiling regardless of host utilization.
5Why are multi-threaded applications especially prone to throttling?
When several threads are briefly active at the same time on different cores, their combined CPU time adds up within the same time window, which can exhaust the quota much faster than the pure average consumption of a single thread would suggest.
6How do I measure whether a container is currently being throttled?
Most directly through the cpu.stat file of the container's cgroup, by observing the nr_throttled and throttled_usec fields before and after a load spike. For ongoing monitoring, the Prometheus metric container_cpu_cfs_throttled_periods_total is a good fit.
7Should I skip CPU limits entirely?
On dedicated or clearly overprovisioned hosts this can make sense, combined with CPU shares for fair distribution during competition. On densely packed multi-tenant hosts, a hard limit is usually necessary to protect other containers.
8How does the CFS period relate to throttling?
The default period is 100 milliseconds. Within each such window, the container may consume at most its configured quota. If the quota is exhausted early in the window, the container pauses for the rest of the window, regardless of how urgent the request is.
9Can language runtimes themselves contribute to throttling?
Yes, when a runtime uses the host's physical core count instead of the effective core count set via cpu.max for sizing its internal thread pools, it oversizes them, which further increases the likelihood of throttling.
10How large should the safety margin be for CPU limits?
A common practice is to set the limit based on measured real peak load with a margin of roughly 20 to 30 percent, similar to the approach used for memory limits, with multi-threaded applications tending to need a larger margin.