,

Kubernetes ResourceQuotas and LimitRanges: Govern Requests at the Namespace Level

Learn how Kubernetes ResourceQuotas and LimitRanges control namespace resource consumption. This guide explains CPU, memory, storage, and object quotas, admission-time defaults, quota scopes, and how to prevent runaway workloads from exhausting cluster resources.

Laurent Gil Avatar
kubernetes resource quotas featured image

Most Kubernetes clusters start clean: one namespace, no quotas, teams ship whatever they build. That works until it doesn’t. A runaway deployment can exhaust cluster CPU in minutes. A misconfigured test pod can starve production. Without namespace-level governance, resource problems compound quietly until something breaks at the worst time.

ResourceQuotas and LimitRanges are Kubernetes’s built-in answer to that problem. ResourceQuotas cap what a namespace can consume in aggregate. LimitRanges set per-container defaults, minimums, and maximums, and inject those values at admission time. Together, they govern requests at the namespace level so cost and reliability are controlled before workloads reach the scheduler.

According to the Cast AI 2026 State of Kubernetes Optimization report, average CPU utilization sits at just 8% across production clusters. 69% of clusters are CPU-overprovisioned – up from 40% in 2024. 79% are memory-overprovisioned. One in two containers uses less than a third of its requested CPU and memory. These numbers describe what happens when resource governance is absent or applied inconsistently.

Key takeaways

  • ResourceQuotas enforce hard caps on total CPU, memory, storage, and object counts for an entire namespace.
  • LimitRanges set per-container defaults, minimums, and maximums, and inject those defaults at pod admission time.
  • The LimitRange admission controller runs before the ResourceQuota check, so injected defaults are counted against namespace totals.
  • Always deploy a LimitRange before (or alongside) a ResourceQuota: if a compute quota exists but no LimitRange is present, pods submitted without explicit resource declarations are rejected with HTTP 403.
  • Object count quotas (pods, secrets, services) protect the Kubernetes control plane from DoS-style resource exhaustion caused by runaway controllers.
  • Quota scopes (BestEffort, NotBestEffort, Terminating, PriorityClass) allow targeting enforcement to specific pod classes within a namespace.

What ResourceQuotas and LimitRanges do

These two objects solve different problems. Understanding the distinction is critical before deploying either, because conflating them leads to gaps in governance and admission errors that are frustrating to debug.

ResourceQuota: namespace aggregate cap

A ResourceQuota object enforces hard limits on total resource consumption across a namespace. When a quota exists, Kubernetes tracks cumulative usage and rejects any new object that would push the namespace over its declared limits.

ResourceQuota covers several resource categories. Compute resources include requests.cpu, requests.memory, limits.cpu, and limits.memory. Storage quotas cover requests.storage and per-StorageClass limits. Object count quotas track pods, services, secrets, configmaps, persistentvolumeclaims, and more.

The key behavior to understand: ResourceQuota is an aggregate check. It does not evaluate any single pod’s footprint in isolation. Instead, it asks whether the namespace total would exceed the hard cap after admitting the new object. Therefore, a single large pod and ten small pods are equivalent to the quota controller, provided they consume the same total resources.

LimitRange: per-container defaults and bounds

A LimitRange object operates at the container level, not the namespace level. It does three things simultaneously at admission time: it injects default request and limit values when a container omits them, it enforces minimum and maximum per-container values, and it validates that declared limits and requests fall within an allowed ratio.

Without a LimitRange, a container spec with no CPU or memory fields gets scheduled with no requests. The scheduler then has no accurate data to place it correctly, and the pod may land on a node that cannot actually support its runtime consumption. LimitRange prevents that by ensuring every container enters the cluster with meaningful resource declarations.

In contrast to ResourceQuota, LimitRange validates only at pod admission. It does not retroactively enforce limits on pods that are already running. Existing workloads are unaffected when you add or update a LimitRange in a namespace.

The one-line comparison

ResourceQuota answers: How much can this namespace consume in total? LimitRange answers: What does each container get if no one specifies, and what is the per-container ceiling?

Configuring each

ResourceQuota YAML

The following YAML sets compute limits, a pod ceiling, and object count caps for a team namespace. Apply it with kubectl apply -f quota.yaml.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-quota
  namespace: team-alpha
spec:
  hard:
    requests.cpu: "4"           # Total CPU requests the namespace can claim
    requests.memory: "8Gi"      # Total memory requests across all pods
    limits.cpu: "8"             # Total CPU limits (burstable ceiling for the namespace)
    limits.memory: "16Gi"       # Total memory limits
    pods: "20"                  # Maximum number of pods; protects the scheduler
    persistentvolumeclaims: "5" # PVC count cap
    count/services: "10"        # Service object count
    secrets: "20"               # Secret count; protects the API server

