Skip to content
Documentation Background

Resource Management

Kubernetes resource management answers two questions:

  1. How much does this container need? (requests)
  2. How much can it take? (limits).

Getting these numbers right determines whether your pods schedule at all, whether they get evicted under pressure, and whether they throttle or crash under load.

Resource Governance Map

Kubernetes governs resources at three distinct levels:

  1. The Container (Micro): Granular requests and limits defined directly on individual workloads.
  2. The Automation (Enforcement): LimitRanges that automatically inject defaults or enforce min/max boundaries per container.
  3. The Namespace (Macro): ResourceQuotas that cap the aggregate consumption of a whole team or environment.

Kubernetes Resource Units

m = millicores = millicpu. 1000m is exactly one logical CPU (vCPU/hyperthread), regardless of the underlying hardware.

FormatMeaningEquivalent
250m250 millicores0.25 of one CPU core
500m500 millicores0.5 of one CPU core
11 whole core1000m
0.5decimal notation500m
22 whole cores2000m
FormatFull nameBytes
64Mi64 Mebibytes2²⁶ bytes
256Mi256 Mebibytes268,435,456 bytes
1Gi1 Gibibyte2³⁰ bytes
4Gi4 Gibibytes4,294,967,296 bytes
Resource typeExample spec fieldExample valueNotes
Ephemeral storageephemeral-storage4GiTemporary on-disk space (logs, caches)
Huge pageshugepages-2Mi60MiMust match page size configured on the node

Pod Requests

A resource request is the minimum guaranteed allocation for a container. It has two effects:

  1. Scheduling: kube-scheduler only places the pod on a node that has at least this much unallocated capacity
  2. QoS: the kubelet uses requests to classify pods into eviction priority tiers (see QoS section)

The scheduler sums requests across all containers in a pod (including sidecars) and finds a node with sufficient remaining capacity:

Pod with:
business-app → 256Mi + 1 CPU
sidecar → 64Mi + 250m
Scheduler looks for a node with at least:
→ 320Mi memory AND 1250m (1.25) CPU unallocated

If no node qualifies, the pod stays Pending.

spec:
containers:
- name: app
image: my-app:1.0
resources:
requests:
cpu: "250m" # scheduler uses this to find a node
memory: "256Mi" # scheduler uses this to find a node
ephemeral-storage: "1Gi"
RuleRationale
Always define CPU requestsScheduler can’t make accurate placement decisions without them
Always define memory requestsPrevents pod from landing on a node that will OOMKill it immediately
Set memory request = memory limitGives the container a guaranteed, non-overcommitted allocation (also achieves Guaranteed QoS)

Pod Limits

A resource limit is the maximum a container is allowed to consume at runtime. The container runtime enforces it — but enforcement differs by resource type.

ResourceWhat happens when the limit is exceeded
MemoryThe Linux kernel sends SIGKILL to the container process. Pod shows OOMKilled reason. Kubelet restarts it (based on restartPolicy).
CPUThe Linux CFS scheduler throttles the container — it can’t use more CPU than its limit for a given scheduling period. The process is not killed, but it slows down.
spec:
containers:
- name: app
image: my-app:1.0
resources:
requests:
memory: "256Mi"
cpu: "500m"
limits:
memory: "256Mi" # same as request → Guaranteed QoS
# cpu: "1" # intentionally omitted — avoids throttling
RuleRationale
Set memory limit = memory requestNon-overcommitted memory allocation; achieves Guaranteed QoS and prevents surprise OOMKills
Avoid CPU limitsCPU limits throttle the container even when the node has idle capacity — degrades latency without protecting anyone
Exception: CPU limits in strict multi-tenant environmentsPrevents a noisy neighbour from consuming all spare CPU on a shared node — benchmark before enabling

Kubernetes automatically assigns a QoS class to every pod based on its resource declarations. The class determines eviction priority when a node runs low on memory.

ClassCriteriaEviction priority
GuaranteedEvery container has both requests and limits for both CPU and memory, and request == limit for eachLast to be evicted
BurstableAt least one container has a CPU or memory request or limit defined, but not all containers have request == limit for both CPU and memoryMiddle
BestEffortNo container has any CPU or memory requests or limitsFirst to be evicted
# → Guaranteed: request == limit for ALL containers on BOTH CPU and memory
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "500m" # same as request
memory: "256Mi" # same as request
# → Burstable: at least one container has a request or limit, but pod is not Guaranteed
# (limits > requests, CPU limit omitted, or only some containers have resources set)
resources:
requests:
cpu: "250m"
memory: "128Mi"
limits:
memory: "256Mi" # memory limit != request; no CPU limit → not Guaranteed
# → BestEffort: no CPU or memory requests or limits on any container
resources: {}
Terminal window
# Verify a pod's assigned QoS class
kubectl describe pod <name> | grep "QoS Class"
# QoS Class: Guaranteed

Understanding requests, limits, and QoS is only half the battle — the harder question is what values to actually set. Get them wrong in either direction and you’ll hit scheduling failures, OOMKills, or wasted cluster capacity.

Resource sizing is an approximation. Setting them too low causes scheduling failures and OOMKills; too high wastes cluster capacity.

MethodWhen to use
kubectl top pods/nodesImmediate — shows real-time actual CPU/memory consumption; first step in any sizing investigation
Load testingPre-production — exercise realistic traffic patterns and measure peak CPU/memory consumption
Runtime monitoringPost-deployment — track actual vs. requested usage with Prometheus + Grafana or cloud monitoring
VPAIn-cluster — automatically adjusts requests and limits based on observed usage; Goldilocks runs VPA in recommendation-only mode under the hood
GoldilocksDashboard — runs VPA in recommendation mode and surfaces sizing suggestions without auto-applying them
KRRCLI — analyses historical Prometheus metrics and outputs concrete recommended requests/limits

