On this page
A namespace does more than group your resources. It is also the place where you limit how much CPU and memory the workloads inside it can use. Without limits, one misbehaving app can take every resource on a shared node.
Think of your cluster as an apartment building. Each namespace is an apartment, and each Pod is a tenant living in it. Without house rules, one noisy tenant could run every appliance at once and leave nothing for the neighbours. Kubernetes gives you two house-rule objects: ResourceQuota and LimitRange.
Both build on two settings that every container can declare: requests and limits. This lesson covers all four building blocks and how they fit together.
What you will learn
- How Kubernetes measures CPU and memory
- What requests (the guaranteed minimum) and limits (the hard ceiling) do, and what happens when a container hits a limit
- How a ResourceQuota sets a total budget for a whole namespace
- How a LimitRange adds defaults and min/max boundaries for every container
- How to read the events and errors these objects produce
How Kubernetes measures CPU and memory
Kubernetes tracks two kinds of computing resources for your containers:
| Resource | Unit | Examples |
|---|---|---|
| CPU | Cores, or millicores (m). 1000m is one full core | 250m is a quarter of a core, 500m is half a core, 1 is one core |
| Memory | Bytes, usually with the suffixes Mi (mebibyte) and Gi (gibibyte). 1Mi is 1,048,576 bytes | 128Mi, 512Mi (about half a gibibyte), 2Gi |
For a whole core, write 1 rather than 1000m. Both mean the same, but 1 is the common style. For memory, watch the suffix: M means 1,000,000 bytes while Mi means 1,048,576, so stick to Mi and Gi.
The capacity of a cluster is the sum of its worker (compute) nodes. To get more CPU and memory you add nodes: buy hardware on-premises, or ask your cloud provider's API for more virtual machines.
How Pods use node resources
When you create a Pod, a control plane component called the kube-scheduler picks a suitable worker node, and the kubelet on that node starts the containers. This process is called Pod scheduling.
By default, nothing stops a scheduled container from using all the CPU and memory on its node. If ten Pods share one node and a single greedy Pod takes everything, all ten suffer. That leads to two needs:
- Each Pod should be able to request the resources it needs to work.
- The cluster should be able to restrict a Pod so it cannot starve its neighbours.
Requests and limits answer exactly these two needs.
Requests: the guaranteed minimum
A request is the amount of CPU and memory a container needs to run properly. The scheduler reads it to find a node with enough free room. Once the Pod is placed, that amount is reserved, so it is guaranteed to the container.
Pending state.Limits: the hard ceiling
A limit is the most a container is allowed to consume. Make it a habit to set a limit whenever you set a request. What happens at the limit depends on the resource:
| Limit reached | What Kubernetes does | What you notice |
|---|---|---|
| CPU | Throttles the container (slows it down) | Slower responses, but the container keeps running |
| Memory | Kills the container (OOMKilled), because memory cannot be throttled | The container restarts, and kubectl describe pod shows the reason OOMKilled |
ResourceQuota vs. LimitRange
Requests and limits work well, as long as nobody forgets to set them. It is very easy to deploy a Pod with no request and no limit, and that Pod can then eat a whole node. Kubernetes closes this gap with two namespace-scoped objects:
| Object | Applies to | What it controls |
|---|---|---|
ResourceQuota | The whole namespace (all Pods combined) | A total CPU and memory budget, plus object counts (Pods, Services, ConfigMaps, PersistentVolumeClaims and more) |
LimitRange | Each individual container (or Pod) | Default requests and limits, plus min and max boundaries |
ResourceQuota covers CPU or memory, every new Pod in the namespace must declare those requests and limits, or the API server rejects it. A LimitRange fills in sensible defaults automatically, so nobody's Pod is rejected just for forgetting them.Commands for inspecting resources
kubectl top shows real CPU and memory usage, but it needs the metrics server. On minikube, enable it as an add-on:
Then check usage per node and per Pod:
List and describe the quotas and limit ranges in a namespace. describe shows the hard values next to what is used right now:
When a Pod is stuck in Pending, the events at the bottom of describe explain why:
Hands-on: a budget for the billing team
You will build a small multi-tenant setup for a fictional billing team: a namespace, an ASP.NET Core app, a quota and a limit range. Any local cluster such as minikube or kind works. Follow the steps in order.
Step 1 — Look at how the app image is built
Every container needs an image. This Dockerfile shows how a real ASP.NET Core app on .NET 10 becomes one: it builds with the SDK image, then copies the output into the smaller runtime image.
To keep the lesson self-contained, the Pods below use Microsoft's ready-made sample image mcr.microsoft.com/dotnet/samples:aspnetapp, which also listens on port 8080. Swap in your own image when you have one.
Step 2 — Create the namespace
Every quota and limit range lives inside a namespace, so create that first. It keeps the billing team's workloads apart from the rest of the cluster. Save this as tenant-billing-namespace.yaml:
Apply it:
Step 3 — Run a Pod with sensible requests and limits
This Pod is guaranteed 250m CPU and 256Mi memory, and is capped at 500m CPU and 512Mi memory. A small node can easily satisfy these values. Save it as catalog-api-pod.yaml:
Apply it and confirm it reaches the Running state:
Expected output:
Step 4 — See what an impossible request does
Now request an unrealistic 200Gi of memory on purpose. No normal node has that much, so the scheduler cannot place the Pod. Save it as catalog-api-oversized-pod.yaml:
Apply it, then look at its status and ask why it is stuck:
The status stays Pending, and the events show a message like this:
That proves a request larger than any node can never be scheduled. Delete the Pod before you continue:
Step 5 — Add a ResourceQuota for the namespace
A ResourceQuota sets a total budget for everything in the namespace combined. This one says: all Pods together may request at most 1 CPU core and 1Gi of memory, and set limits of at most 2 cores and 2Gi. The namespace may hold at most 10 Pods, 10 ConfigMaps and 5 Services. Save it as tenant-billing-resourcequota.yaml:
Apply it and check how much of the budget is already used:
Expected output. The running catalog-api Pod already counts. The one ConfigMap is kube-root-ca.crt, which Kubernetes adds to every namespace:
From now on, if a new Pod would push the namespace past this budget, the API server rejects it immediately with a Forbidden: exceeded quota error. The Pod is never created.
Step 6 — Try a Pod without requests or limits
This Pod declares no resources at all. Save it as catalog-api-defaults-pod.yaml:
Try to create it:
The quota covers CPU and memory, so the API server refuses the Pod. You will see an error similar to this:
This is exactly the gap a LimitRange fills.
Step 7 — Add a LimitRange for defaults and boundaries
A LimitRange protects you from forgotten requests and limits, and blocks containers that are too small or too large. Its keys mean:
default: the limit applied automatically when a container sets none.defaultRequest: the request applied automatically when a container sets none.max: the highest value a container may declare, for its limit and its request.min: the lowest value a container may declare, for its request and its limit.
Save it as tenant-billing-limitrange.yaml:
Apply it and view the resulting table:
Expected output:
Step 8 — Retry the Pod without resources
Apply the same Pod again:
This time it is accepted. Check what the LimitRange filled in for it:
Expected output:
Any container in tenant-billing that omits its resources now inherits 250m/256Mi requests and 500m/512Mi limits, and can never go outside the min/max boundaries. Run kubectl describe resourcequota tenant-billing-quota -n tenant-billing again: both Pods now count, so requests.cpu shows 500m of 1.
max, for example a 2Gi memory limit here, is rejected with a Forbidden error.Step 9 — Clean up
Remove the objects you no longer need. Always include -n so you target the right namespace:
Deleting the namespace alone would also remove everything inside it. The separate commands just show how to remove each object type.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Pod stays Pending with Insufficient memory or Insufficient cpu | The request is bigger than the free room on any node | Lower the request, or add nodes |
Forbidden: exceeded quota | The namespace budget is used up | Delete unused Pods, lower requests, or raise the quota |
failed quota: … must specify limits.cpu | The quota covers CPU or memory, but the Pod declares none | Add requests and limits, or add a LimitRange with defaults |
maximum memory usage per Container is 1Gi | A container asks for more than the LimitRange max | Lower the value, or raise max |
Container restarts with reason OOMKilled | The memory limit is too low for the app | Raise the memory limit, or fix the memory leak |
| App is slow while CPU usage sits at the limit | The container is being throttled | Raise the CPU limit |
Summary
Namespaces do far more than group resources. They are the natural boundary for controlling how much CPU and memory your applications can consume.
- Requests guarantee a minimum and drive scheduling. A request larger than any node leaves the Pod stuck in
Pending. - Limits cap consumption. Hitting the CPU limit throttles the container, while hitting the memory limit kills it with
OOMKilled. - A ResourceQuota enforces a total budget for a whole namespace, covering both compute resources and object counts. Once it covers CPU or memory, every Pod must declare them.
- A LimitRange supplies defaults and
min/maxboundaries, so no container is left unconstrained and no Pod is rejected for forgetting its resources. - Define a
ResourceQuotaand aLimitRangefor every namespace you create.
Together they keep resource-hungry workloads contained at both the namespace and the container level, giving you a predictable and stable cluster.