Hedronite · Cert Lesson · CNCF CKA · Tue 2026-10-06

CKA ResourceQuota versus LimitRange the pod refused at admission and the defaults that admit it

The quota asks whether the namespace can afford the pod. The LimitRange writes the numbers the pod forgot.

Lesson Class: Cert (T2 · CKA admission and resource management)
Referent: ResourceQuota + LimitRange · real k3s v1.34.12 transcripts
Not a repeat: not 09-30 HPA · not 09-24 storage · not 10-03 CKS attestation
Paired Ops: AKS managed namespace + census
Paired Dev: typed Quantity fit check
Read the Verb
must specify points at the container. exceeded points at the sum.
Mutate, then validate
LimitRanger fills before ResourceQuota counts.
Deployment hides it
0/N with no pods: read the ReplicaSet events.
A ceiling-only LimitRange hands every bare container its ceiling as a request.

<!-- hal:authoritative:yaml -->

The quota asks whether the namespace can afford the pod. The LimitRange writes the numbers the pod forgot. Read the error to learn which one spoke.

§I. Frame

CKA day, even counter. Recent CKA lessons took HPA (09-30), storage binding (09-24), Gateway API (09-18), and NetworkPolicy (09-09). Admission came up on 10-03 as a CKS attestor. Today stays on two admission plugins that kube-apiserver enables by default: LimitRanger, which mutates, and ResourceQuota, which validates. Mutation runs before validation, which is why a LimitRange can rescue a pod the quota would refuse.

Every transcript below is real output from a local k3s v1.34.12 cluster run for this lesson.

§II. Two objects, two jobs

ResourceQuotaLimitRange
ScopeSum across the namespaceOne container, pod, or PVC
Keysrequests.cpu, limits.memory, pods, count/...default, defaultRequest, min, max, maxLimitRequestRatio
EffectRefuses a create that breaks the sumFills missing values, refuses outliers
WhenAdmission, plus a live used tallyAdmission only

Neither object changes pods that already exist.

§III. The refusal

Namespace tenant-a, quota compute-quota with requests.cpu: 2, requests.memory: 2Gi, limits.cpu: 4, limits.memory: 4Gi, pods: 10. No LimitRange yet. A pod with no resources block:

$ kubectl apply -f bare-pod.yaml
Error from server (Forbidden): error when creating "bare-pod.yaml": pods "bare" is forbidden: failed quota: compute-quota: must specify limits.cpu for: app; limits.memory for: app; requests.cpu for: app; requests.memory for: app

A pod with requests but no limits:

pods "requests-only" is forbidden: failed quota: compute-quota: must specify limits.cpu for: app; limits.memory for: app

Read the Verb (named technique). failed quota ... must specify means the quota meters a key the container left empty. exceeded quota means the sum would pass hard. They need different fixes. The first needs a value on the container. The second needs smaller requests or a larger quota.

The quota demands each key it lists. Listing limits.* makes limits mandatory even when requests are set.

§IV. The fix: LimitRange defaults

apiVersion: v1
kind: LimitRange
metadata: {name: tenant-defaults, namespace: tenant-a}
spec:
  limits:
  - type: Container
    defaultRequest: {cpu: 250m, memory: 256Mi}
    default: {cpu: 500m, memory: 512Mi}
    max: {cpu: "1", memory: 1Gi}

The same two manifests now pass. The bare pod got every value from the LimitRange, and the plugin left an annotation:

{"limits":{"cpu":"500m","memory":"512Mi"},"requests":{"cpu":"250m","memory":"256Mi"}}
kubernetes.io/limit-ranger: LimitRanger plugin set: cpu, memory request for container app; cpu, memory limit for container app

The requests-only pod kept its own requests and took only the default limits. Then:

$ kubectl describe quota compute-quota -n tenant-a
Resource         Used   Hard
--------         ----   ----
limits.cpu       1      4
limits.memory    1Gi    4Gi
pods             2      10
requests.cpu     500m   2
requests.memory  512Mi  2Gi

§V. Exceeded quota, and where a Deployment hides it

After a third pod at 1 CPU / 1Gi, a fourth identical pod:

pods "big2" is forbidden: exceeded quota: compute-quota, requested: requests.cpu=1,requests.memory=1Gi, used: requests.cpu=1500m,requests.memory=1536Mi, limited: requests.cpu=2,requests.memory=2Gi

A Deployment does not show that error at create time. In tenant-c (quota requests.cpu: 1, 900m used), kubectl create deployment api printed created. Then:

$ kubectl get deploy api -n tenant-c
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
api    0/1     0            0           5s
$ kubectl describe rs -n tenant-c
Warning  FailedCreate  4s    replicaset-controller  Error creating: pods "api-9599f5f64-mmwx6" is forbidden: exceeded quota: compute-quota, requested: requests.cpu=1,requests.memory=1Gi, ...

0/1 with no pods means look at the ReplicaSet events, not the pods.

Why did api ask for a full CPU when its manifest set nothing? tenant-c had a LimitRange written with only max: {cpu: "1", memory: 1Gi}. The API server filled default from max and defaultRequest from default. kubectl get limitrange -o yaml shows all three at 1 and 1Gi. A ceiling-only LimitRange hands every bare container its ceiling as a request.

§VI. Scopes

A quota can meter a subset of pods. scopeSelector.matchExpressions takes a scopeName: BestEffort, NotBestEffort, Terminating (has activeDeadlineSeconds), NotTerminating, PriorityClass (with In/NotIn values), CrossNamespacePodAffinity, or VolumeAttributesClass for PVCs. A PriorityClass In [high] quota counts only pods with priorityClassName: high. kubectl describe quota lists every quota in the namespace, one table each.

§VII. Drill

  1. must specify requests.memory for: app. Which object, which fix? Quota. Set requests.memory on the container or add a LimitRange defaultRequest.
  2. Deployment 0/3, no pods listed. First command? kubectl describe rs (or kubectl get events) for FailedCreate.
  3. A LimitRange is added with defaults. Do running pods change? No. Admission only.
  4. A LimitRange sets only max: {cpu: 2}. What request does a bare container get? 2, through default then defaultRequest.
  5. Quota lists requests.cpu only. Pod sets limits.cpu: 500m and no request. Admitted? Yes. The request defaults to the limit, so requests.cpu is stated.

§VIII. Close

Two plugins, two jobs. LimitRanger fills and bounds each container. ResourceQuota meters the namespace and refuses the create that breaks it. Read the Verb: must specify points at the container, exceeded points at the sum. On a Deployment, the ReplicaSet events hold the answer.

Paired Ops declares an AKS managed namespace and censuses the gaps. Paired Dev computes used + candidate <= hard with a typed Quantity and matches the big2 refusal above.

Related