Before 1.27, changing a container’s resource requests/limits required deleting and rescheduling the pod. From 1.27, container resize policies allow CPU and memory to be adjusted at runtime without a restart:

spec:
containers:
- name: app
resizePolicy:
- resourceName: cpu
restartPolicy: NotRequired # CPU can be resized without restart
- resourceName: memory
restartPolicy: RestartContainer # memory resize requires restart

All the settings so far — requests, limits, QoS — operate at the container level. Kubernetes also provides two mechanisms to govern resources at the namespace level, preventing any single team or workload from monopolising shared cluster capacity. ResourceQuota is the first of these.

A ResourceQuota is a namespace-level cap on aggregate resource consumption. It prevents one team or workload from monopolising a shared cluster.

Resource Quota
TypeExample constraintEffect
Object countpods: 2Maximum 2 pods in the namespace
Compute (requests)requests.cpu: "1"Sum of all pod CPU requests ≤ 1 core
Compute (limits)limits.memory: 4GiSum of all pod memory limits ≤ 4Gi
QoS classscopes: [BestEffort]Only BestEffort pods allowed
apiVersion: v1
kind: ResourceQuota
metadata:
name: backend-quota
namespace: backend-team
spec:
hard:
pods: "2" # max 2 pods in this namespace
requests.cpu: "1" # total CPU requests ≤ 1 core
requests.memory: 1Gi # total memory requests ≤ 1Gi
limits.cpu: "4" # total CPU limits ≤ 4 cores
limits.memory: 4Gi # total memory limits ≤ 4Gi
Terminal window
# Create namespace + apply quota
kubectl create namespace backend-team
kubectl apply -f backend-quota.yaml
# Check live usage vs hard limits
kubectl describe resourcequota backend-quota -n backend-team
# NAME AGE REQUEST LIMIT
# backend-quota 5m requests.cpu: 500m/1, ... limits.memory: 512Mi/4Gi

When a ResourceQuota constrains compute resources, every pod in that namespace MUST explicitly define those resources — or the API server rejects it:

# Pod with no resources block → rejected
Error from server (Forbidden): pods "nginx" is forbidden:
failed quota: backend-quota: must specify limits.cpu, limits.memory,
requests.cpu, requests.memory for: nginx

When cumulative usage would exceed a hard limit → rejected:

Error from server (Forbidden): pods "nginx3" is forbidden:
exceeded quota: backend-quota,
requested: pods=1,requests.cpu=500m,requests.memory=512Mi,
used: pods=2,requests.cpu=1,requests.memory=1Gi,
limited: pods=2,requests.cpu=1,requests.memory=1Gi

A LimitRange sets per-object (per-container or per-PVC) minimum, maximum, and default resource values. It complements ResourceQuota:

ResourceQuotaLimitRange
ScopeAggregate namespace totalsIndividual container / PVC
PurposeCap total cluster consumptionEnforce healthy per-object sizing
Effect on missing resourcesRejects the podInjects defaults automatically
apiVersion: v1
kind: LimitRange
metadata:
name: cpu-constraints
namespace: default
spec:
limits:
- type: Container # applies to each container in every pod
min:
cpu: "100m" # container cannot request less than 100m
max:
cpu: "2" # container cannot limit more than 2 cores
defaultRequest:
cpu: "200m" # injected if container omits requests.cpu
default:
cpu: "200m" # injected if container omits limits.cpu
Limit Range

If a container defines no CPU resource at all, the LimitRange automatically injects the defaultRequest and default values. The mutation is recorded in a pod annotation:

Terminal window
kubectl describe pod my-pod
# Annotations:
# kubernetes.io/limit-ranger: LimitRanger plugin set cpu request to 200m; cpu limit to 200m

If a container explicitly requests less than min or limits more than max, the pod is rejected immediately:

Error from server (Forbidden): pods "nginx" is forbidden:
[minimum cpu usage per Container is 100m, but request is 50m,
maximum cpu usage per Container is 2, but limit is 3]
  • One LimitRange per namespace — multiple LimitRanges cause non-deterministic default injection (unpredictable which one wins)
  • LimitRange changes do not affect already-running pods — only new pods created after the change

SymptomLikely causeDiagnosticFix
Pod stuck PendingRequests exceed available node capacitykubectl describe pod → events show PodExceedsFreeCPU or PodExceedsFreeMemoryLower requests, add nodes, or free capacity on existing nodes
Container keeps restarting, reason OOMKilledContainer exceeded its memory limitkubectl describe podLast State: OOMKilledIncrease memory limit; check for memory leak
Container slow under load, no crashCPU limit causing throttlingCheck CPU throttle metric (container_cpu_cfs_throttled_seconds_total)Remove CPU limit or raise it
Pod creation fails with Forbidden: must specify limits.cpuResourceQuota active; pod has no resource declarationskubectl describe resourcequota -n <ns>Add resources.requests and resources.limits to the pod spec
Pod creation fails with exceeded quotaNamespace has hit its hard capkubectl describe resourcequota → compare Used vs HardDelete unused pods, scale down deployments, or request quota increase
Pod creation fails with minimum cpu usage per Container is XmLimitRange min violatedkubectl get limitranges -n <ns> then describeRaise the container’s CPU request above the minimum
Unknown defaults injected into podLimitRange auto-mutationkubectl describe podkubernetes.io/limit-ranger annotationInspect the LimitRange; explicitly set resources if defaults don’t suit you