Resource requests and limits are the foundation of Kubernetes resource management. They determine where Pods are scheduled (requests), how much they can use (limits), and what happens when they exceed those bounds (throttling or OOMKill). Getting them right is the difference between a stable cluster and 3 AM pager alerts.
1. Requests vs Limits
spec:
containers:
- name: app
image: myapp:latest
resources:
requests: # GUARANTEED minimum
cpu: 250m # 250 millicores = 0.25 CPU cores
memory: 256Mi # 256 mebibytes
limits: # MAXIMUM allowed
cpu: 1000m # 1 full CPU core
memory: 512Mi # 512 mebibytes
| Requests | Limits | |
|---|---|---|
| Purpose | Scheduling guarantee — "I need at least this much" | Hard cap — "Never use more than this" |
| Used by scheduler? | ✅ Yes — determines if Pod fits on node | ❌ No — scheduler ignores limits |
| CPU exceeded | N/A (always gets requests) | Throttled (CFS quota) |
| Memory exceeded | N/A (always gets requests) | OOMKilled |
| Can be omitted? | Yes (but BestEffort QoS) | Yes (unlimited usage) |
• CPU: If a container exceeds its limit, it's throttled (slowed down) — it still runs, just slower.
• Memory: If a container exceeds its limit, it's killed (OOMKill) — the kernel has no choice because memory can't be "taken back" from a process without killing it.
Resource Units
| Resource | Unit | Examples |
|---|---|---|
| CPU | Cores (decimal) | 0.5 = half a core |
| Millicores | 500m = half a core, 100m = 0.1 core | |
| Whole cores | 2 = 2 full cores | |
| Memory | Bytes (decimal SI) | 128M = 128,000,000 bytes |
| Bytes (binary) | 128Mi = 128 × 1024² = 134,217,728 bytes | |
| Other units | 1Gi = 1 gibibyte, 500Ki = 500 kibibytes |
1 CPU = 1 AWS vCPU = 1 GCP Core = 1 Azure vCore = 1 hyperthread. 100m is the minimum meaningful request. Always use Mi/Gi (binary) for memory — it matches how the kernel measures RSS.
2. CPU — How It Actually Works
CPU Requests → CFS Shares
CPU requests map to Linux CFS (Completely Fair Scheduler) shares. They're a relative weight, not an absolute reservation:
# Pod A: requests.cpu = 500m → CFS shares = 512 # Pod B: requests.cpu = 250m → CFS shares = 256 # When both compete for CPU on the same core: # Pod A gets 2/3 of CPU time (512 / (512+256)) # Pod B gets 1/3 of CPU time (256 / (512+256)) # When Pod B is idle, Pod A can use ALL available CPU # Requests only matter under contention
CPU Limits → CFS Quota (Throttling)
CPU limits map to CFS bandwidth control. The kernel enforces a hard ceiling:
# limits.cpu = 1000m means: # In every 100ms period (cfs_period_us), this container can use 100ms of CPU time # If it tries to use more → throttled (process is paused until next period) # limits.cpu = 500m: # 50ms of CPU per 100ms period # Check throttling: cat /sys/fs/cgroup/cpu/cpu.stat # nr_throttled 12847 ← times the container was throttled # throttled_time 8234567890 ← nanoseconds spent throttled
3. Memory — How It Actually Works
Memory Requests → Scheduling Only
Memory requests tell the scheduler: "This Pod needs at least 256Mi." The scheduler won't place the Pod on a node unless allocatable memory - sum(requests of existing Pods) ≥ 256Mi. But requests don't enforce anything at runtime — a Pod can use more than its request.
Memory Limits → Cgroup Hard Limit (OOMKill)
# limits.memory = 512Mi means: # Kernel sets cgroup memory.limit_in_bytes = 536870912 # If the process's RSS exceeds this → OOMKilled immediately # OOMKill event visible in: kubectl describe pod myapp # Last State: Terminated # Reason: OOMKilled # Exit Code: 137 # 128 + 9 (SIGKILL)
What Counts Toward Memory Usage?
| Counts | Doesn't Count |
|---|---|
| Resident Set Size (RSS) — actual pages in RAM | File-backed pages (page cache) that can be evicted |
| Anonymous memory (heap, stack) | Shared memory mapped from files (can be reclaimed) |
| tmpfs volumes (emptyDir medium: Memory) | Kernel memory (partially — depends on cgroup v1 vs v2) |
OOMKill Priority (within a cgroup)
# When a container hits its memory limit: # The kernel's OOM killer picks a process within that cgroup to kill # Usually the process with the highest memory usage (oom_score) # In single-process containers (most K8s Pods): the main process dies # → Container exits with code 137 → kubelet restarts it
Node-Level OOM (Eviction)
Separate from container-level OOMKill: if the node's available memory drops below the kubelet's eviction threshold (default 100Mi), the kubelet evicts Pods based on their QoS class (BestEffort first, then Burstable exceeding requests, then Guaranteed).
4. LimitRange — Namespace-Level Defaults & Constraints
A LimitRange sets default requests/limits and min/max constraints for Pods in a namespace. Enforced by the LimitRanger admission controller.
apiVersion: v1
kind: LimitRange
metadata:
name: resource-constraints
namespace: production
spec:
limits:
- type: Container
default: # Applied if Pod doesn't specify limits
cpu: 500m
memory: 256Mi
defaultRequest: # Applied if Pod doesn't specify requests
cpu: 100m
memory: 128Mi
min: # Minimum allowed (reject if below)
cpu: 50m
memory: 64Mi
max: # Maximum allowed (reject if above)
cpu: 4000m
memory: 4Gi
- type: Pod
max: # Max for entire Pod (sum of containers)
cpu: 8000m
memory: 8Gi
What LimitRange Enforces
| Field | Effect |
|---|---|
default | Auto-injects limits if container doesn't specify them |
defaultRequest | Auto-injects requests if container doesn't specify them |
min | Reject Pod creation if requests/limits below minimum |
max | Reject Pod creation if requests/limits above maximum |
maxLimitRequestRatio | Max ratio of limit/request (prevents huge burst allowance) |
defaultRequest values automatically. Check with kubectl describe pod — you'll see resources you didn't set.
5. Production Best Practices
The Recommended Pattern
# For stateless web services:
resources:
requests:
cpu: 100m # Based on observed average usage
memory: 256Mi # Based on observed RSS
limits:
# cpu: omitted # Allow bursting (no throttling)
memory: 512Mi # 2x request — hard cap to prevent OOM impact on node
# For databases:
resources:
requests:
cpu: 1000m # Generous — databases need consistent CPU
memory: 2Gi # Observed working set
limits:
cpu: 2000m # Allow some burst
memory: 2Gi # Same as request (Guaranteed QoS — no OOM surprise)
How to Determine the Right Values
# 1. Start without limits, deploy, observe actual usage: kubectl top pods # Current usage snapshot # NAME CPU(cores) MEMORY(bytes) # web-abc 45m 180Mi # 2. Use metrics-server or Prometheus for historical data: # Look at P95 CPU over 7 days → set request at P50, limit at P99 # Look at max memory over 7 days → set request at average, limit at max + 20% # 3. Use VPA (Vertical Pod Autoscaler) in recommendation mode: kubectl get vpa web-vpa -o yaml # → recommendation: requests.cpu=120m, requests.memory=200Mi
Summary
| Concept | Key Point |
|---|---|
| Requests | Scheduling guarantee — scheduler uses this to place Pods |
| Limits | Hard ceiling — enforced by cgroups at runtime |
| CPU exceeded limit | Throttled (CFS quota) — slows down, doesn't kill |
| Memory exceeded limit | OOMKilled — process killed (exit code 137) |
| CPU is compressible | Can be shared/throttled without harm |
| Memory is incompressible | Can't be reclaimed without killing — OOM is the only option |
| No limits set | Pod can use all node resources (dangerous for memory) |
| LimitRange | Namespace-level defaults and min/max constraints |
| CPU units | 1000m = 1 core; 100m = 0.1 core |
| Memory units | Use Mi/Gi (binary). 128Mi = 128 × 1024² |
| Best practice | Always set memory limits. CPU limits are debated — many teams omit them. |
📝 Quiz: Resource Requests & Limits
Q1: A container has requests.cpu: 250m and limits.cpu: 1000m. The node has spare CPU. Can the container use 800m?
Q2: A container has limits.memory: 512Mi and its RSS reaches 520Mi. What happens?
Q3: A node has 4 CPU cores allocatable. Pod A requests 2 CPU, Pod B requests 1 CPU. Both have no limits. Both are CPU-bound. How is CPU divided?
Q4: Why do many production teams set memory limits but NOT CPU limits?
CPU limits omitted: CPU throttling causes latency spikes (P99 increases) even when the node has idle CPU. Without limits, Pods burst freely using spare CPU. The downside (noisy neighbor) is manageable with proper requests and monitoring.
Q5: A namespace has a LimitRange with default.memory: 256Mi. A developer creates a Pod with no resource spec. What memory limit does the Pod get?
limits.memory: 256Mi even though the developer didn't set it. Check with kubectl describe pod. If defaultRequest is also set, requests are injected too.Q6: A container is being throttled (high nr_throttled in cpu.stat) even though node CPU utilization is only 30%. How is this possible?