Configuring ResourceQuota and LimitRange at the Namespace Level

Lesson 5 of 39 12 min read Updated

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

  1. How Kubernetes measures CPU and memory
  2. What requests (the guaranteed minimum) and limits (the hard ceiling) do, and what happens when a container hits a limit
  3. How a ResourceQuota sets a total budget for a whole namespace
  4. How a LimitRange adds defaults and min/max boundaries for every container
  5. 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:

ResourceUnitExamples
CPUCores, or millicores (m). 1000m is one full core250m is a quarter of a core, 500m is half a core, 1 is one core
MemoryBytes, usually with the suffixes Mi (mebibyte) and Gi (gibibyte). 1Mi is 1,048,576 bytes128Mi, 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:

  1. Each Pod should be able to request the resources it needs to work.
  2. 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.

A request bigger than any node means Pending forever. A Pod cannot span several nodes. If you request eight cores but every node has only four, no node can satisfy it and the Pod stays in the 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 reachedWhat Kubernetes doesWhat you notice
CPUThrottles the container (slows it down)Slower responses, but the container keeps running
MemoryKills the container (OOMKilled), because memory cannot be throttledThe 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:

ObjectApplies toWhat it controls
ResourceQuotaThe whole namespace (all Pods combined)A total CPU and memory budget, plus object counts (Pods, Services, ConfigMaps, PersistentVolumeClaims and more)
LimitRangeEach individual container (or Pod)Default requests and limits, plus min and max boundaries
Use them together. Once a 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:

minikube addons enable metrics-server

Then check usage per node and per Pod:

# CPU and memory used by each node
kubectl top node

# CPU and memory used by each Pod in a namespace
kubectl top pod -n tenant-billing

List and describe the quotas and limit ranges in a namespace. describe shows the hard values next to what is used right now:

kubectl get resourcequota -n tenant-billing
kubectl describe resourcequota tenant-billing-quota -n tenant-billing

kubectl get limitrange -n tenant-billing
kubectl describe limitrange tenant-billing-limits -n tenant-billing

When a Pod is stuck in Pending, the events at the bottom of describe explain why:

kubectl describe pod catalog-api-oversized -n tenant-billing

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.

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish CatalogApi.csproj -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "CatalogApi.dll"]

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:

apiVersion: v1
kind: Namespace
metadata:
name: tenant-billing

Apply it:

kubectl apply -f tenant-billing-namespace.yaml

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:

apiVersion: v1
kind: Pod
metadata:
name: catalog-api
namespace: tenant-billing
labels:
app: catalog-api
spec:
containers:
- name: catalog-api
image: mcr.microsoft.com/dotnet/samples:aspnetapp
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_URLS
value: "http://+:8080"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"

Apply it and confirm it reaches the Running state:

kubectl apply -f catalog-api-pod.yaml
kubectl get pod -n tenant-billing

Expected output:

NAME READY STATUS RESTARTS AGE
catalog-api 1/1 Running 0 15s

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:

apiVersion: v1
kind: Pod
metadata:
name: catalog-api-oversized
namespace: tenant-billing
labels:
app: catalog-api
spec:
containers:
- name: catalog-api
image: mcr.microsoft.com/dotnet/samples:aspnetapp
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_URLS
value: "http://+:8080"
resources:
requests:
memory: "200Gi"
cpu: "250m"
limits:
memory: "200Gi"
cpu: "500m"

Apply it, then look at its status and ask why it is stuck:

kubectl apply -f catalog-api-oversized-pod.yaml
kubectl get pod -n tenant-billing
kubectl describe pod catalog-api-oversized -n tenant-billing

The status stays Pending, and the events show a message like this:

Warning FailedScheduling default-scheduler 0/1 nodes are available: 1 Insufficient memory.

That proves a request larger than any node can never be scheduled. Delete the Pod before you continue:

kubectl delete pod catalog-api-oversized -n tenant-billing

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:

apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-billing-quota
namespace: tenant-billing
spec:
hard:
requests.cpu: "1"
requests.memory: "1Gi"
limits.cpu: "2"
limits.memory: "2Gi"
pods: "10"
configmaps: "10"
services: "5"

Apply it and check how much of the budget is already used:

kubectl apply -f tenant-billing-resourcequota.yaml
kubectl describe resourcequota tenant-billing-quota -n tenant-billing

Expected output. The running catalog-api Pod already counts. The one ConfigMap is kube-root-ca.crt, which Kubernetes adds to every namespace:

