Kubernetes Requests and Limits

By Vishvesh Patel · DevOps EngineerUpdated September 12, 2026

Set CPU and memory requests and limits correctly. Enforcement behaviour, QoS classes, LimitRange defaults, and how to diagnose throttling and OOMKilled.

Every container can declare two numbers for CPU and two for memory. They look symmetrical and they are not: requests are a scheduling input, limits are a runtime constraint, and they are enforced by entirely different parts of the system.

The YAML

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments-api
spec:
  replicas: 3
  selector:
    matchLabels: { app: payments-api }
  template:
    metadata:
      labels: { app: payments-api }
    spec:
      containers:
        - name: api
          image: ghcr.io/acme/payments-api:1.4.2
          resources:
            requests:
              cpu: 200m
              memory: 256Mi
            limits:
              memory: 512Mi

Note the deliberate asymmetry: a memory limit but no CPU limit. That is a defensible default, for reasons below.

Resources are per container, not per Pod. A Pod's effective request is the sum across its containers. Init containers count too, taking the maximum rather than the sum, since they run sequentially.

Units

CPU is in cores, where m is millicores:

ValueMeaning
1 or 1000mOne core
500mHalf a core
100mA tenth of a core

Memory is in bytes, and the suffix matters:

SuffixMultiplier128 of it
Mi1024²134,217,728 bytes
M1000²128,000,000 bytes
Gi1024³()
G1000³()

Use Mi and Gi. The difference is about 5% at gigabyte scale, which is enough to turn a limit you calculated carefully into one that kills the process.

How each is enforced

Requests: the scheduler

The scheduler sums the requests of everything on a node and places your Pod where the remainder fits. It never looks at actual usage.

kubectl describe node ip-10-0-1-42 | grep -A8 "Allocated resources"
Allocated resources:
  Resource   Requests      Limits
  cpu        3800m (95%)   7200m (180%)
  memory     6.1Gi (81%)   12Gi (160%)

Requests at 95% means the node is full for scheduling purposes, whatever kubectl top says. Limits above 100% is normal overcommit.

CPU limits: throttling

Exceed a CPU limit and the kernel stops scheduling the container for the rest of the current 100ms period. Nothing crashes; things get slower.

kubectl exec -it <pod> -- cat /sys/fs/cgroup/cpu.stat
nr_periods 15420
nr_throttled 3891
throttled_usec 42150000

nr_throttled as a meaningful fraction of nr_periods means the limit is costing latency. This is worth checking whenever a service has unexplained tail latency, because it is invisible from inside the application. The process simply sees wall-clock time passing with no CPU.

Memory limits: the OOM killer

Exceed a memory limit and the container is killed immediately, with no signal and no chance to log.

kubectl get pod <name> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
OOMKilled

A Pod with a rising restart count and no errors in its own logs is almost always this.

QoS classes

Kubernetes derives a class from what you set, and it determines eviction order under node memory pressure.

ClassConditionEvicted
GuaranteedRequests equal limits, every container, both resourcesLast
BurstableRequests set, not equal to limitsMiddle
BestEffortNothing setFirst
kubectl get pods -o custom-columns=NAME:.metadata.name,QOS:.status.qosClass

Guaranteed requires both CPU and memory to have requests equal to limits, for every container. Setting only memory equal leaves you Burstable. This is the trade-off behind omitting CPU limits: you give up Guaranteed in exchange for no throttling.

For a critical, latency-sensitive service the choice is genuinely between the two. Pick deliberately rather than by accident.

Namespace defaults with LimitRange

A Pod submitted with no resources is BestEffort: scheduled carelessly, evicted first. A LimitRange stops that happening by accident:

apiVersion: v1
kind: LimitRange
metadata:
  name: defaults
  namespace: payments
spec:
  limits:
    - type: Container
      defaultRequest:        # used when request is unset
        cpu: 100m
        memory: 128Mi
      default:               # used when limit is unset
        cpu: 500m
        memory: 512Mi
      max:
        cpu: "4"
        memory: 8Gi
      min:
        cpu: 10m
        memory: 16Mi

defaultRequest and default fill in missing values; max and min reject Pods outside the range at admission.

A subtlety worth knowing: if a LimitRange sets a default limit but no defaultRequest, Kubernetes sets the request equal to the limit. That can quietly make every Pod in the namespace request far more than it needs.

ResourceQuota

Where LimitRange constrains individual containers, ResourceQuota caps the namespace as a whole:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-quota
  namespace: payments
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    pods: "50"
kubectl describe resourcequota team-quota -n payments

Once a quota constrains a resource, specifying it becomes mandatory. A Pod without a memory request is rejected outright. Always pair a ResourceQuota with a LimitRange, or every existing manifest in the namespace stops deploying.

Choosing values

kubectl top pods --containers -n payments
POD                      NAME   CPU(cores)   MEMORY(bytes)
payments-api-7d4f-x8k2   api    143m         310Mi
payments-api-7d4f-m3p9   api    156m         298Mi

A workable starting point:

