Certified Kubernetes Administrator (CKA) #15 Resource Management: requests/limits, QoS, LimitRange, ResourceQuota
In #14 Scheduling 2, taints/tolerations and PriorityClass let us decide which Pod lands on which node at what priority. This post goes one step deeper into how much cpu and memory that Pod reserves and how much it is allowed to use. We’ll bind individual containers’ resources with requests/limits, understand which Pod gets evicted first when a node is under pressure via QoS classes, and enforce namespace-level operational policy with LimitRange and ResourceQuota.
This topic is the last piece of the Workloads and Scheduling domain, and it’s also the root cause of the OOMKilled and Pending you meet so often in troubleshooting. Without understanding resource settings, you can’t trace the OOM and Pending problems in #22 Troubleshooting 1 to the end.
requests and limits: reservation and ceiling #
In Kubernetes, a container’s resources are expressed with two values. requests is the reservation and limits is the ceiling. If you don’t clearly separate the two, both scheduling and troubleshooting wobble.
- requests: the minimum a container needs to operate normally. The scheduler only places a Pod on a node that has room for the requests sum within the node’s allocatable resources. In other words, requests is the basis of the scheduling decision.
- limits: the maximum a container can use. The runtime enforces this ceiling with cgroups. When a container tries to exceed limits, cpu gets throttled and memory gets the process killed.
The key point is that the scheduler places Pods based on requests only, not limits. The sum of limits can exceed node capacity and still be placed — this is called overcommit.
Unit notation #
cpu and memory use different unit notation.
| Resource | Notation | Meaning |
|---|---|---|
| cpu | 1, 0.5, 500m | 1 is 1 vCPU core, 500m is 0.5 core (millicore) |
| memory | 128Mi, 256Mi, 1Gi | Mi = mebibyte (1024², 2^20), Gi = gibibyte |
cpu can be sliced finely in millicore units, so 250m means 0.25 core. For memory, Mi and Gi (binary prefixes) differ from M and G (decimal prefixes), so in operations it’s safer to standardize on Mi and Gi to avoid confusion.
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: app
image: nginx
resources:
requests:
cpu: "250m" # reserve 0.25 core
memory: "128Mi" # reserve 128Mi
limits:
cpu: "500m" # 0.5 core ceiling
memory: "256Mi" # 256Mi ceilingThe container above reserves 0.25 core and 128Mi, and when load spikes it can use up to 0.5 core and 256Mi. The band between requests and limits is the burst headroom.
CPU throttle vs memory OOMKilled #
The behavior when limits is exceeded is fundamentally different for cpu and memory. This difference is the spot people get confused about most often in the exam and in operations.
| Resource | Behavior on exceeding limits | Result |
|---|---|---|
| cpu | throttle. cgroup shaves off CPU time | The process doesn’t die, it slows down |
| memory | OOMKilled. The kernel OOM killer terminates the process | Container restarts (per restartPolicy) |
cpu is a compressible resource. When a container hits its cpu limit, the kernel merely reduces the CPU time allocated to that container — it doesn’t kill the process. So when a cpu limit is too low, the symptom is that the app responds slowly instead of dying.
memory is an incompressible resource. There’s no way to reclaim already-allocated memory, so when a container exceeds its memory limit the kernel OOM killer terminates the process. At this point the status in kubectl describe pod shows OOMKilled, and depending on restartPolicy it leads to a restart and CrashLoopBackOff.
# Check OOMKilled
k describe pod web | grep -A3 "Last State"
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137When you see exit code 137 (= 128 + 9, SIGKILL), suspect a memory overrun. The fix is to raise the memory limit or reduce the app’s memory usage. We’ll revisit this pattern in #22 Troubleshooting 1.
QoS classes: who gets evicted first #
When a node runs short on memory, the kubelet evicts Pods to reclaim resources. Which Pod gets sent out first is decided by the QoS (Quality of Service) class. QoS isn’t a value you set directly — it’s automatically derived from how you set requests and limits.
| QoS class | Condition | Eviction priority |
|---|---|---|
| Guaranteed | Every container has requests and limits for both cpu and memory, and for each, requests == limits | Last (protected) |
| Burstable | At least one container has requests but the Guaranteed condition isn’t met | Middle |
| BestEffort | No container has any requests/limits at all | First (evicted first) |
When a node is under memory pressure, the kubelet evicts Pods in the order BestEffort → Burstable → Guaranteed. In other words, the more clearly a Pod has reserved resources, the more it is protected. The operational implication is clear: the more important the workload, the more you should specify requests and limits to keep it close to Guaranteed, so it survives node pressure.
How to make a Pod Guaranteed #
Guaranteed is granted when requests and limits are exactly equal in every container.
spec:
containers:
- name: app
image: nginx
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "500m" # same as requests
memory: "256Mi" # same as requestsIf you specify only limits and omit requests, Kubernetes auto-fills requests to match the limits value. So a Pod can reach Guaranteed even by writing only limits. Check the QoS class as follows.
k get pod web -o jsonpath='{.status.qosClass}'
# GuaranteedLimitRange: namespace defaults and bounds #
Writing requests/limits by hand on every individual Pod is impractical in operations, and omitting them makes a Pod BestEffort, which is risky. LimitRange is an object that enforces container defaults and an allowed range at the namespace level.
| Field | Role |
|---|---|
default | The default limit to fill in when a container omits limits |
defaultRequest | The default request to fill in when a container omits requests |
min | The minimum allowed (creation rejected if smaller) |
max | The maximum allowed (creation rejected if larger) |
apiVersion: v1
kind: LimitRange
metadata:
name: container-limits
namespace: dev
spec:
limits:
- type: Container
default: # value to fill when limits is unset
cpu: "500m"
memory: "256Mi"
defaultRequest: # value to fill when requests is unset
cpu: "250m"
memory: "128Mi"
min: # minimum allowed
cpu: "100m"
memory: "64Mi"
max: # maximum allowed
cpu: "1"
memory: "1Gi"In the dev namespace where this LimitRange applies, even if you create a Pod with resources left empty, defaultRequest and default get filled in automatically. Also, if you request a memory limit exceeding the max of 1Gi, the API server rejects the creation. Since LimitRange validates at creation time, it doesn’t apply retroactively to Pods already running before it was applied.
k apply -f limitrange.yaml
k describe limitrange container-limits -n devResourceQuota: namespace total limits #
If LimitRange is the boundary of a single container, ResourceQuota limits the total for the entire namespace. It’s a core multi-tenancy operations tool that caps a namespace so one team can’t monopolize cluster resources.
ResourceQuota limits two broad things.
- Resource totals: the sum of cpu,memory requests/limits across all Pods in the namespace
- Object counts: the number of Pods, Services, ConfigMaps, Secrets, PVCs, and so on
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: dev
spec:
hard:
# resource totals
requests.cpu: "4" # requests cpu sum max 4 cores
requests.memory: "8Gi" # requests memory sum max 8Gi
limits.cpu: "8" # limits cpu sum max 8 cores
limits.memory: "16Gi" # limits memory sum max 16Gi
# object counts
pods: "20" # max 20 Pods
services: "5"
configmaps: "10"
persistentvolumeclaims: "4"There’s one important constraint. In a namespace where ResourceQuota limits requests.* or limits.*, every container must specify the corresponding requests/limits. If they don’t, quota accounting is impossible, so Pod creation is rejected. That’s why the canonical practice is to use ResourceQuota paired with LimitRange. When LimitRange fills in defaults, even a Pod that forgot its requests/limits passes quota validation.
k apply -f resourcequota.yaml
k describe resourcequota team-quota -n dev
# Resource Used Hard
# -------- ---- ----
# requests.cpu 1500m 4
# requests.memory 3Gi 8Gi
# pods 7 20When Used reaches Hard, a Pod that requests more of that resource is rejected with a forbidden error. If creation is blocked by a quota overrun and the cause isn’t obvious, troubleshooting drags on — so when you encounter a Pending or a creation failure, it helps to make a habit of checking usage with describe resourcequota first.
Operational view: the namespace policy set #
In practice, when you create a new team namespace you apply these three things as a bundle.
- Namespace: create the isolation unit.
- LimitRange: enforce container defaults and min/max to prevent BestEffort from missing requests and to block an oversized single container.
- ResourceQuota: cap total namespace resources and object counts so one team can’t monopolize the cluster.
These three objects need to be present together for multi-tenancy to run stably. If you apply only ResourceQuota without LimitRange, every Pod that didn’t write requests is rejected and developers get confused; if you apply only LimitRange without ResourceQuota, the namespace total can grow without bound even when individual containers are reasonable.
The foundational concepts of this resource policy were first laid out from an introductory angle — requests/limits and QoS — in Kubernetes Intermediate #4, so if the concepts feel fuzzy, it helps to read it alongside this.
Exam points #
In CKA, this topic frequently shows up in the following forms.
- Adding requests/limits: a task asking you to specify cpu,memory requests/limits on an existing Deployment or Pod. Handle it with
k editor a manifest edit, and writing the unit notation (m,Mi,Gi) accurately is a grading point. - OOMKilled diagnosis: troubleshooting to find why a Pod keeps restarting. Confirm
OOMKilledand exit code137indescribe, then raise the memory limit. - LimitRange creation: a task to create a LimitRange with default/defaultRequest/min/max in a namespace. Don’t drop
type: Container. - ResourceQuota creation: a task to set a resource-total or object-count quota on a namespace. Write keys with a dot (
.) likerequests.cpuaccurately. - QoS class check: checking the class with
k get pod <name> -o jsonpath='{.status.qosClass}', or tuning requests/limits so a Pod lands in a specific QoS.
Learning the quick-creation commands too saves time.
# create a Pod with requests/limits specified
k run web --image=nginx \
--requests='cpu=250m,memory=128Mi' \
--limits='cpu=500m,memory=256Mi'
# quickly create a ResourceQuota on a namespace
k create quota team-quota -n dev \
--hard=requests.cpu=4,requests.memory=8Gi,pods=20Wrap-up #
What this post locked in:
- requests is the reservation, limits is the ceiling. The scheduler places based on requests only, and overcommit — where the sum of limits exceeds node capacity — is possible.
- cpu throttles, memory gets OOMKilled. cpu is a compressible resource so it just slows down without dying, while memory is incompressible and terminates (code 137) when the limit is exceeded.
- Three QoS classes. Protection strength increases in the order Guaranteed (requests==limits) , Burstable , BestEffort, and eviction starts from BestEffort.
- LimitRange. Enforces default , defaultRequest , min , max per container at the namespace level.
- ResourceQuota. Limits total namespace resources and object counts, and should be used paired with LimitRange to be stable.
Next: Storage 1 #
Now that we’ve bound cpu and memory, next is the storage that holds the data.
In #16 Storage 1: Volume types, PV, PVC static provisioning, we’ll work through it hands-on with manifests — from Volume types like emptyDir,hostPath, to static provisioning where you declaratively request and bind storage with PersistentVolume and PersistentVolumeClaim, and what difference accessModes and reclaim policy make.