The pods: "20" and secrets: "20" entries are worth highlighting separately. Object count quotas protect the Kubernetes API server from DoS-style abuse, where a misconfigured controller loops and creates thousands of secrets or pods. For example, a broken Helm release that retries on failure can generate hundreds of secrets in seconds. Object count quotas stop that before it cascades.

Note: Helm release secrets count against this quota. Each helm install and helm upgrade creates a new secret. A quota of 20 secrets on an active namespace with frequent Helm deployments may be too low. Monitor with kubectl get secrets -n team-alpha | wc -l.

One important interaction: if your namespace sets a pods quota and you run HPAs, the scale-out can silently stall. When an HPA tries to scale a deployment past the pod ceiling, the ReplicaSet controller cannot create new pods – but no obvious error surfaces to the developer. The deployment shows as degraded, not as quota-exhausted. Set your pods quota to at least 2x the sum of all HPA maxReplicas values in the namespace. Check for stalled scale-out with:

kubectl get events -n team-alpha --field-selector reason=FailedCreate

Or directly on the deployment’s ReplicaSet:

kubectl describe replicaset -n team-alpha | grep -A3 FailedCreate

Example FailedCreate event output:

Warning  FailedCreate  3m  replicaset-controller  Error creating: pods "app-7d9f" is forbidden: exceeded quota: team-quota, requested: pods=1, used: pods=20, limited: pods=20

The HPA itself shows ScalingLimited in its conditions (kubectl describe hpa -n team-alpha | grep Conditions), but the actual error surfaces on the ReplicaSet.

Rolling updates face the same ceiling. A deployment at 18 pods with maxSurge: 1 and a pods: 20 quota cannot roll out – the replacement pod exceeds the ceiling before the old pod terminates. Set your pods quota at least current_replicas + max_surge_value to allow in-progress rolling updates.

DaemonSets in the same namespace count against its quota. On a 50-node cluster, a single DaemonSet in team-alpha deploys 50 pods from day one. Calculate your baseline pod count from DaemonSets before setting the pods ceiling:

kubectl get ds -n team-alpha -o json | jq '.items[] | {name: .metadata.name, desired: .status.desiredNumberScheduled}'

Subtract this from your quota ceiling before workload teams start deploying.

LimitRange YAML

This LimitRange injects sensible defaults and enforces per-container bounds in the same namespace. Deploy it alongside (or before) the ResourceQuota above.

apiVersion: v1
kind: LimitRange
metadata:
  name: team-limits
  namespace: team-alpha
spec:
  limits:
  - type: Container
    default:            # Limit applied when a container omits its limits field
      cpu: 500m
      memory: 256Mi
    defaultRequest:     # Request applied when a container omits its requests field
      cpu: 100m
      memory: 128Mi
    max:                # Hard ceiling per container; admission rejects anything above this
      cpu: "2"
      memory: 2Gi
    min:                # Minimum per container; prevents zero-request pods from bypassing quota
      cpu: 50m
      memory: 64Mi

With this LimitRange deployed, a container that declares no resource fields automatically receives requests.cpu: 100m, limits.cpu: 500m, requests.memory: 128Mi, and limits.memory: 256Mi. The ResourceQuota then counts those injected values against the namespace total. Consequently, developers can ship without specifying resource fields, and the namespace still stays within governed bounds.

LimitRange also supports a maxLimitRequestRatio field, which enforces a maximum ratio between the limit and request for a container – useful if you want to prevent extreme burst configurations like a 1m request with a 2-CPU limit.

Checking quota utilization

After applying a ResourceQuota, inspect current usage with kubectl describe:

kubectl describe resourcequota team-quota -n team-alpha

The output shows Used vs Hard for each resource:

Name:                    team-quota
Namespace:               team-alpha
Resource                 Used  Hard
--------                 ----  ----
limits.cpu               500m  8
limits.memory            256Mi 16Gi
persistentvolumeclaims   0     5
pods                     3     20
requests.cpu             100m  4
requests.memory          128Mi 8Gi

Monitor quota consumption with kube-state-metrics. Alert when usage exceeds 80% of any hard limit:

sum(kube_resourcequota_used) by (resource, namespace)
  / sum(kube_resourcequota_hard) by (resource, namespace) > 0.8

Defaults and admission

The admission sequence is where most operational problems with this pairing originate. Understanding it prevents the most common mistake: deploying a ResourceQuota without a LimitRange.

How admission controllers order their checks