Set it to
Memory requestSteady-state usage + ~25%
Memory limitRequest + headroom for genuine peaks
CPU requestObserved average, not peak
CPU limitWell above peak, or omit for latency-sensitive services

kubectl top requires metrics-server and shows only a moment in time. For real sizing use Prometheus history, or run Vertical Pod Autoscaler in Off (recommendation) mode, which observes usage and suggests values without changing anything.

One language-specific trap: runtimes that manage their own heap, the JVM above all, but also Node and .NET, need to be told about the container limit. A JVM that sizes its heap against the node's 64GB while the container limit is 512Mi will be OOMKilled reliably. Modern JVMs handle this with -XX:MaxRAMPercentage; older ones need explicit -Xmx.

Quick reference

TaskCommand
Current usagekubectl top pods --containers
Node allocationkubectl describe node <n> | grep -A8 "Allocated resources"
QoS classkubectl get pod <n> -o jsonpath='{.status.qosClass}'
OOM checkkubectl get pod <n> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
Throttlingkubectl exec -it <n> -- cat /sys/fs/cgroup/cpu.stat
Quota usagekubectl describe resourcequota -n <ns>
Namespace defaultskubectl describe limitrange -n <ns>

Key takeaways

  • Requests schedule, limits constrain. Different mechanisms, different failure modes.
  • Mi not M: the 5% difference is enough to matter on a tight limit.
  • CPU throttles, memory kills. Silent periodic restarts with clean logs mean OOMKilled.
  • Guaranteed needs requests equal to limits for CPU and memory, every container.
  • Omitting CPU limits avoids throttling but forfeits Guaranteed. Choose deliberately.
  • A default limit with no defaultRequest sets the request equal to the limit, silently inflating reservations.
  • ResourceQuota makes requests mandatory: always pair it with a LimitRange.
  • Tell the JVM about the container limit, or it sizes its heap against the node.

Check your understanding

Exercise

Almost Guaranteed

A container sets memory request and limit both to 512Mi, and a CPU request of 200m with no CPU limit. What QoS class does the Pod get?

Exercise

Deploys stop after a quota is added

A platform team adds a ResourceQuota with requests.memory to a namespace. Existing Deployments that previously worked now fail to create Pods. Why?


Frequently Asked Questions

What is the difference between requests and limits in Kubernetes?

A request is what the scheduler reserves when deciding which node can host a container. It is pure accounting and the scheduler never consults actual usage. A limit is the ceiling enforced at runtime by the kernel through cgroups. The consequences differ by resource: exceeding a CPU limit throttles the container, while exceeding a memory limit terminates it. Requests also determine QoS class, which decides eviction order under node pressure.

Why does my container keep getting OOMKilled?

It exceeded its memory limit and the kernel killed it without warning, which is why the application's own logs show nothing. Confirm with kubectl get pod <name> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'. Measure real usage with kubectl top pods --containers before simply raising the limit, if usage grows without bound it is a leak that a larger limit only delays. For JVM, Node or .NET applications, check that the runtime knows about the container limit, since a heap sized against the node's total memory will exceed a small container limit every time.

Should I set CPU limits on my containers?

It is a genuine trade-off rather than a settled best practice. Limits prevent one workload starving its neighbours and are required for the Guaranteed QoS class, but they throttle rather than kill, and throttling adds latency that is very hard to see from inside the application. Multi-threaded runtimes reach the quota quickly because it is consumed across all threads within each 100ms window. A common position is to always set CPU requests and memory limits, and either omit CPU limits on latency-sensitive services or set them comfortably above observed peak.

What is the difference between Mi and M?

Mi is mebibytes, a multiple of 1024², and M is megabytes, a multiple of 1000². So 128Mi is 134,217,728 bytes while 128M is 128,000,000, about 5% apart, which grows to a meaningful gap at gigabyte scale. Kubernetes accepts both, but the binary suffixes Ki, Mi and Gi match how memory is actually reported by tools and by the kernel, so mixing them up can turn a carefully sized limit into one that triggers the OOM killer.

How do I know what values to use?

Measure before guessing. kubectl top pods --containers gives a current snapshot but misses daily peaks, so Prometheus history is better if you have it. Vertical Pod Autoscaler in recommendation mode watches real usage over time and proposes values without modifying anything, which is a safe way to validate your numbers. As a starting point, set memory request to steady-state usage plus around 25%, memory limit with real headroom above that, and CPU request to the observed average rather than peak.

What happens if I set no requests or limits at all?

The Pod becomes BestEffort, which is the worst of both worlds. The scheduler believes it needs nothing and will place it on an already-loaded node, and under memory pressure the kubelet evicts BestEffort Pods first. The container is also uncapped, so a memory leak can consume the node and take down everything else running on it. A LimitRange in the namespace with defaultRequest and default values prevents this happening by accident.

Continue Learning

Explore Related Topics

Try the Tool

Practise this in the browser

Kubernetes Playground

Practise kubectl against a simulated cluster in your browser.

When it breaks

Related Resources