| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Any host with kubectl configured; any Kubernetes cluster |
| Cert prep | CKA · CKAD |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm |
| Privilege | Normal user (no sudo required on the workstation) |
| Scope | ResourceQuota for namespace totals and object counts, LimitRange for per-container defaults and min/max bounds, admission failure diagnosis, and quota exhaustion behaviour. Does not cover cluster capacity planning, autoscaling, NetworkPolicy, RBAC, quota scopes, or detailed storage-class quotas. |
ResourceQuota and LimitRange both shape what can land in a namespace, but at different levels. ResourceQuota tracks totals across the namespace; LimitRange shapes individual Pods and containers before those totals are calculated. This walkthrough uses one isolated namespace, applies both policies, and deliberately triggers admission failures so you can read the exact error messages.
ResourceQuota vs LimitRange
Neither object applies cluster-wide. Each only affects the namespace where you create it.
| Resource | Controls |
|---|---|
| ResourceQuota | Aggregate resource use and object counts across one namespace |
| LimitRange | Defaults, minimums, and maximums for individual Pods, containers, or PVCs in one namespace |
Per-container requests and limits are covered in Kubernetes requests, limits and QoS. This lesson adds the namespace policies that wrap those fields.
Prepare the Namespace and Apply ResourceQuota
Create the namespace
I use namespace quota-demo so every manifest and command stays in one sandbox.
kubectl create namespace quota-demoSample output:
namespace/quota-demo createdThe namespace starts with no quota or limit policy until you apply the objects below.
Apply and inspect the quota
A ResourceQuota object sets hard caps on what the namespace may consume. This example limits CPU and memory requests and limits, Pod count, and Service count:
apiVersion: v1
kind: ResourceQuota
metadata:
name: ns-quota
namespace: quota-demo
spec:
hard:
requests.cpu: "500m"
requests.memory: 256Mi
limits.cpu: "1"
limits.memory: 512Mi
pods: "3"
services: "2"Apply the quota:
kubectl apply -f ns-quota.yamlSample output:
resourcequota/ns-quota createdWait for the quota status to initialize before reading used values:
kubectl wait resourcequota/ns-quota -n quota-demo --for='jsonpath={.status.hard.pods}=3' --timeout=60sList quotas in the namespace:
kubectl get resourcequota -n quota-demoSample output:
NAME REQUEST LIMIT AGE
ns-quota pods: 0/3, requests.cpu: 0/500m, requests.memory: 0/256Mi, services: 0/2 limits.cpu: 0/1, limits.memory: 0/512Mi 1sThe REQUEST and LIMIT columns are shorthand views. For the full ledger, use describe:
kubectl describe resourcequota ns-quota -n quota-demoSample output:
Name: ns-quota
Namespace: quota-demo
Resource Used Hard
-------- ---- ----
limits.cpu 0 1
limits.memory 0 512Mi
pods 0 3
requests.cpu 0 500m
requests.memory 0 256Mi
services 0 2Hard is the ceiling. Used is the current quota-accounted total for non-terminal Pod requests and limits, plus counted objects such as Pods and Services. Admission compares the requested increment against Hard - Used.
ResourceQuota sums requests and limits across non-terminal Pods.
Trigger missing-resource admission errors
With only ResourceQuota in place, try a Pod that omits resources:
apiVersion: v1
kind: Pod
metadata:
name: bare-pod
namespace: quota-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
restartPolicy: NeverSubmit the manifest:
kubectl apply -f bare-pod.yamlSample output:
Error from server (Forbidden): error when creating "bare-pod.yaml": pods "bare-pod" is forbidden: failed quota: ns-quota: must specify limits.cpu for: app; limits.memory for: app; requests.cpu for: app; requests.memory for: appThe quota tracks those four fields, but the Pod left them empty. The admission controller rejects the create before scheduling. This is not a scheduling problem—see Pod Pending and ContainerCreating when Pods stall after admission succeeds.
The fix is either add explicit resources to every container or add a LimitRange that injects defaults, which is the next step.
Apply LimitRange Defaults
Inject default requests and limits
LimitRange can supply defaults when a container omits resource fields:
apiVersion: v1
kind: LimitRange
metadata:
name: container-limits
namespace: quota-demo
spec:
limits:
- type: Container
default:
cpu: 200m
memory: 128Mi
defaultRequest:
cpu: 100m
memory: 64MiApply the LimitRange:
kubectl apply -f limitrange-defaults.yamlSample output:
limitrange/container-limits createdInspect the defaults:
kubectl describe limitrange container-limits -n quota-demoSample output:
Name: container-limits
Namespace: quota-demo
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container cpu - - 100m 200m -
Container memory - - 64Mi 128Mi -Inspect the admitted Pod
Re-apply the same bare Pod manifest:
kubectl apply -f bare-pod.yamlThe Pod is admitted. Check what the API stored:
kubectl get pod bare-pod -n quota-demo -o jsonpath='{.spec.containers[0].resources}{"\n"}'Sample output:
{"limits":{"cpu":"200m","memory":"128Mi"},"requests":{"cpu":"100m","memory":"64Mi"}}Your original YAML had no resources block. The admitted Pod spec shows values the LimitRange injected at admission time.
Enforce Minimum, Maximum, and Ratio Rules
Replace the defaults-only LimitRange with one that also sets bounds and a limit-to-request ratio:
apiVersion: v1
kind: LimitRange
metadata:
name: container-limits
namespace: quota-demo
spec:
limits:
- type: Container
min:
cpu: 50m
memory: 32Mi
max:
cpu: 300m
memory: 256Mi
default:
cpu: 100m
memory: 64Mi
defaultRequest:
cpu: 50m
memory: 32Mi
maxLimitRequestRatio:
cpu: 2
memory: 2Apply the updated LimitRange (this replaces the defaults-only object in the same namespace):
kubectl apply -f limitrange-minmax.yamlSample output:
limitrange/container-limits configuredReject requests below the minimum
LimitRange minimums validate requests. A Pod below the minimum request fails:
apiVersion: v1
kind: Pod
metadata:
name: tiny-pod
namespace: quota-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 20m
memory: 32Mi
restartPolicy: Neverkubectl apply -f tiny-pod.yamlSample output:
Error from server (Forbidden): error when creating "tiny-pod.yaml": pods "tiny-pod" is forbidden: [minimum cpu usage per Container is 50m, but request is 10m, minimum memory usage per Container is 32Mi, but request is 16Mi]Exact ordering may vary.
The resulting error focuses on the minimum CPU and memory requests. LimitRange minimums validate requests, maximums validate limits, and maxLimitRequestRatio validates the limit divided by the request.
Reject limits above the maximum
LimitRange maximums validate limits. Use values that remain inside ResourceQuota but violate LimitRange:
apiVersion: v1
kind: Pod
metadata:
name: huge-pod
namespace: quota-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
resources:
requests:
cpu: 100m
memory: 100Mi
limits:
cpu: 400m
memory: 300Mi
restartPolicy: Neverkubectl apply -f huge-pod.yamlSample output:
Error from server (Forbidden): error when creating "huge-pod.yaml":
pods "huge-pod" is forbidden:
[maximum cpu usage per Container is 300m, but limit is 400m,
maximum memory usage per Container is 256Mi, but limit is 300Mi,
cpu max limit to request ratio per Container is 2, but provided ratio is 4.000000,
memory max limit to request ratio per Container is 2, but provided ratio is 3.000000]Exact formatting and ordering may vary.
Handle defaults lower than explicit requests
When a container sets requests but omits limits, LimitRange supplies the default limit. If that limit is below the explicit request, admission fails. LimitRange does not verify that its injected defaults are consistent with explicitly supplied requests.
apiVersion: v1
kind: Pod
metadata:
name: high-req-pod
namespace: quota-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
resources:
requests:
cpu: 250m
memory: 128Mi
restartPolicy: NeverThe LimitRange injects limits of 100m and 64Mi, which are lower than the explicit requests:
kubectl apply -f high-req-pod.yamlSample output:
The Pod "high-req-pod" is invalid:
* spec.containers[0].resources.requests: Invalid value: "128Mi": must be less than or equal to memory limit of 64Mi
* spec.containers[0].resources.requests: Invalid value: "250m": must be less than or equal to cpu limit of 100mRaise the LimitRange default limit or lower the Pod request so request stays at or below limit.
LimitRange checks run per container before ResourceQuota totals are evaluated.
Demonstrate ResourceQuota Exhaustion
Fill the Pod and compute quotas
With bare-pod already admitted, add two more Pods each requesting 100m CPU / 64Mi memory and limiting 200m CPU / 128Mi memory. Save both in fill-pods.yaml:
apiVersion: v1
kind: Pod
metadata:
name: fill-pod-1
namespace: quota-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
restartPolicy: Never
---
apiVersion: v1
kind: Pod
metadata:
name: fill-pod-2
namespace: quota-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
restartPolicy: NeverApply both Pods:
kubectl apply -f fill-pods.yamlSample output:
pod/fill-pod-1 created
pod/fill-pod-2 createdWait for the quota ledger to reflect three Pods:
kubectl wait resourcequota/ns-quota -n quota-demo --for='jsonpath={.status.used.pods}=3' --timeout=60sCheck consumption:
kubectl describe resourcequota ns-quota -n quota-demoSample output:
Resource Used Hard
-------- ---- ----
limits.cpu 600m 1
limits.memory 384Mi 512Mi
pods 3 3
requests.cpu 300m 500m
requests.memory 192Mi 256Mi
services 0 2Let the updated LimitRange inject its current defaults for the next Pod:
apiVersion: v1
kind: Pod
metadata:
name: over-quota-pod
namespace: quota-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
restartPolicy: NeverThis keeps CPU and memory within quota, making pods=3 the only exceeded hard limit. Try to create it:
kubectl apply -f over-quota-pod.yamlSample output:
Error from server (Forbidden): error when creating "over-quota-pod.yaml": pods "over-quota-pod" is forbidden: exceeded quota: ns-quota, requested: pods=1, used: pods=3, limited: pods=3The three admitted, non-terminal Pods remain in the namespace. ResourceQuota blocks the additional create request but does not remove existing Pods.
Release quota and retry admission
Delete one Pod and wait for the quota ledger to update before retrying:
kubectl delete pod fill-pod-1 -n quota-demokubectl wait resourcequota/ns-quota -n quota-demo --for='jsonpath={.status.used.pods}=2' --timeout=60skubectl apply -f over-quota-pod.yamlThe create succeeds because used dropped below hard. kubectl wait supports JSONPath-based conditions and prints confirmation when the value is reached.
Limit Service and other object counts
ResourceQuota can cap API object counts. This lab already sets pods: "3" and services: "2".
Create two ClusterIP Services:
kubectl create service clusterip svc1 -n quota-demo --tcp=80:80Sample output:
service/svc1 createdkubectl create service clusterip svc2 -n quota-demo --tcp=8080:8080Sample output:
service/svc2 createdA third Service hits the quota:
kubectl create service clusterip svc3 -n quota-demo --tcp=9090:9090Sample output:
error: failed to create ClusterIP service: services "svc3" is forbidden: exceeded quota: ns-quota, requested: services=1, used: services=2, limited: services=2You can add persistentvolumeclaims the same way when you need to cap storage claims per namespace. Detailed storage-class quota keys are out of scope here; see Kubernetes persistent volumes for PVC basics.
Understand How ResourceQuota and LimitRange Interact
Admission applies namespace policy in a practical sequence:
- LimitRange may inject default requests and limits on containers that omit them.
- LimitRange validates per-container minimums, maximums, and limit-to-request ratios.
- ResourceQuota sums the resulting values (and object counts) against namespace hard limits.
- The API accepts or rejects the request.
The bare Pod example shows step 1 solving step 3: once LimitRange filled in requests.cpu and limits.cpu, the Pod satisfied the quota that previously returned must specify limits.cpu.
If a Deployment stops creating Pods after admission succeeds, check controller events in Deployment not creating Pods.
Existing workloads and policy changes
A few rules that catch people in production:
- Changing a LimitRange does not rewrite resources on Pods that are already running.
- Non-terminal Pods contribute their requests and limits to ResourceQuota
used. The displayed status can take a short time to reflect recent creates, updates, or deletions. - New creates and updates must satisfy the policy in force at admission time.
- Neither ResourceQuota nor LimitRange adds CPU or memory to nodes—they only constrain what the namespace may admit.
In-place resource resize
ResourceQuota does not resize running containers itself. It checks new objects and supported updates; Kubernetes also enforces ResourceQuota when an in-place Pod resize is requested, rejecting a resize that would exceed the namespace quota.
Kubernetes applies ResourceQuota and LimitRange constraints to supported in-place Pod resource resize requests.
Inspect hard and used values
A compact inspection set for any namespace:
kubectl get resourcequota,limitrange -n quota-demoSample output:
NAME REQUEST LIMIT AGE
resourcequota/ns-quota pods: 3/3, requests.cpu: 250m/500m, requests.memory: 160Mi/256Mi, services: 2/2 limits.cpu: 500m/1, limits.memory: 320Mi/512Mi 2m
NAME CREATED AT
limitrange/container-limits 2026-07-26T16:06:42Zbare-pod retained the original defaults, fill-pod-2 uses explicit resources, and over-quota-pod received the newer LimitRange defaults. Updating a LimitRange does not rewrite existing Pods.
Drill into the quota ledger:
kubectl describe resourcequota ns-quota -n quota-demoThen inspect the LimitRange defaults and bounds:
kubectl describe limitrange container-limits -n quota-demoThe describe output for LimitRange with min/max active looks like:
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container cpu 50m 300m 50m 100m 2
Container memory 32Mi 256Mi 32Mi 64Mi 2Diagnose Common Admission Errors
must specify limits.cpu
The ResourceQuota tracks CPU limits, but the container left limits unset and LimitRange did not supply a default. Add explicit resources or create a LimitRange with default and defaultRequest.
exceeded quota
The object would push namespace used above a hard value. Delete or scale down workloads, raise the quota, or reduce the Pod's requests and limits.
maximum cpu usage per Container
The container limit exceeds the LimitRange max for that resource. Lower the limit or relax the LimitRange maximum.
minimum memory usage per Container
The container request is below the LimitRange min. Raise the request or lower the LimitRange minimum.
Default limit is lower than an explicit request
When a container sets requests but omits limits, LimitRange supplies the default limit. If that limit is below the explicit request, admission fails. See the high-req-pod example earlier in this walkthrough.
What's Next
- Kubernetes Taints and Tolerations with Examples
- Kubernetes Scheduling, nodeSelector and Affinity
- Kubernetes Horizontal Pod Autoscaler with Examples
References
- Resource quotas — Kubernetes documentation
- Limit ranges — defaults and per-object bounds
- Configure default CPU requests and limits for a namespace — official LimitRange task
Summary
ResourceQuota caps what a namespace may consume in aggregate—CPU and memory requests and limits summed across Pods, plus counts of objects such as Pods and Services. LimitRange works on individual containers, injecting defaults when fields are missing and rejecting values outside configured minimums, maximums, or limit-to-request ratios.
The lab showed the natural order: a quota-only namespace rejected a bare Pod with must specify limits.cpu; adding LimitRange defaults let the same manifest through; min/max LimitRange rules produced targeted forbidden errors; and filling the Pod quota blocked further creates while three admitted Pods remained in the namespace. Neither policy changes node capacity—they only gate admission.
On exams, read the full forbidden message: must specify usually means missing fields, minimum or maximum points to LimitRange, and exceeded quota means namespace totals. Pair namespace policy with per-container requests and limits from Kubernetes requests, limits and QoS before you tune production namespaces.

