Ch. 14 · Kubernetes

Kubernetes Requests, Limits and Scheduling

How CPU and memory requests drive scheduling, what limits enforce (throttling vs OOMKill), how QoS classes form, and how to choose numbers.

~6 min readbeginnerupdated Oct 7, 2026

Requests and limits are two numbers per resource per container, and they answer different questions. The request is what the scheduler reserves for you. The limit is what the kernel will let you use. Confusing the two causes most of the resource problems people bring to an interview: Pods stuck Pending on a cluster that looks half idle, latency spikes on a service using 30% CPU, and OOMKills that seem to come from nowhere.

Numbers and behaviour assume Kubernetes 1.30+ on Linux nodes using cgroup v2. The manifest parses as valid YAML, and the node and Pod output is a representative sample.

Units first

CPU is measured in cores. 1 is one vCPU or core, 500m is half of one, 100m is a tenth. Memory is in bytes, with binary suffixes Ki, Mi, Gi (powers of 1024) or decimal ones K, M, G (powers of 1000). 512Mi is 536,870,912 bytes; 512M is 512,000,000. The lowercase m on memory means milli-bytes, so 512m of memory is about half a byte, which Kubernetes accepts and nobody wants. It’s a real typo people make.

Requests are a scheduling reservation

Each node advertises allocatable resources: its capacity minus what’s reserved for the OS, the kubelet and the eviction threshold. The scheduler adds up the requests of every Pod already bound to a node and only places a new Pod if its requests fit in what’s left. It doesn’t look at how much CPU or memory the existing Pods are actually using.

$ kubectl describe node node-b | sed -n '/Allocatable:/,/pods:/p;/Allocated resources:/,/memory/p'
Allocatable:
  cpu:                3920m
  memory:             14Gi
  pods:               110
Allocated resources:
  Resource           Requests       Limits
  --------           --------       ------
  cpu                3600m (91%)    6 (153%)
  memory             9Gi (64%)      12Gi (85%)
Terminal

This node has 320m of CPU left to reserve. A Pod requesting 500m won’t fit here, even if kubectl top node node-b shows the machine at 20% CPU. That gap between reserved and used is why a cluster can be “full” while looking idle, and the scheduler’s event says so plainly: 0/6 nodes are available: 6 Insufficient cpu. The Pending Pod guide walks through reading that event.

Notice limits can exceed 100% of allocatable. Limits aren’t reserved; they’re ceilings, and a node can be overcommitted on them.

Requests also matter after scheduling. CPU requests become cgroup weights, so when the node’s CPU is contended, each container gets CPU time in proportion to its request. A container requesting 1 gets roughly ten times the share of one requesting 100m. When there’s no contention, either can use idle CPU freely.

Limits are enforced by the kernel, differently per resource

CPU limits throttle. A limit of 500m becomes a CFS quota: 50ms of CPU time per 100ms period. A container that uses its 50ms in the first 20ms of a period waits 80ms for the next one. Its average usage might be 25% of a core, but individual requests that land at the wrong moment wait. Multithreaded runtimes make this worse: a JVM with 8 threads can burn the 50ms quota in about 6ms of wall time. You see it in the nr_throttled and throttled_usec counters in the container’s cpu.stat, or the container_cpu_cfs_throttled_periods_total metric.

Memory limits kill. Memory can’t be throttled; it’s either available or not. When a container’s memory use hits its limit, the kernel OOM killer kills a process in that cgroup. Kubernetes reports the container as terminated with reason OOMKilled and exit code 137 (128 + SIGKILL’s 9), and restarts it. See the OOMKilled guide for diagnosing it.

That difference leads to the most common advice in this area: always set a memory limit (an unbounded leak would otherwise push the whole node into memory pressure), and consider leaving CPU limits off for latency-sensitive services, so they can burst into idle CPU without throttling. CPU requests still protect each container’s fair share under contention. Some teams keep CPU limits anyway, for predictable performance in multi-tenant clusters or for cost attribution, and that’s a defensible choice; the point is to make it knowingly.