When a pod creation request reaches the API server, two admission controllers run in sequence. First, the LimitRanger admission controller inspects each container. It injects default requests and limits where values are absent, and it validates that declared values fall within the min and max bounds defined in the LimitRange. Second, the ResourceQuota controller runs. It takes the now-complete resource declarations, including any injected defaults, and checks whether the namespace totals would be exceeded.

As a result, the ResourceQuota always sees fully populated resource fields. That ordering is intentional: defaults must exist before totals can be tallied accurately.

Auditing existing namespaces before applying quotas

Before applying a ResourceQuota to an existing namespace, audit which pods already lack resource declarations:

kubectl get pods -n team-alpha -o json | jq '.items[] | select(any(.spec.containers[]; .resources.requests == null)) | .metadata.name'

This catches containers with explicitly null requests. To also catch empty resource declarations (resources: {}), add a second audit:

kubectl get pods -n team-alpha -o json | jq '.items[] | select(any(.spec.containers[]; (.resources.requests // empty | length) == 0)) | .metadata.name'

Any pod returned by these commands will prevent its controller from recreating it once the quota is active. Update those workloads first.

The critical operational tip

Always deploy your LimitRange before (or at the same time as) your ResourceQuota. Here is why: when a CPU or memory quota exists in a namespace, the API server requires that every new pod declares requests and limits. If no LimitRange exists to inject those defaults, a pod submitted without explicit resource fields is rejected at admission with HTTP 403.

The failure message is often opaque. Developers see a generic admission error rather than a clear explanation that the namespace quota demands resource declarations. The fix is straightforward: deploy the LimitRange alongside the ResourceQuota so defaults are always available at admission time.

This ordering matters especially during namespace bootstrapping. However, it also applies when retrofitting quotas onto existing namespaces. In that case, existing pods are unaffected (LimitRange validates only at admission, not retroactively). New pods, though, will hit the HTTP 403 error unless a LimitRange is already in place.

Admission path for a pod without resource declarations

Trace the path for a pod spec that includes no resource fields at all:

  1. LimitRanger checks whether a LimitRange exists in the namespace.
  2. If it finds one, it mutates the pod spec and injects the defaultRequest and default values from the LimitRange.
  3. The mutated pod spec proceeds to ResourceQuota validation with complete resource fields.
  4. ResourceQuota checks namespace totals and admits or rejects based on available capacity.
  5. If no LimitRange exists and the namespace has a compute quota, step 3 delivers an empty spec to step 4, and the pod is rejected with HTTP 403.

Using quota scopes

ResourceQuota supports scopes that target specific pod classes: BestEffort, NotBestEffort, Terminating, and PriorityClass. For example, you can create a separate quota that applies only to BestEffort pods (those with no requests or limits set) and a different quota for NotBestEffort pods. This lets platform teams apply different caps to batch jobs, critical services, and spot workloads sharing a namespace, without requiring separate namespaces for each class.

Conclusion

ResourceQuotas and LimitRanges are the namespace-level governance layer every multi-team Kubernetes platform needs. ResourceQuotas cap what a namespace can consume in aggregate. LimitRanges ensure every pod enters the cluster with well-formed resource declarations. Together, they establish the policy ceiling before workloads reach the scheduler.

Namespace quotas set the ceiling on what a team is allowed to request. Cast AI Workload Optimization continuously adjusts per-container resource requests using live performance data. On Kubernetes 1.27 and later with in-place vertical scaling enabled, adjustments apply without pod restarts. On earlier versions, Cast AI applies updates through pod eviction and rescheduling – the same mechanism as VPA, but with SLO-aware pacing. Either way, namespace quotas set the ceiling; Cast AI drives actual requests toward real usage. Governance above, optimization below.

Frequently Asked Questions

What is a ResourceQuota?

A ResourceQuota is a Kubernetes object that enforces hard limits on the total CPU, memory, storage, and object counts a namespace can use. When the cumulative total across all pods in the namespace would exceed a declared limit, the API server rejects the new object at admission. ResourceQuota applies to the namespace as a whole, not to individual containers.

What is a LimitRange?

A LimitRange is a Kubernetes object that sets per-container (or per-pod) defaults, minimums, and maximums. It injects default request and limit values at admission time for any container that does not declare them, and it rejects containers whose declared values fall outside the allowed range. LimitRange validates only at pod admission and does not enforce limits on already-running pods.

How do I set default requests for a namespace?

Deploy a LimitRange with a defaultRequest field under the Container limit type. Any container submitted without explicit resource requests automatically receives those defaults at admission. This approach prevents zero-request pods and ensures ResourceQuota enforcement works correctly, because the quota controller requires fully populated resource fields before it can check namespace totals.

Cast AIBlogKubernetes ResourceQuotas and LimitRanges: Govern Requests at the Namespace Level