Want resource limits in your cluster operated permanently? WZ-IT operates Kubernetes clusters with monitoring, regular capacity reviews and updates in a maintenance window - on your infrastructure or hosted by WZ-IT in Germany. See Managed Kubernetes
A container without a limit can take up a node's memory, and a team without a quota can take up the rest of the cluster. Kubernetes limits this with five building blocks: requests reserve, limits cap, the QoS class follows from them, the LimitRange sets defaults per container and the ResourceQuota limits the sum per namespace. This guide shows each building block with YAML, the related error messages, a worked example for a namespace budget and the metrics for monitoring. As of October 2026, checked against the documentation for Kubernetes 1.37 (currently 1.37.1).
Table of contents
- Requests, limits and QoS classes
- LimitRange: defaults and bounds per container
- ResourceQuota: the budget per namespace
- Scopes: forbid BestEffort, separate priorities
- Common errors and their cause
- Planning a namespace budget: a worked example
- Multi-tenancy pattern
- Monitoring utilization
- Status and methodology
- Our approach at WZ-IT
- Further guides
Requests, limits and QoS classes
The request is the amount the scheduler reserves for a container. The sum of the requests on a node must not exceed its capacity, even if actual utilization is low (Resource Management). When CPU is contended, the CPU request also acts as a weight. The limit is the upper bound enforced by the kubelet and the kernel. It acts differently for each resource:
- CPU is throttled. The container keeps running but gets no more CPU time.
- Memory is enforced through OOM kills. If the container exceeds its limit, the kernel may terminate it; this happens reactively under memory pressure.
If you set only a limit, you get a request of the same size, even if a LimitRange provides a lower default request (example). If you set only a request, you get the default limit of the LimitRange, provided it defines one.
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 512Mi
The notation matters for memory: 400m is 0.4 bytes, usually 400Mi is meant. Requests and limits determine the pod's QoS class (Pod QoS):
| QoS class | Condition (for all containers of the pod) | Behavior under memory pressure on the node |
|---|---|---|
| Guaranteed | CPU and memory: request and limit set, request equal to limit | not evicted because of other pods' consumption |
| Burstable | at least one request or limit, but not Guaranteed | evicted when consumption is above the request |
| BestEffort | no requests or limits for CPU or memory | request is 0, any usage is above it: affected first |
The class is fixed on creation. An in-place resize (stable since 1.35) must keep it as well (Resize). The eviction order depends on consumption above the request and on priority, not directly on the class. The class does determine the oom_score_adj the kernel uses when node memory runs out: Guaranteed -997, BestEffort 1000 (Node-pressure Eviction).
Our operating rule: memory request equal to memory limit, so reservation and upper bound match. A pod only becomes Guaranteed if the same holds for CPU. Memory request equal to limit plus a CPU request without a CPU limit gives Burstable and is the pragmatic default for most workloads.
LimitRange: defaults and bounds per container
A LimitRange acts when pods and PVCs are created in the namespace: first it sets defaults for containers without their own values, then it checks minimum, maximum and limit-to-request ratio. The API answers violations with 403 (LimitRange). Running pods stay unchanged.
apiVersion: v1
kind: LimitRange
metadata:
name: container-defaults
namespace: team-a
spec:
limits:
- type: Container
defaultRequest:
cpu: 100m
memory: 128Mi
default:
memory: 256Mi
min:
memory: 32Mi
max:
memory: 2Gi
maxLimitRequestRatio:
memory: "4"
Every container without its own values gets a request of 100m CPU and 128Mi memory and a memory limit of 256Mi, but no CPU limit. Two pitfalls:
- Defaults arise automatically. Setting
maxin a container entry createsdefault(the limit) anddefaultRequest; setting onlymincreatesdefaultRequest. Amax.cpuof 800m withoutdefaultproduces a default limit and a default request of 800m as well (example in the docs, logic indefaults.go). Every container without a CPU value then reserves 800m. Always set defaults explicitly. - Defaults are not checked for consistency. If the default limit is below the pod's request, the API rejects the pod as invalid (see the table below). Avoid several LimitRange objects per type, because which default applies is then undetermined.
ResourceQuota: the budget per namespace
A ResourceQuota limits the sum across all objects in the namespace and takes effect as soon as a ResourceQuota object exists in the namespace (Resource Quotas). Two objects cover compute as well as object counts and storage:
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute
namespace: team-a
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.memory: 12Gi
pods: "40"
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: objects-and-storage
namespace: team-a
spec:
hard:
count/jobs.batch: "20"
count/secrets: "50"
services.loadbalancers: "1"
services.nodeports: "0"
persistentvolumeclaims: "10"
requests.storage: 200Gi
fast-ssd.storageclass.storage.k8s.io/requests.storage: 100Gi
count/<resource>.<group> (for resources of the core group count/<resource>) counts arbitrary objects including custom resources and protects the control plane, for example from a CronJob that creates too many Jobs. fast-ssd is the name of a StorageClass. These rules govern the behavior:
- Mandatory values. If the quota names CPU or memory, every new pod must set the named value, otherwise the API rejects it (
must specify limits.memory for: app, checked inpods.go). The LimitRange from the previous section closes this gap. - The effective value is counted. Init containers (docs) and pod overhead (docs) are included. Pods in the Succeeded or Failed state do not count for
podsand the compute resources (count/podscounts all pods regardless of state). - Exceeding the quota. The API rejects directly created pods with 403 and
exceeded quota. With a Deployment, creation succeeds but the pods are missing: the status showsReplicaFailurewith the reasonFailedCreate(Deployment docs). - Hard limits only. There are neither soft quotas nor warnings under
spec.hard. - Absolute values. Additional nodes do not raise a quota, and a quota does not assign pods to nodes: pods from several namespaces still share nodes.
- Protection from changes. The
adminrole has no write access to quotas (RBAC); anyone with full write access in the namespace can delete them. Quotas and LimitRanges belong to the platform team, not to the tenant.
Scopes: forbid BestEffort, separate priorities
With scopes, a quota applies only to a subset of pods. The BestEffort scope allows only pods as a resource (validation in the source). With pods: "0" it forbids BestEffort pods in the namespace:
apiVersion: v1
kind: ResourceQuota
metadata:
name: no-best-effort
namespace: team-a
spec:
hard:
pods: "0"
scopes:
- BestEffort
With LimitRange defaults no BestEffort pods arise anyway; the quota catches namespaces where the LimitRange is missing. With PriorityClass, high-priority pods get a budget of their own; the docs show three quotas for high, medium and low.
Common errors and their cause
| Message or symptom | Cause | Check and fix |
|---|---|---|
exceeded quota: mem-cpu-demo, requested: requests.memory=700Mi, used: requests.memory=600Mi, limited: requests.memory=1Gi |
Namespace sum exhausted | kubectl describe quota -n NAMESPACE; lower requests, raise the quota or remove workloads. For Deployments check kubectl describe deployment and the ReplicaFailure condition |
must specify limits.memory for: app |
The quota names the value, the container does not set it | Create a LimitRange with defaults or set the value in the manifest |
maximum cpu usage per Container is 800m, but limit is 1500m |
LimitRange max violated |
Lower the limit or adjust max |
spec.containers[0].resources.requests: Invalid value: "700m": must be less than or equal to cpu limit of 500m |
The LimitRange default limit is below the request | Set request and limit in the manifest or align the defaults |
OOMKilled, exit code 137, rising restart count |
Container exceeds its memory limit | kubectl describe pod, section Last State; rule out a memory leak, otherwise raise limit and request |
| High latency at low CPU utilization | A CPU limit throttles the container | Check the metric container_cpu_cfs_throttled_periods_total; raise or remove the limit |
Pod stays Pending, event FailedScheduling |
The request fits on no node | kubectl describe node, section Allocated resources; lower the request or add nodes |
| Rollout hangs although the quota suffices in steady state | A rolling update needs room for additional pods | Plan the quota with headroom (see the worked example) |
CPU limits and throttling. The kernel hands out CPU time per period; the kubelet uses 100 ms by default (kubelet reference, kernel docs). A limit of 500m means 50 ms of CPU time per period. If four threads run at the same time on idle cores, the quota is used up after 12.5 ms (4 times 12.5 ms equals 50 ms). Until the period ends, all threads stand still, that is 87.5 ms. The average utilization over one second still shows only 0.5 CPU. Averages on a dashboard hide the problem, the throttling metric shows it.
Planning a namespace budget: a worked example
Assumptions: three workers with 8 vCPU and 32 GiB memory each, 24 vCPU and 96 GiB in total. 10 percent per node stays for kubelet, system services and eviction thresholds, which leaves 21.6 vCPU and 86.4 GiB allocatable. The requests should survive the loss of one node, so the planning basis is two nodes: 14.4 vCPU and 57.6 GiB. Distribution across the namespaces: 50, 20, 20 and 10 percent, rounded to whole units so that the sums stay below the bounds.
| Namespace | Share | requests.cpu |
requests.memory |
limits.memory |
|---|---|---|---|---|
| production | 50 % | 7 | 28Gi | 28Gi |
| staging | 20 % | 3 | 12Gi | 24Gi |
| development | 20 % | 3 | 12Gi | 24Gi |
| platform | 10 % | 1 | 5Gi | 10Gi |
| Sum | 14 (bound 14.4) | 57Gi (bound 57.6) | 86Gi (bound 86.4) |
The rules behind it: the sum of the requests still fits on the remaining nodes after losing one node. The sum of limits.memory stays below the allocatable memory of all three nodes, so a healthy cluster has no memory overcommit. CPU is not capped through limits, because CPU can be throttled while memory cannot.
Rolling updates need headroom. maxSurge defaults to 25 percent and is rounded up. A Deployment with 6 replicas at 250m CPU and 512Mi memory each occupies 1,500m and 3Gi in steady state, but 8 pods with 2,000m and 4Gi during a rollout. Our planning rule: steady state at most 80 percent of the quota. The figures are a worked example with disclosed assumptions, not a recommendation for a specific cluster. Real values come from measurements over a representative period that includes load peaks.
Multi-tenancy pattern
- One namespace per team and environment, for example
team-a-prodandteam-a-staging. Namespaces are the basis for RBAC, quotas and network policies (Multi-tenancy). - The same set in each namespace: one LimitRange, one compute quota, one quota for objects and storage, optionally
no-best-effort. The set belongs in a namespace template in the Git repository, not in manual work. - Tenants get the
adminrole through a RoleBinding. That lets them read quotas but not change them. - Limits of quotas: they do not limit network traffic. The docs name node isolation for that; for even stricter separation they describe a dedicated cluster per tenant.
- Platform components get a budget of their own. One example is the scan jobs of the Trivy Operator (Trivy for Kubernetes).
Monitoring utilization
kubectl describe quota -n team-a-prod # Used and Hard per resource
kubectl top pod -n team-a-prod --containers # requires the metrics-server
kubectl describe node NODE # section Allocated resources
kubectl top reads the Metrics API and therefore requires the metrics-server. For alerts, these Prometheus queries work, with metric names taken from the documentation of kube-state-metrics and cAdvisor:
# Quota utilization per resource (1 = full)
kube_resourcequota{type="used"} / ignoring(type) kube_resourcequota{type="hard"}
# Share of throttled CPU periods per container
sum by (namespace, pod, container) (rate(container_cpu_cfs_throttled_periods_total[5m]))
/ sum by (namespace, pod, container) (rate(container_cpu_cfs_periods_total[5m]))
# OOM events in the last hour
increase(container_oom_events_total[1h]) > 0
Thresholds such as 90 percent quota utilization are freely chosen examples. Whether the metrics are available depends on the cluster's metrics setup.
Status and methodology
As of 28 October 2026. The basis is the Kubernetes documentation for version 1.37, the Kubernetes source code for error messages and defaulting, and the documentation of kube-state-metrics and cAdvisor. The YAML manifests were validated against the JSON schemas of Kubernetes API 1.37.1 and additionally applied on a test cluster running Kubernetes 1.34, where defaults, QoS classes and error messages were reproduced as well. For the queries, metric and label names and the syntax (promtool) were checked, not the results. Operating rules (memory request equal to limit, CPU limit only with a reason, steady state at most 80 percent of the quota) are WZ-IT rules, not Kubernetes requirements. The worked example holds only under the stated assumptions.
Our approach at WZ-IT
The inventory of workloads is part of taking over a cluster (checklist). For resources the guiding questions are: which namespaces have a quota and a LimitRange, which workloads run without a memory limit or request, how high is the quota utilization. In operations we monitor the control plane, nodes, core components and capacity. Monitoring runs 24/7, and the response to P1 incidents follows the service level. The scope of operations is described on the Managed Kubernetes page. Requests and limits of the applications are the application teams' responsibility unless agreed otherwise.
Further guides
- Taking over a Kubernetes cluster: audit checklist
- Trivy for Kubernetes: scan images, clusters, operator
- Longhorn vs. Rook-Ceph for choosing the StorageClass behind the storage quotas
- What is Kubernetes?
- Kubernetes support without a contract for individual tasks
Want resource limits and utilization checked in live operations? Managed Kubernetes costs from €590 excl. VAT per cluster and month including 3 worker nodes, €129.90 for each additional worker node, cancellable monthly with 30 days' notice to the end of the month (as of October 2026). Individual tasks without a contract: Kubernetes support at €120 excl. VAT per hour.











