Kubernetes Resource Limits and Requests Guide
How to set CPU and memory requests and limits in Kubernetes correctly, avoiding OOMKills, CPU throttling and wasted cluster capacity.

Resource requests and limits are the most misconfigured settings in Kubernetes. Get them wrong and your pods get OOMKilled in production, throttled to unusable CPU speeds, or waste expensive cluster capacity by reserving resources they never use. The defaults are not safe — pods without requests are the first to be evicted, and pods without limits can consume an entire node.
Understanding how the scheduler uses requests and how the kernel enforces limits is the difference between a stable cluster and one that crashes under load.
Requests vs Limits
Requests and limits serve different purposes. Requests tell the scheduler how much capacity a pod needs. Limits tell the kernel the maximum a pod is allowed to use.
# Pod resource configuration
apiVersion: v1
kind: Pod
metadata:
name: api-server
spec:
containers:
- name: api
image: api-server:latest
resources:
requests:
cpu: "250m" # Scheduler reserves 0.25 CPU cores
memory: "256Mi" # Scheduler reserves 256 MiB
limits:
cpu: "1000m" # Kernel throttles above 1 CPU core
memory: "512Mi" # Kernel OOMKills above 512 MiB# ❌ No requests or limits — dangerous
apiVersion: v1
kind: Pod
spec:
containers:
- name: api
image: api-server:latest
# No resource constraints!
# - Scheduler doesn't know how much capacity this needs
# - Pod can consume unlimited CPU and memory
# - Pod is BestEffort QoS — first to be evicted
# - One runaway pod can crash an entire node
# ✅ Properly configured resources
apiVersion: v1
kind: Pod
spec:
containers:
- name: api
image: api-server:latest
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"
# Scheduler places pod on a node with enough capacity
# Pod is Burstable QoS — better eviction priority
# Memory is capped — no OOM on the node itselfHow CPU Requests and Limits Work
CPU is a compressible resource. When a pod exceeds its CPU limit, the kernel throttles it — the pod runs slower but does not get killed. When a pod is below its request, the scheduler should have placed it on a node with enough spare CPU.
# CPU is measured in millicores
# 1000m = 1 full CPU core
# 250m = 0.25 cores (25% of one core)
# 100m = 0.1 cores (10% of one core)
resources:
requests:
cpu: "250m" # Guaranteed 25% of a core
limits:
cpu: "1000m" # Can burst to 100% of a core when available// The CPU throttling problem — a real production issue
// Application handles 100 requests/second at 250m CPU
// Under load spike: 400 requests/second needs 800m CPU
// CPU limit is 1000m — no problem
// But CFS (Completely Fair Scheduler) enforces limits per 100ms period
// At 1000m limit: pod gets 100ms of CPU per 100ms period
// A single request that takes 15ms blocks the entire quota for 15ms
// Other requests wait, causing latency spikes
// This is "CPU throttling" — the pod has burst capacity
// but the CFS period enforcement creates micro-pauses
// Monitoring commands:
// kubectl top pod api-server
// kubectl describe pod api-server | grep -A5 "Limits"How Memory Limits Work
Memory is incompressible. When a pod exceeds its memory limit, the kernel OOMKills it immediately. There is no throttling for memory — you are either within limits or your process is dead.
# Memory is measured in bytes with suffixes
# Mi = mebibytes (1 Mi = 1,048,576 bytes)
# Gi = gibibytes
# M = megabytes (1 M = 1,000,000 bytes)
# Use Mi/Gi to be precise
resources:
requests:
memory: "256Mi" # Scheduler reserves this much
limits:
memory: "512Mi" # OOMKill above this threshold# Diagnosing OOMKill events
kubectl describe pod api-server
# Look for:
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
# Check memory usage over time
kubectl top pod api-server --containers
# Check node memory pressure
kubectl describe node <node-name> | grep -A5 "Conditions"// Common causes of OOMKill in Node.js applications
// 1. Memory leaks — unbounded caches, event listener accumulation
const cache = new Map(); // Grows forever without eviction
// 2. Large request payloads loaded entirely into memory
app.post('/upload', (req, res) => {
const body = req.body; // 500MB JSON payload = OOMKill
});
// 3. Heap size exceeds container limit
// Node.js default max heap ≈ 1.7GB (V8 default)
// Container limit set to 512Mi → OOMKill before V8 GC kicks in
// Fix: set --max-old-space-size to ~75% of the container memory limit
// Container limit: 512Mi → --max-old-space-size=384
// This gives V8 room to garbage collect before hitting the limitQoS Classes
Kubernetes assigns a Quality of Service class to each pod based on its resource configuration. This determines eviction priority when a node runs out of resources.
# BestEffort — no requests or limits (evicted first)
resources: {}
# Burstable — requests set, different from limits
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"
# Guaranteed — requests equal limits (evicted last)
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "500m"
memory: "512Mi"// Choosing the right QoS class
const qosGuide = {
guaranteed: {
useFor: [
'Production databases',
'Core API servers',
'Anything where eviction means downtime',
],
tradeoff: 'No bursting — you pay for reserved capacity',
},
burstable: {
useFor: [
'Most web applications',
'Background workers',
'Services with variable load',
],
tradeoff: 'Can be evicted under node pressure (after BestEffort)',
},
bestEffort: {
useFor: [
'Development/staging namespaces',
'Batch jobs that can be retried',
'Non-critical cron jobs',
],
tradeoff: 'First to be evicted — no guarantees',
},
};Sizing Strategy
Setting the right resource values requires measurement. Start with generous limits, observe actual usage, then tighten.
# Step 1: Deploy with generous limits, monitor for 1-2 weeks
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "2000m"
memory: "1Gi"
# Step 2: Check actual usage with metrics
kubectl top pod -l app=api-server --containers
# Step 3: Query Prometheus for P99 usage over 7 days
# CPU: rate(container_cpu_usage_seconds_total[5m])
# Memory: container_memory_working_set_bytes
# Step 4: Set requests to P95 usage, limits to P99 + 25% headroom
# If P95 CPU is 200m and P99 is 350m:
resources:
requests:
cpu: "200m" # P95 — covers normal operation
memory: "256Mi"
limits:
cpu: "500m" # P99 + headroom — covers spikes
memory: "384Mi"# ❌ Common sizing mistakes
resources:
requests:
cpu: "2000m" # Massively over-provisioned
memory: "2Gi" # Wastes cluster capacity
limits:
cpu: "2000m"
memory: "2Gi"
# Pod uses 100m CPU and 200Mi memory
# You're paying for 20x the CPU you need
# ✅ Right-sized based on observed usage
resources:
requests:
cpu: "150m" # Based on P95 observed usage
memory: "256Mi" # Based on steady-state usage + 20%
limits:
cpu: "500m" # Room for burst
memory: "384Mi" # Room for spikes, triggers GC before OOMLimitRange and ResourceQuota
Enforce sane defaults at the namespace level so teams cannot accidentally deploy pods without limits or with excessive requests.
# LimitRange — default limits for the namespace
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: production
spec:
limits:
- default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "4000m"
memory: "4Gi"
min:
cpu: "50m"
memory: "64Mi"
type: Container
---
# ResourceQuota — cap total resource usage per namespace
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-budget
namespace: team-alpha
spec:
hard:
requests.cpu: "10" # Total CPU requests across all pods
requests.memory: "20Gi"
limits.cpu: "20"
limits.memory: "40Gi"
pods: "50" # Max number of podsKey Takeaways
- Always set both requests and limits — pods without them are BestEffort QoS and first to be evicted under pressure
- CPU throttles, memory kills — exceeding CPU limits slows your pod; exceeding memory limits terminates it instantly
- Set Node.js max-old-space-size to 75% of memory limit — gives V8 room to garbage collect before the kernel OOMKills
- Size based on observed usage — deploy with generous limits, monitor P95/P99 for one to two weeks, then right-size
- Use LimitRange for namespace defaults — prevent teams from deploying pods without resource constraints
- Guaranteed QoS for critical workloads — set requests equal to limits for databases and core services that must not be evicted


