A Kubernetes namespace with no ResourceQuota is an open floor: any deployment in it can request as much CPU or memory as the node scheduler will allow, and a bad rollout or a runaway job can starve every other workload sharing that cluster. ResourceQuota and LimitRange are the two objects that turn a shared cluster from "whoever asks for the most wins" into something with actual, enforced boundaries per team or per environment.
What ResourceQuota actually constrains
A ResourceQuota is a namespaced object that caps aggregate resource consumption and object counts across everything in that namespace: total CPU and memory requests/limits, the number of Pods, Services, PersistentVolumeClaims, ConfigMaps, and Secrets, and even storage class-specific quotas for PVCs. It doesn't cap any single pod — a pod can still request more than its fair share as long as the namespace total stays under quota. That's the key thing people get wrong: quota is an aggregate ceiling, not a per-pod limit. Per-pod limits are LimitRange's job.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-checkout-quota
namespace: checkout
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
pods: "50"
services: "10"
persistentvolumeclaims: "20"
count/deployments.apps: "15"
Why LimitRange usually has to come first
The moment you apply a ResourceQuota that includes requests.cpu or requests.memory, every pod created afterward in that namespace is required to specify resource requests — the API server rejects any pod spec that omits them, because otherwise it has nothing to subtract from the quota. If your existing deployments were written without requests and limits (common in namespaces that grew organically), applying quota first will start failing every new rollout with an admission error. A LimitRange fixes this by defining defaults: if a container doesn't specify requests/limits, the LimitRange injects them automatically, so existing manifests keep working while the quota is enforced underneath.
apiVersion: v1
kind: LimitRange
metadata:
name: checkout-defaults
namespace: checkout
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
max:
cpu: "2"
memory: 2Gi
min:
cpu: 50m
memory: 64Mi
Apply the LimitRange before the ResourceQuota in a namespace that has existing workloads without explicit requests/limits. Doing it the other way round means the next deploy, restart, or scale event for any pod missing requests will be rejected by the API server until someone notices and patches the manifests.
Scoping quota to priority classes and QoS
ResourceQuota supports scopes and scopeSelector, which let you carve out separate quotas for different slices of the same namespace — for example, a stricter quota for BestEffort pods versus Guaranteed ones, or a quota that only applies to pods with a given PriorityClass. This is the mechanism for saying "batch jobs in this namespace can burst up to X, but the always-on API service has its own separate, protected ceiling" without splitting the workloads into different namespaces. It's more setup than a single flat quota, and most teams don't need it until they're running mixed batch-and-serving workloads in the same namespace.
What happens when quota is hit
Hitting a ResourceQuota limit doesn't evict or throttle running pods — it blocks the creation of new objects. A kubectl apply or a Horizontal Pod Autoscaler trying to scale up will get an admission error (exceeded quota) and the object simply won't be created. This is usually the first place teams discover a quota exists: an autoscaler silently stops scaling, or a CI pipeline's deploy step starts failing, and the actual error is buried in an event rather than surfaced anywhere obvious. kubectl describe resourcequota -n <namespace> and kubectl get events -n <namespace> are the first two commands worth running when a namespace stops accepting new pods for no visible reason.
Run kubectl describe resourcequota against a namespace's current usage before tightening its hard limits. Quotas set from a guess rather than observed usage tend to either block legitimate scaling within a month, or sit so loose they don't actually protect the cluster from anything.
Rolling quotas into a cluster that doesn't have them yet
Retrofitting quotas onto a cluster that's been running quota-free for a while is mostly a communication problem, not a technical one. Start by measuring actual usage per namespace over a couple of weeks with kubectl top pods or your metrics stack, set quotas at roughly 1.5-2x observed peak rather than at whatever number seems safe in the abstract, and land the LimitRange first as described above. Expect the first month to surface a handful of namespaces where someone's deployment has no resource requests at all and was quietly consuming far more than anyone realized — that's usually the actual value of doing this exercise, independent of the quota enforcement itself.
Wrapping up
ResourceQuota caps aggregate namespace consumption and object counts; LimitRange fills in per-container defaults and per-container min/max so that quota enforcement doesn't break workloads that never specified requests in the first place. Land the LimitRange before the ResourceQuota, base the numbers on measured usage rather than guesses, and expect `kubectl describe resourcequota` plus namespace events to be your primary debugging tool the first time a rollout gets silently blocked.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.