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

  • LimitRange sets per-container defaults and maximums — without it, containers without resources: run unbounded
  • ResourceQuota caps 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: Auto is 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