Skip to main content

LimitRange

A namespace-scoped policy that sets default, minimum, and maximum resource constraints for individual containers and pods within a namespace. While ResourceQuota caps the total consumption of a namespace, LimitRange enforces boundaries at the per-container level — automatically injecting default requests/limits into pods that don't specify them.

LimitRange — Guardrails for Individual Containers

LimitRange vs ResourceQuota — The Key Difference

These two objects are complementary, not competing. Use both together:

◈ DIAGRAM
+--------------------------------------------+
| ResourceQuota | <- Namespace ceiling
| "This entire namespace can use at most |
| 32 CPU and 64Gi memory total" |
+--------------------------------------------+
|
v
+--------------------------------------------+
| LimitRange | <- Per-container guardrails
| "Each individual container must use |
| between 100m and 4 CPU" |
+--------------------------------------------+

ResourceQuota without LimitRange means a single container could claim all 32 CPU at once. LimitRange without ResourceQuota means you have per-container safety but no namespace ceiling. A Razorpay payments namespace running 30 microservices needs both.

A Production-Grade LimitRange

YAML
apiVersion: v1
kind: LimitRange
metadata:
name: payments-container-limits
namespace: payments-prod
spec:
limits:
# ── Per Container Limits ──
- type: Container
default: # Auto-injected limits if container specifies none
cpu: "500m"
memory: "512Mi"
defaultRequest: # Auto-injected requests if container specifies none
cpu: "100m"
memory: "128Mi"
max: # No container can exceed this ceiling
cpu: "4"
memory: "8Gi"
min: # No container can go below this floor
cpu: "50m"
memory: "64Mi"
# ── Per Pod Limits ──
- type: Pod
max:
cpu: "8" # Total CPU across all containers in one pod
memory: "16Gi"
# ── Per PersistentVolumeClaim Limits ──
- type: PersistentVolumeClaim
max:
storage: 100Gi # No single PVC can claim more than 100Gi
min:
storage: 1Gi

The Auto-Injection Superpower

The most underused feature of LimitRange: if a pod is created without resource requests or limits, the kubelet automatically injects the default and defaultRequest values from the LimitRange before scheduling.

◈ DIAGRAM
+------------------------------------------+
| Developer submits pod with no resources | <- Simple spec, no resources block
+------------------------------------------+
|
v
+------------------------------------------+
| Kubernetes Admission Controller | <- Intercepts the create request
| checks namespace LimitRange |
+------------------------------------------+
|
v
+------------------------------------------+
| LimitRange auto-injects defaults | <- requests: 100m CPU, 128Mi RAM
| | limits: 500m CPU, 512Mi RAM
+------------------------------------------+
|
v
+------------------------------------------+
| Pod scheduled with safe resource bounds | <- Protects the node and other pods
+------------------------------------------+

Before and after auto-injection in practice:

YAML
# Developer submits this minimal pod spec — no resources block at all:
containers:
- name: api
image: registry.razorpay.in/api:v2.1
# Kubernetes applies LimitRange and the pod actually runs as:
containers:
- name: api
image: registry.razorpay.in/api:v2.1
resources:
requests:
cpu: "100m" # <- Auto-injected from defaultRequest
memory: "128Mi" # <- Auto-injected from defaultRequest
limits:
cpu: "500m" # <- Auto-injected from default
memory: "512Mi" # <- Auto-injected from default

Validating LimitRange Is Working

Bash
# Inspect the active LimitRange in a namespace
kubectl describe limitrange payments-container-limits -n payments-prod
# Deploy a pod without any resource spec
kubectl run test-pod --image=nginx -n payments-prod
# Verify LimitRange injected defaults
kubectl get pod test-pod -n payments-prod -o yaml | grep -A 12 resources:
# Should show auto-injected cpu and memory values
# Check if a pod was rejected for violating min/max
kubectl get events -n payments-prod | grep LimitRangeError
# Clean up test pod
kubectl delete pod test-pod -n payments-prod

LimitRange Troubleshooting Reference

Symptom Likely Cause Fix
Pod rejected: exceeds max limit Container requests exceed LimitRange max Lower the container's resource request or raise LimitRange max
Pod rejected: below min limit Container request set too low Remove the explicit request and let LimitRange inject defaults
LimitRange not injecting defaults Pod already specifies resources explicitly LimitRange only injects when the field is completely absent
OOMKilled after LimitRange applied Default memory limit is too low for the workload Increase default.memory in LimitRange or set explicit limits on that container
All pods in namespace failing admission default.cpu exceeds max.cpu Fix the LimitRange — default must be ≤ max
Tip

LimitRange is especially useful when onboarding new teams onto a shared cluster at CRED or Zerodha. New engineers can write simple pod specs without worrying about resource tuning — LimitRange ensures they always get sane defaults and cannot accidentally starve neighbouring services.

Common Mistake

Setting max.cpu in LimitRange lower than the default.cpu value. This creates an impossible constraint — the auto-injected default immediately violates the max, and every pod without explicit resources is rejected at admission. Always verify: default ≤ max and defaultRequest ≤ default.

Remember

LimitRange is namespace-scoped. You must create one per namespace that needs guardrails. A LimitRange in payments-prod has zero effect on pods in orders-prod.

Security

Without a LimitRange, any pod in a namespace can consume unbounded CPU and memory — one misconfigured deployment can starve every other service on the node. Always apply a LimitRange to every production namespace before allowing teams to deploy.

Frequently Asked Questions

How does LimitRange interact with ResourceQuota when both are set on a namespace?

ResourceQuota caps the aggregate CPU/memory (and object counts) across the entire namespace, but it actually requires every pod in that namespace to specify requests/limits explicitly — without them, quota can't do its accounting. LimitRange fills that gap: it injects default requests/limits onto any container that omits them, and separately enforces a per-container min/max, so a namespace with both effectively guarantees every pod has explicit resource values within an allowed range.

What's a common mistake when setting LimitRange defaults on a namespace?

Setting a default that's too low for the namespace's typical workloads, so pods that don't specify their own requests silently get under-provisioned defaults and then get OOMKilled or throttled under real load — the failure often looks unrelated to LimitRange because the pod manifest itself never mentions resources at all. It's worth explicitly checking `kubectl describe limitrange` in a namespace before assuming a pod's effective resource values match what's in its own YAML.