Name: tenant-billing-quota
Namespace: tenant-billing
Resource Used Hard
-------- ---- ----
configmaps 1 10
limits.cpu 500m 2
limits.memory 512Mi 2Gi
pods 1 10
requests.cpu 250m 1
requests.memory 256Mi 1Gi
services 0 5

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:

apiVersion: v1
kind: Pod
metadata:
name: catalog-api-defaults
namespace: tenant-billing
labels:
app: catalog-api
spec:
containers:
- name: catalog-api
image: mcr.microsoft.com/dotnet/samples:aspnetapp
ports:
- containerPort: 8080

Try to create it:

kubectl apply -f catalog-api-defaults-pod.yaml

The quota covers CPU and memory, so the API server refuses the Pod. You will see an error similar to this:

Error from server (Forbidden): error when creating "catalog-api-defaults-pod.yaml":
pods "catalog-api-defaults" is forbidden: failed quota: tenant-billing-quota:
must specify limits.cpu for: catalog-api; limits.memory for: catalog-api;
requests.cpu for: catalog-api; requests.memory for: catalog-api

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:

  1. default: the limit applied automatically when a container sets none.
  2. defaultRequest: the request applied automatically when a container sets none.
  3. max: the highest value a container may declare, for its limit and its request.
  4. min: the lowest value a container may declare, for its request and its limit.

Save it as tenant-billing-limitrange.yaml:

apiVersion: v1
kind: LimitRange
metadata:
name: tenant-billing-limits
namespace: tenant-billing
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "250m"
memory: "256Mi"
max:
cpu: "1"
memory: "1Gi"
min:
cpu: "100m"
memory: "128Mi"

Apply it and view the resulting table:

kubectl apply -f tenant-billing-limitrange.yaml
kubectl describe limitrange tenant-billing-limits -n tenant-billing

Expected output:

Name: tenant-billing-limits
Namespace: tenant-billing
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container cpu 100m 1 250m 500m -
Container memory 128Mi 1Gi 256Mi 512Mi -

Step 8 — Retry the Pod without resources

Apply the same Pod again:

kubectl apply -f catalog-api-defaults-pod.yaml

This time it is accepted. Check what the LimitRange filled in for it:

kubectl get pod catalog-api-defaults -n tenant-billing -o jsonpath='{.spec.containers[0].resources}'

Expected output:

{"limits":{"cpu":"500m","memory":"512Mi"},"requests":{"cpu":"250m","memory":"256Mi"}}

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.

A LimitRange only affects new Pods. It is applied when a Pod is created, so Pods that already run keep their old values. A container that asks for more than max, for example a 2Gi memory limit here, is rejected with a Forbidden error.
Two layers of protection. The LimitRange keeps every single container within sensible bounds, and the ResourceQuota keeps the namespace as a whole within its budget.

Step 9 — Clean up

Remove the objects you no longer need. Always include -n so you target the right namespace:

kubectl delete limitrange tenant-billing-limits -n tenant-billing
kubectl delete resourcequota tenant-billing-quota -n tenant-billing
kubectl delete pod catalog-api catalog-api-defaults -n tenant-billing
kubectl delete namespace tenant-billing

Deleting the namespace alone would also remove everything inside it. The separate commands just show how to remove each object type.

Troubleshooting

SymptomLikely causeFix
Pod stays Pending with Insufficient memory or Insufficient cpuThe request is bigger than the free room on any nodeLower the request, or add nodes
Forbidden: exceeded quotaThe namespace budget is used upDelete unused Pods, lower requests, or raise the quota
failed quota: … must specify limits.cpuThe quota covers CPU or memory, but the Pod declares noneAdd requests and limits, or add a LimitRange with defaults
maximum memory usage per Container is 1GiA container asks for more than the LimitRange maxLower the value, or raise max
Container restarts with reason OOMKilledThe memory limit is too low for the appRaise the memory limit, or fix the memory leak
App is slow while CPU usage sits at the limitThe container is being throttledRaise 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.

  1. Requests guarantee a minimum and drive scheduling. A request larger than any node leaves the Pod stuck in Pending.
  2. Limits cap consumption. Hitting the CPU limit throttles the container, while hitting the memory limit kills it with OOMKilled.
  3. 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.
  4. A LimitRange supplies defaults and min/max boundaries, so no container is left unconstrained and no Pod is rejected for forgetting its resources.
  5. Define a ResourceQuota and a LimitRange for 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.