Resource Limits & Denial-of-Service Prevention
The Noisy Neighbour Problem
Why Resource Limits Are a Security Control
An application without resource limits is a ticking clock. It might behave well under normal load — but under a bug, a traffic spike, or a deliberate attack, it will consume all available CPU and memory on the node, evicting every other pod.
This is a denial-of-service condition from inside your own cluster. It can be caused by:
- A memory leak in your application
- A recursive loop or runaway goroutine
- An attacker who has compromised one pod and is deliberately starving others
Kubernetes addresses this with three things:
- Resource requests — the scheduler uses these to decide which node a pod runs on
- Resource limits — the kernel kills processes that exceed these
- Quality of Service (QoS) classes — Kubernetes's priority system for which pods survive when a node runs out of resources
Create the Demo Namespace
kubectl create namespace limits-demo
Deploy a Pod WITHOUT Limits
cat > unlimited-pod.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: unlimited-app
namespace: limits-demo
spec:
containers:
- name: app
image: public.ecr.aws/nginx/nginx:1.25-alpine
EOF
kubectl apply -f unlimited-pod.yaml
kubectl wait --for=condition=Ready pod/unlimited-app -n limits-demo --timeout=60s
Check Its QoS Class
kubectl get pod unlimited-app -n limits-demo \
-o jsonpath='{.status.qosClass}'
Expected: BestEffort
BestEffort is Kubernetes's lowest QoS class — these pods are the first to be evicted when the node runs low on memory. They have no resource guarantees whatsoever.
# Prove there are no limits
kubectl get pod unlimited-app -n limits-demo \
-o jsonpath='{.spec.containers[0].resources}'
Expected: {} — completely empty.
The Three QoS Classes
┌─────────────────────────────────────────────────────────┐
│ Guaranteed (highest priority — last to be evicted) │
│ requests == limits for ALL containers │
├─────────────────────────────────────────────────────────┤
│ Burstable (middle priority) │
│ at least one container has a request OR limit set │
├─────────────────────────────────────────────────────────┤
│ BestEffort (lowest priority — first evicted) │
│ NO requests or limits set on ANY container │
└─────────────────────────────────────────────────────────┘
Under memory pressure, the node's Out-of-Memory (OOM) killer evicts BestEffort pods first, then Burstable, and only touches Guaranteed pods as a last resort.
Setting Limits, LimitRanges & ResourceQuotas
Burstable vs Guaranteed
Deploy the two correctly configured pods and observe how QoS class changes with resource settings:
cat > burstable-pod.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: burstable-app
namespace: limits-demo
spec:
containers:
- name: app
image: public.ecr.aws/nginx/nginx:1.25-alpine
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "500m"
memory: "256Mi"
EOF
kubectl apply -f burstable-pod.yaml
kubectl wait --for=condition=Ready pod/burstable-app -n limits-demo --timeout=60s
kubectl get pod burstable-app -n limits-demo -o jsonpath='{.status.qosClass}'
Expected: Burstable
Requests (100m CPU, 64Mi memory) differ from limits (500m, 256Mi). The pod is guaranteed at least the request and can burst up to the limit.
cat > guaranteed-pod.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: guaranteed-app
namespace: limits-demo
spec:
containers:
- name: app
image: public.ecr.aws/nginx/nginx:1.25-alpine
resources:
requests:
cpu: "200m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "128Mi"
EOF
kubectl apply -f guaranteed-pod.yaml
kubectl wait --for=condition=Ready pod/guaranteed-app -n limits-demo --timeout=60s
kubectl get pod guaranteed-app -n limits-demo -o jsonpath='{.status.qosClass}'
Expected: Guaranteed
Requests equal limits for all resources. Kubernetes guarantees this pod its allocation; it will not be evicted until Guaranteed eviction begins (near-OOM state).
Enforce Limits Namespace-Wide with LimitRange
A LimitRange sets default limits for every container in a namespace — so a developer cannot forget to set limits, and a malicious pod cannot be deployed without them:
cat > limitrange.yaml << 'EOF'
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: limits-demo
spec:
limits:
- type: Container
default:
cpu: "200m"
memory: "128Mi"
defaultRequest:
cpu: "100m"
memory: "64Mi"
max:
cpu: "1"
memory: "512Mi"
min:
cpu: "50m"
memory: "32Mi"
EOF
kubectl apply -f limitrange.yaml
kubectl describe limitrange default-limits -n limits-demo
Now any container deployed without explicit resources gets the defaults. And any container requesting more than max is rejected at admission time.
Cap Total Namespace Consumption with ResourceQuota
A LimitRange controls individual containers. A ResourceQuota caps the total resources a namespace can consume — preventing a compromised namespace from using the whole cluster:
cat > resourcequota.yaml << 'EOF'
apiVersion: v1
kind: ResourceQuota
metadata:
name: ns-quota
namespace: limits-demo
spec:
hard:
requests.cpu: "2"
requests.memory: "1Gi"
limits.cpu: "4"
limits.memory: "2Gi"
pods: "10"
secrets: "20"
services: "5"
EOF
kubectl apply -f resourcequota.yaml
kubectl describe resourcequota ns-quota -n limits-demo
Expected output shows current vs. allowed:
Resource Used Hard
-------- ---- ----
limits.cpu ... 4
limits.memory ... 2Gi
pods 3 10
requests.cpu ... 2
requests.memory ... 1Gi
secrets ... 20
services 0 5
Security Summary
| Control | Protects Against |
|---|---|
resources.limits | Single container consuming all node resources |
LimitRange | Developers forgetting to set limits; pods deployed without resource controls |
ResourceQuota | A compromised namespace claiming the entire cluster's resources |
Guaranteed QoS | Critical workloads surviving under node memory pressure |
Cleanup
kubectl delete namespace limits-demo
- Previous lesson
- Your First Deployment — Security Defaults from Day One
- Next lesson
- Rolling Updates — Zero-Downtime CVE Patching