Step 1: Admission: the rules before scheduling
The first layer of rules — admission
Imagine you and four teammates share a single Kubernetes cluster — say, 16 CPU cores and 64 GiB of RAM total, with two GPUs. Everyone deploys whatever they want. Whose pods get the resources? Whose get killed when memory runs out?
Without rules, the answer is: whichever pod was scheduled first usually wins, and whichever pod the kernel happens to pick when memory runs out is the one that dies. That's a recipe for late-night pages.
This is the problem resource requests and resource limits solve. But before any pod even reaches the scheduler, your cluster admins have probably set up two earlier checkpoints that filter out bad submissions: LimitRange and ResourceQuota.
The three layers a pod passes through
When you kubectl apply a pod, it goes through three checks before running:
- LimitRange — a per-namespace policy that:
- Fills in default
requestsandlimitsfor any container that didn't specify them. - Rejects the pod if any container asks for more than the per-container max.
- Fills in default
- ResourceQuota — a per-namespace policy that sums all pods' requests and limits and rejects new pods that would push the namespace over its hard cap.
- Scheduler — only after admission does the scheduler look for a node with room.
Your lab namespace has both. Take a look at the actual numbers:
kubectl get limitrange -o yaml
kubectl get resourcequota -o yaml
Two things to find: the LimitRange's default / defaultRequest / max for cpu and memory, and the ResourceQuota's requests.cpu cap. Hold those in your head — you'll watch them in action below.
Demo 1 — a pod with no rules
Open unbounded.yaml. The pod has no resources: block at all. Apply it as-is:
kubectl apply -f unbounded.yaml
Now look at what actually got admitted:
kubectl get pod unbounded -o jsonpath='{.spec.containers[0].resources}'; echo
You didn't write a resources block — but there's one now. Compare its values against the LimitRange's default and defaultRequest. That's the LimitRange filling in the gaps at admission time.
Then ask Kubernetes what QoS class the pod landed in:
kubectl get pod unbounded -o jsonpath='{.status.qosClass}'; echo
We'll dig into what QoS class means in step 4 — for now, just notice the LimitRange has converted "no rules" into "default rules." A textbook BestEffort class (no requests, no limits anywhere) would only happen in a namespace without a LimitRange. In well-run production clusters, BestEffort is rare.
Demo 2 — a pod that asks for too much
Open over-max.yaml. It has TODOs to fill in — request and limit both set to a large CPU value (5 cores). The LimitRange max is 2 cores. Try to apply:
kubectl apply -f over-max.yaml
This will fail. The error message tells you which clause rejected it and what the limit is. The pod is never admitted — it won't show up in kubectl get pods. There's no "Pending" intermediate state, just a hard rejection at submission.
Why this matters in practice: LimitRange and ResourceQuota are the two main reasons a developer can't deploy without anyone touching the workload itself. If a pod is rejected with
exceeded quotaormaximum cpu usage per Container, the fix isn't a bigger cluster — it's a smaller request, or a conversation with the namespace owner about the cap. Recognizing these admission errors saves hours of staring at the wrong layer.
When you've verified unbounded is running with injected defaults and over-max was rejected at admission, click Run.
over-max.yaml, the file you edit20 lines
# This pod intentionally requests more CPU than the namespace
# LimitRange max allows. Fill in the TODOs and apply to observe
# the rejection at admission.
apiVersion: v1
kind: Pod
metadata:
name: over-max
spec:
restartPolicy: Never
containers:
- name: greedy
image: docker.io/library/busybox:1.36
command: ["sleep", "60"]
resources:
requests:
# TODO: request 5 cpu cores (over the LimitRange max of 2)
cpu: ______
limits:
# TODO: limit also 5 cpu cores
cpu: ______provided for you:unbounded.yaml