DevOps in Production · Containers Under Load

Low CPU, High Latency

CPU limits are enforced per 100 ms period. Bursty services get throttled and p99 latency jumps even when average CPU looks low.

Create a free account to save this page, take notes and track your progress. Sign in

Scenario

Example team: an API pod has limits.cpu: 500m. Dashboards show average CPU usage around 30% of the limit, yet p99 latency is 400 ms while p50 is 20 ms. Adding replicas barely helps.

What you see in production

Latency spikes line up with bursts: a request that needs 80 ms of CPU sometimes takes 250 ms. The metric container_cpu_cfs_throttled_periods_total is climbing on every pod, but nobody alerts on it.

The naive code

resources:
  requests: { cpu: 250m }
  limits:   { cpu: 500m }    # 50 ms of CPU time per 100 ms period, across all threads

Reproduce it

The Linux CFS quota gives a container a CPU-time budget per 100 ms period. A burst of parallel work uses the whole budget early in the period, and the container is paused until the next period starts:

CFS quota throttling a burst of workTry it yourself
PERIOD_MS = 100

def run(cpu_needed_ms, threads, limit_cores):
    quota = limit_cores * PERIOD_MS            # CPU-ms allowed per period across all threads
    elapsed, remaining, throttled = 0, cpu_needed_ms, 0
    while remaining > 0:
        burst = min(remaining, quota, threads * PERIOD_MS)
        remaining -= burst
        busy_ms = burst / threads              # wall time to use the burst with N threads
        if remaining > 0:
            elapsed += PERIOD_MS               # wait for the next period
            throttled += 1
        else:
            elapsed += busy_ms
    return elapsed, throttled

for limit in [0.5, 1.0, 2.0, None]:
    if limit is None:
        print(f"no CPU limit    -> request takes {80 / 4:5.0f} ms, throttled 0 periods")
        continue
    wall, throttled = run(cpu_needed_ms=80, threads=4, limit_cores=limit)
    print(f"limit {limit:3} cores -> request takes {wall:5.0f} ms, throttled {throttled} period(s)")

Output

limit 0.5 cores -> request takes   108 ms, throttled 1 period(s)
limit 1.0 cores -> request takes    20 ms, throttled 0 period(s)
limit 2.0 cores -> request takes    20 ms, throttled 0 period(s)
no CPU limit    -> request takes    20 ms, throttled 0 periods

Root cause

CPU limits are enforced by the CFS bandwidth controller: once a container uses its quota in a 100 ms period, all its threads wait for the next period. Multi-threaded runtimes (JVM GC threads, Go's goroutines, Node's libuv pool) burn through quota quickly in bursts. Average utilisation hides it, because it's measured over seconds. Runtimes that size thread pools by the host's core count (older JVMs, Go before container-aware GOMAXPROCS) make it worse.

The fix

  • Set CPU requests from measurement and consider removing CPU limits for latency-sensitive services: requests guarantee a share when the node is busy, and without a limit the container can use idle CPU for bursts. Keep memory limits.
  • If limits are required (multi-tenant clusters, cost control), set them well above typical usage and alert on throttling.
  • Make runtimes container-aware: modern JVMs read the CPU quota; set GOMAXPROCS to match (newer Go versions do it automatically; older ones need automaxprocs).
resources:
  requests: { cpu: "1", memory: 1Gi }
  limits:   { memory: 1Gi }     # no CPU limit: bursts use spare node CPU

Trade-offs

  • Without CPU limits, a misbehaving pod can use spare CPU that other pods expected for their own bursts; requests still protect each pod's guaranteed share.
  • Higher requests reserve more capacity and cost more; that's the honest price of the latency you want.
  • Some platforms enforce limits by policy; work with the platform team on exceptions for latency-sensitive services.

Detect it in production

  • Throttling ratio: rate(container_cpu_cfs_throttled_periods_total) / rate(container_cpu_cfs_periods_total); alert above ~5-10% for latency-critical services.
  • Compare p99 latency with throttling; correlated spikes confirm the cause.
  • Per-container CPU usage at 1-second resolution during load tests.

Key points

  • CPU limits are a per-100 ms budget; bursts get paused even at low average usage.
  • Prefer accurate CPU requests and no CPU limit for latency-sensitive services; always keep memory limits.
  • Monitor throttled periods, not just CPU percentage.

Practice questions

Why can a container with low average CPU usage suffer CPU throttling?

Answer

Limits are enforced per 100 ms CFS period. A burst that uses the quota early in a period pauses all threads until the next period, even if average usage over seconds is low.

What's the difference between CPU requests and CPU limits in Kubernetes?

Answer

Requests are used for scheduling and guarantee a proportional share when CPU is contended; limits cap usage with CFS quotas and throttle the container when exceeded.

How do you detect CPU throttling?

Answer

Use the container_cpu_cfs_throttled_periods_total and container_cpu_cfs_periods_total metrics to compute the throttled ratio, and correlate it with latency percentiles.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?