Quota & Capacity Requests
Summary
- A quota request is a team asking the platform for a slice of finite cluster capacity, enforced by
ResourceQuotaand priced in real nodes. - Insight: Quota is not a permission slip. Every core you grant is a core someone must buy, so the conversation is a budget conversation wearing a YAML costume.
- Key Concepts:
ResourceQuota(namespace ceiling),LimitRange(per-pod defaults), requests vs limits (scheduling vs throttling), headroom (unallocated capacity), and overcommit (sum of quotas exceeding physical cores). - When to Use: Onboarding a new team, sizing a launch, or arbitrating between two teams that both want the same node pool.
- Limitations: Quota caps requests, not actual usage efficiency. A team can hold quota it never uses and still block others.
Recipe
The shortest path from "we need more capacity" to a merged manifest.
- Ask the team for expected pods, per-pod CPU/memory requests, and a peak multiplier.
- Multiply into a total, then add 25% headroom for rollouts.
- Convert that total into node count and monthly dollars using your node pool's price.
- Check cluster headroom. If the request does not fit, the answer is a purchase decision, not a YAML edit.
- Land a
ResourceQuotaplus aLimitRangein the team's namespace. - Set a review date. Quota granted without an expiry becomes permanent.
Working Example
A namespace for a checkout team expecting 20 pods at 500m CPU and 1Gi memory each.
apiVersion: v1
kind: ResourceQuota
metadata:
name: checkout-quota
namespace: checkout
spec:
hard:
requests.cpu: "12"
requests.memory: 24Gi
limits.cpu: "24"
limits.memory: 48Gi
pods: "40"
services.loadbalancers: "1"
persistentvolumeclaims: "6"
---
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: "4"
memory: 8GiThe services.loadbalancers: "1" line is the quiet money-saver.
Each LoadBalancer Service provisions a cloud load balancer with its own hourly charge. Capping it at one forces the team onto shared ingress instead of billing you for ten.
Verify what the team is actually holding.
kubectl describe resourcequota checkout-quota -n checkout
kubectl get pods -n checkout -o custom-columns=\
NAME:.metadata.name,CPU:.spec.containers[*].resources.requests.cpuDeep Dive
Requests are the currency, not limits
The scheduler places pods using requests only.
A pod requesting 500m CPU consumes 500m of a node's allocatable capacity whether it burns 5m or 500m at runtime. That reservation is what you pay for.
Limits do something different. They throttle CPU and OOM-kill on memory, but they never reserve anything.
So when a team asks for quota, they are really asking you to reserve nodes. Frame it that way and the number gets more honest fast.
From quota to node count
Node allocatable is always less than node capacity.
The kubelet reserves CPU and memory for the OS, the kubelet itself, and containerd. A 16-core node typically offers roughly 15.1 allocatable cores, and a 64Gi node meaningfully less than 64Gi.
So 12 requested cores does not mean "less than one node." Do the arithmetic against allocatable, then add a node for rollout surge, because a RollingUpdate briefly runs old and new pods together.
The translation looks like this: 12 cores plus 25% headroom is 15 cores, which is one 16-core node plus surge room on shared capacity.
Overcommit is a policy choice
You may deliberately grant quota summing to more than the cluster has.
This works when teams peak at different times and Cluster Autoscaler or Karpenter can add nodes on demand. It fails loudly when everyone peaks together and pods sit Pending with FailedScheduling.
Decide your overcommit ratio explicitly and tell stakeholders. An unstated 2x overcommit is a reliability promise you never made but will be blamed for.
Priority classes break ties
Quota decides who may ask. PriorityClass decides who wins when the cluster is full.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: revenue-critical
value: 100000
globalDefault: false
description: "Checkout and payments. Preempts batch."Give revenue paths a high value and batch jobs a low one. Now a full cluster degrades in the order the business would choose rather than at random.
Gotchas
Quota with no requests set rejects every pod. Once a ResourceQuota constrains requests.cpu, any pod in that namespace without a CPU request is rejected outright. Always ship a LimitRange alongside it so existing manifests keep working.
Teams size from limits, not measurements. They pick 2 CPU because it felt safe. Ask for a week of Prometheus container_cpu_usage_seconds_total data instead, and quota shrinks by half on most first passes.
Granted quota is never returned. Nobody files a ticket to give capacity back. Attach a 90-day review to every grant and reclaim what sits idle.
Load balancers escape the CPU conversation. A team under quota can still create ten LoadBalancer Services and add real monthly cost. Quota services.loadbalancers explicitly.
PVCs outlive namespaces. Storage keeps billing after pods are gone. Quota persistentvolumeclaims and requests.storage, and check the reclaim policy.
Alternatives
Chargeback or showback tooling such as OpenCost or Kubecost attributes real spend per namespace. Pick this when teams argue about fairness, because a bill ends arguments faster than a quota table.
Dedicated node pools with taints and tolerations give a team physical isolation instead of a logical cap. Pick this for compliance boundaries or noisy-neighbor problems, and accept the lower utilization.
Separate clusters per team give the cleanest blast-radius and billing story. Pick this only when the control-plane and operational cost of many clusters is genuinely cheaper than the coordination cost of one.
No quota at all is viable in small, high-trust clusters. Pick this when the cost of a runaway job is lower than the cost of gatekeeping every deploy.
FAQs
Does ResourceQuota limit what a pod actually uses? No. It caps the sum of declared requests and limits in a namespace. Runtime enforcement comes from the container limits themselves.
Why did adding a quota break existing deployments?
Because those pods have no resource requests. A quota on requests.cpu makes requests mandatory. Add a LimitRange with defaultRequest to fix it.
How much headroom should I add?
Roughly 25% covers rolling updates and normal variance. Add more if the team runs StatefulSet workloads or long pod startup times.
Can quota stop a team from creating LoadBalancer Services?
Yes. Set services.loadbalancers in the quota spec. This is the cheapest cost control you can apply in a single line.
What do I tell a team whose request does not fit? Give them the node count and monthly figure their ask implies, then let the product owner decide whether to fund it or shrink it. That is a budget answer, not a platform refusal.
Should quota be per namespace or per team? Per namespace, since that is what Kubernetes enforces. Map one team to one or more namespaces and track the total in your capacity sheet.
Related
- Translating Clusters into Business Language
- Stakeholder Basics
- Risk Communication
- Stakeholder Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).