A worked configuration

The catalog API was load-tested and measured at about 300m CPU and 380Mi memory at steady state, peaking at 900m and 520Mi during cache warm-up:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog
  namespace: shop
spec:
  replicas: 4
  selector:
    matchLabels:
      app: catalog
  template:
    metadata:
      labels:
        app: catalog
    spec:
      containers:
        - name: api
          image: registry.example.com/catalog:3.8.2
          resources:
            requests:
              cpu: 350m
              memory: 450Mi
            limits:
              memory: 768Mi
yaml

The reasoning behind each number:

  • CPU request 350m, a little above steady state, so the scheduler places Pods where that much is actually available and contention gives a fair share.
  • No CPU limit, so warm-up can burst to 900m when the node has idle CPU.
  • Memory request 450Mi, above steady state. Memory requests that are too low let the scheduler pack Pods onto a node that then runs out of real memory.
  • Memory limit 768Mi, about 50% above the measured peak, so a normal spike doesn’t kill the container but a leak gets stopped before it hurts neighbours.

For runtimes that size their heap from available memory, align the two. A JVM with -XX:MaxRAMPercentage=75 inside a 768Mi limit gets a heap of about 576Mi, leaving room for metaspace, threads and direct buffers. A fixed -Xmx1g in a 768Mi container is an OOMKill waiting to happen.

QoS classes and who gets evicted first

Kubernetes derives a Quality of Service class from how requests and limits are set, and the kubelet uses it when the node itself runs short of memory:

  • Guaranteed: every container has CPU and memory limits, and requests equal limits. Evicted last.
  • Burstable: at least one container has a request or limit, but not Guaranteed. The catalog Pod above is Burstable.
  • BestEffort: no requests or limits at all. Evicted first, and can be scheduled anywhere since it reserves nothing.
$ kubectl get pod -n shop -l app=catalog -o jsonpath='{.items[0].status.qosClass}'
Burstable
Terminal

Node-pressure eviction is different from an OOMKill. An OOMKill happens when a container crosses its own limit. Eviction happens when the node drops below its memory threshold (often 100Mi available); the kubelet then evicts Pods, starting with those whose usage exceeds their request, ranked by Pod priority and then by how far over their request they are. Evicted Pods show Status: Failed, Reason: Evicted and are replaced by their controller elsewhere. Pods using less than they requested are the safest, which is another reason honest memory requests matter.

If a namespace has a ResourceQuota on requests, Pods without requests are rejected outright. A LimitRange can fill in defaults so they’re not.

Interview practice

“A Go service with a CPU limit of 1 shows p99 latency of 400ms, but its dashboard says average CPU use is 35% of the limit. The node has spare CPU. What’s your hypothesis and how would you confirm it?”

Answer

CFS throttling. Average CPU says nothing about how usage is distributed inside each 100ms period. A Go program with GOMAXPROCS defaulting to the node’s core count (say 16) can run 16 threads in parallel, consume its 100ms quota in about 6ms of wall time, and then be frozen for the rest of the period. Requests caught in that window wait up to ~90ms, and a request that crosses several periods waits more, so p99 suffers while the average looks comfortable.

To confirm, I’d check the throttling counters: container_cpu_cfs_throttled_periods_total divided by container_cpu_cfs_periods_total for that container, or cat /sys/fs/cgroup/cpu.stat inside it to read nr_throttled. A high ratio confirms it. Fixes, roughly in order: set GOMAXPROCS to match the limit (Go 1.25 and later do this automatically from the cgroup limit; older versions can use automaxprocs), raise the limit, or remove the CPU limit and rely on a correctly sized request. I’d re-measure p99 after each change rather than assuming.

Further reading

More in Kubernetes

read ✓Kubernetes · mid

Kubernetes CronJobs and Overlapping Runs

What happens when a CronJob run outlasts its schedule: Allow, Forbid and Replace, startingDeadlineSeconds, and why the job must be idempotent.

~6 min readread →
esc