LimitRange, ResourceQuota, and VPA: controlling resource usage per namespace
Published: 2026-04-21
Kubernetes doesn't enforce resource limits by default. A misconfigured app can consume all node memory and evict everything else. Three tools prevent this: LimitRange (defaults per container), ResourceQuota (caps per namespace), and VPA (recommendations for right-sizing).
LimitRange
Injects default requests/limits into containers that don't specify them, and caps what any single container can request:
yamlapiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: dev
spec:
limits:
- type: Container
default: # applied when limits: not set
cpu: "500m"
memory: "512Mi"
defaultRequest: # applied when requests: not set
cpu: "100m"
memory: "128Mi"
max: # container can't exceed these
cpu: "4"
memory: "4Gi"
min: # container must request at least this
cpu: "50m"
memory: "64Mi"
- type: Pod
max:
cpu: "8"
memory: "8Gi"
- type: PersistentVolumeClaim
max:
storage: "50Gi"
Without this, a pod without any resources: block gets scheduled with zero requests — the scheduler doesn't account for it, nodes get overcommitted, and OOM kills happen.
Verify LimitRange is working:
bashkubectl describe limitrange default-limits -n dev
ResourceQuota
Caps total resource consumption in a namespace:
yamlapiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "20"
requests.memory: "20Gi"
limits.cpu: "40"
limits.memory: "40Gi"
pods: "100"
services: "30"
services.loadbalancers: "0"
persistentvolumeclaims: "20"
requests.storage: "200Gi"
count/secrets: "100"
count/configmaps: "100"
services.loadbalancers: "0" prevents developers from accidentally creating LoadBalancer services (which on cloud providers provision real load balancers and incur cost).
When a namespace has a ResourceQuota, every pod must have resource requests defined. The LimitRange default handles this automatically.
Per-environment quota sizing
yaml# dev namespace — generous, fast iteration
limits.cpu: "40"
limits.memory: "40Gi"
# test namespace — production-like but smaller
limits.cpu: "20"
limits.memory: "20Gi"
# sre namespace — minimal, just enough for tools
limits.cpu: "8"
limits.memory: "8Gi"
Stored in spoke/namespaces/ as separate YAML files per namespace. Kustomize patches override values per environment.
VPA for right-sizing recommendations
The Vertical Pod Autoscaler in Off mode computes recommendations without changing anything:
yamlapiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: elasticsearch
namespace: observability
spec:
targetRef:
apiVersion: apps/v1
kind: StatefulSet
name: elasticsearch-master
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: elasticsearch
minAllowed:
cpu: 100m
memory: 1Gi
maxAllowed:
cpu: 8
memory: 16Gi
After a few days of traffic, check recommendations:
bashkubectl describe vpa elasticsearch -n observability
Example output:
Container Recommendations:
Container Name: elasticsearch
Lower Bound:
cpu: 216m
memory: 2Gi
Target:
cpu: 500m
memory: 3Gi
Upper Bound:
cpu: 2
memory: 5Gi
Use Target values to update the HelmRelease. Don't use updateMode: Auto for stateful apps — VPA evicts pods to apply changes, causing downtime.
Checking quota usage
bashkubectl describe resourcequota -n dev
Name: dev-quota
Namespace: dev
Resource Used Hard
-------- ---- ----
limits.cpu 12500m 40
limits.memory 14Gi 40Gi
pods 47 100
requests.cpu 3200m 20
requests.memory 5Gi 20Gi
services.loadbalancers 0 0
When a deployment fails with exceeded quota, this view immediately shows which resource is exhausted.
Troubleshooting quota errors
Pod fails to create: pods "name" is forbidden: exceeded quota
bashkubectl describe resourcequota -n <namespace>
# Find which resource is at limit
# Either increase quota or remove unused workloads
Pod fails: must specify limits.memory — ResourceQuota exists but LimitRange is missing. Add LimitRange with defaults.
Helm upgrade fails with quota error — The HelmRelease adds new pods before removing old ones during rolling update. Temporarily increase pod count quota, or add terminationGracePeriodSeconds to speed up pod termination.
Priority classes
For critical system workloads that should survive eviction:
yamlapiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: critical-addons
value: 100000000
preemptionPolicy: PreemptLowerPriority
globalDefault: false
description: "Used for critical cluster addons"
Apply to pods:
yamlspec:
priorityClassName: critical-addons
Pods with higher priority are evicted last when a node runs out of memory.
Summary
LimitRangesets per-container defaults and maximums — without it, containers withoutresources:run unboundedResourceQuotacaps total CPU and memory per namespace — forces developers to declare resource requests before pods can start- VPA recommends right-sized requests based on actual usage; it doesn't auto-apply unless
updateMode: Autois set - PriorityClass prevents critical pods (
coredns,metrics-server) from being evicted when nodes run low on resources - These four controls work together: LimitRange for defaults, ResourceQuota for caps, VPA for recommendations, PriorityClass for eviction ordering