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.

ResourceNotationMeaning
cpu1, 0.5, 500m1 is 1 vCPU core, 500m is 0.5 core (millicore)
memory128Mi, 256Mi, 1GiMi = 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 ceiling

The 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.

ResourceBehavior on exceeding limitsResult
cputhrottle. cgroup shaves off CPU timeThe process doesn’t die, it slows down
memoryOOMKilled. The kernel OOM killer terminates the processContainer 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:    137

When 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 classConditionEviction priority
GuaranteedEvery container has requests and limits for both cpu and memory, and for each, requests == limitsLast (protected)
BurstableAt least one container has requests but the Guaranteed condition isn’t metMiddle
BestEffortNo container has any requests/limits at allFirst (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 requests

If 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}'
# Guaranteed

LimitRange: 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.

FieldRole
defaultThe default limit to fill in when a container omits limits
defaultRequestThe default request to fill in when a container omits requests
minThe minimum allowed (creation rejected if smaller)
maxThe 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 dev

ResourceQuota: 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      20

When 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.

  1. Namespace: create the isolation unit.
  2. LimitRange: enforce container defaults and min/max to prevent BestEffort from missing requests and to block an oversized single container.
  3. 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 edit or 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 OOMKilled and exit code 137 in describe, 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 (.) like requests.cpu accurately.
  • 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=20

Wrap-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.

X