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
RequestsLimits
PurposeScheduling 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 exceededN/A (always gets requests)Throttled (CFS quota)
Memory exceededN/A (always gets requests)OOMKilled
Can be omitted?Yes (but BestEffort QoS)Yes (unlimited usage)
The fundamental asymmetry: CPU is compressible, memory is not.
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

ResourceUnitExamples
CPUCores (decimal)0.5 = half a core
Millicores500m = half a core, 100m = 0.1 core
Whole cores2 = 2 full cores
MemoryBytes (decimal SI)128M = 128,000,000 bytes
Bytes (binary)128Mi = 128 × 1024² = 134,217,728 bytes
Other units1Gi = 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 requests are guarantees under contention. If the node has spare CPU, a container can burst beyond its request (up to its limit). Requests only kick in when multiple containers compete — then each gets at least their requested share.

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
CPU Throttling: limit=500m (50ms per 100ms period) 50ms used Period 1: OK 🛑 Period 2: Throttled! 30ms used Period 3: Under limit Throttling adds latency spikes — not OOM, but P99 latency increases
The CPU limits debate: Many production teams set CPU requests but NOT CPU limits. Rationale: CPU throttling causes latency spikes (visible in P99) even when the node has idle CPU available. Without limits, Pods can burst freely when there's spare capacity. The risk: one Pod can starve others during peak. The compromise: set requests generously, omit limits, and monitor for noisy neighbors. Google's Borg and many SRE teams follow this pattern.

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?

CountsDoesn't Count
Resident Set Size (RSS) — actual pages in RAMFile-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)
Memory is non-compressible. Unlike CPU (where the kernel just schedules less time), memory can't be "squeezed." If a process allocates 600Mi and the limit is 512Mi, the only option is to kill the process. This is why memory limits are more dangerous than CPU limits — an OOMKill disrupts the workload entirely.

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).

Always set memory limits in production. Without them, a memory leak in one Pod can consume all node memory, causing the kernel OOM killer to kill random processes (potentially kubelet itself or other critical Pods). Memory limits contain the blast radius.

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

FieldEffect
defaultAuto-injects limits if container doesn't specify them
defaultRequestAuto-injects requests if container doesn't specify them
minReject Pod creation if requests/limits below minimum
maxReject Pod creation if requests/limits above maximum
maxLimitRequestRatioMax ratio of limit/request (prevents huge burst allowance)
CKA exam: LimitRange silently injects defaults. If you create a Pod without resource requests, and a LimitRange exists, the Pod gets the 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
Right-sizing is iterative. Start with generous estimates (prevent OOM), observe actual usage with monitoring (Prometheus, Datadog, kubectl top), then tighten. Over-requesting wastes cluster resources (under-utilized nodes). Under-requesting causes evictions and scheduling failures. Tools like Goldilocks and VPA automate this.

Summary

ConceptKey Point
RequestsScheduling guarantee — scheduler uses this to place Pods
LimitsHard ceiling — enforced by cgroups at runtime
CPU exceeded limitThrottled (CFS quota) — slows down, doesn't kill
Memory exceeded limitOOMKilled — process killed (exit code 137)
CPU is compressibleCan be shared/throttled without harm
Memory is incompressibleCan't be reclaimed without killing — OOM is the only option
No limits setPod can use all node resources (dangerous for memory)
LimitRangeNamespace-level defaults and min/max constraints
CPU units1000m = 1 core; 100m = 0.1 core
Memory unitsUse Mi/Gi (binary). 128Mi = 128 × 1024²
Best practiceAlways 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?

Yes. The container can burst up to its limit (1000m). CPU requests only matter under contention — when the node has spare capacity, the container can use up to its limit freely. It would only be throttled if it tried to exceed 1000m.

Q2: A container has limits.memory: 512Mi and its RSS reaches 520Mi. What happens?

The kernel's OOM killer immediately kills the process (SIGKILL, exit code 137). Memory limits are hard — there's no "throttling" for memory. The kubelet then restarts the container based on the Pod's restartPolicy. If this happens repeatedly → CrashLoopBackOff.

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?

Under contention, CPU is divided proportionally to requests: Pod A gets 2/3 of available CPU (≈2.67 cores) and Pod B gets 1/3 (≈1.33 cores). Without limits, both can burst beyond their requests — the total is divided by CFS share ratio. The 4th core's share is split 2:1.

Q4: Why do many production teams set memory limits but NOT CPU limits?

Memory limits: Essential because a memory leak without a limit can OOMKill other Pods or crash the node. The blast radius is uncontained.
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?

256Mi. The LimitRanger admission controller automatically injects the default limit. The Pod spec will show 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?

CPU limits use CFS quota per period — they're enforced regardless of node-wide utilization. If the limit is 500m, the container gets 50ms per 100ms period — even if the other 3.5 cores are idle. The limit is a hard per-container cap, not a share of available resources. This is the core argument against CPU limits: they waste available capacity.