2:33 AM. This one is not actually an incident, it is a CI job screaming. A developer pushed a change to a Helm chart and the deploy pipeline has gone red on the kubectl apply step. The error message, if you squint at the pipeline logs, is short and angry. I have seen it enough times to know what it is before I even scroll. Limit less than request. The API server refused the pod at submission time, which is actually the friendliest failure mode Kubernetes has, because the cluster state never gets dirty. The bad manifest never becomes a live object.
Most of the complaints I get about Kubernetes error messages are fair. This one, honestly, is not. This one the API server gets right.
The scenario
git clone https://github.com/vellankikoti/troubleshoot-kubernetes-like-a-pro.git
cd troubleshoot-kubernetes-like-a-pro/scenarios/resource-requests-limits-mismatch
lsdescription.md, issue.yaml, fix.yaml, resource_mismatch.sh. The issue manifest sets CPU request to 500 millicores and CPU limit to 100 millicores. That is not a typo, that is the whole point of the scenario.
Reproduce the issue
kubectl apply -f issue.yamlThe Pod "resource-mismatch-pod" is invalid:
spec.containers[0].resources.requests: Invalid value: "500m":
must be less than or equal to cpu limitUnlike everything else in this series so far, there is no Pending pod to stare at. No describe to read. No events to parse. The API server rejected the manifest and nothing was created.
kubectl get pod resource-mismatch-podError from server (NotFound): pods "resource-mismatch-pod" not foundConfirm it does not exist. That is the signature of a validation failure: no object, not even a bad one.
Debug the hard way
The "debug" here is literally reading the error message out loud. spec.containers[0].resources.requests is the offending path, 500m is the offending value, and the rule is must be less than or equal to cpu limit. Three pieces of information in one line. Look at the manifest:
grep -A 6 resources issue.yamlresources:
requests:
memory: "200Mi"
cpu: "500m"
limits:
memory: "500Mi"
cpu: "100m"Request 500m, limit 100m. The request is the floor the scheduler guarantees, the limit is the ceiling the kubelet enforces. A floor above a ceiling is nonsense. The API server runs this check during admission and refuses.
If you want to see the rule in the API docs, the field is documented on the ResourceRequirements type. The admission controller that enforces it is called LimitRanger when a LimitRange is in play, but even without one, the core API server still runs the floor-below-ceiling check for you.
Why this happens
Requests and limits are two different things and most people conflate them. The request is what the scheduler uses to decide where a pod fits. It is a reservation. The kubelet uses it to set the cgroup's cpu.shares and the container's memory guarantee. The limit is the cap that the kernel enforces. For CPU it is a throttle, for memory it is a hard ceiling that, when crossed, gets you OOMKilled. Tomorrow's post is about that exact death.
When the limit is smaller than the request, the scheduler and the kernel would be operating on contradictory instructions. You would be telling the scheduler "reserve 500 millicores for me" and telling the kernel "never give me more than 100 millicores." The scheduler would find a node with 500m free, place the pod, and then the kernel would immediately throttle it down to 100. Kubernetes refuses to let you set up that trap.
The reason it is a friendly failure is that the rejection happens at kubectl apply, before any scheduling, before any image pull, before any pod ever exists. Your CI pipeline dies cleanly. There is no half-running workload to clean up.
The fix
kubectl apply -f fix.yaml
kubectl get pod resource-mismatch-fixed-podNAME READY STATUS RESTARTS AGE
resource-mismatch-fixed-pod 1/1 Running 0 4sThe diff that matters:
resources:
requests:
cpu: "500m"
limits:
cpu: "500m" # was "100m"Setting request and limit to the same value is the Guaranteed QoS class, which is the right default for most serious workloads. If you want burstable behaviour, set the limit higher than the request, never lower.
The lesson
- When
kubectl applyfails and no object is created, read the error message literally. It is almost always a validation rule and it will tell you the field. - Requests are a floor, limits are a ceiling. The floor can never be above the ceiling.
- Setting request equal to limit gives you the Guaranteed QoS class, which is the safest default for serious workloads.
Day 13 of 35 — tomorrow, the Linux kernel kills a container in cold blood, and we read the OOM score to find out why.
