open atlas
↑ Back to track
Deployment & Infra DEP · 10 · 03

Probes and resource limits

Readiness gates traffic, liveness restarts, startup shields slow boots; requests schedule and limits cap — over-CPU throttles, over-memory is OOMKilled. Set them wrong and a healthy app CrashLoops or gets evicted.

DEP Middle ◷ 17 min
Level
FoundationsJuniorMiddleSenior

A service ships fine, then at 3am pods start flapping. The dashboard shows CrashLoopBackOff climbing across every replica at once. Nobody pushed code. The cause: a livenessProbe with initialDelaySeconds: 5 on an app that takes 25 seconds to warm its JIT and connect to the database. Every boot, the probe failed three times before the app was ready, Kubernetes ruled the container dead and restarted it — forever. A perfectly healthy app was being executed by its own health check. The fix was four lines of YAML. The lesson took the night.

Three probes that answer three different questions

Before you configure any probe, pin down which question you are actually trying to answer — because the three probes look similar but have opposite consequences when they fire.

Kubernetes does not assume a running container is a working one. It asks, repeatedly, via probes — and the kind of probe decides what happens when the answer is “no”. Confusing them is the single most common source of self-inflicted outages in this lesson.

Readiness asks can this pod serve traffic right now? When it fails, Kubernetes removes the pod from the Service’s endpoints — traffic stops flowing to it — but it does not restart the container. The pod stays up, just out of rotation, and rejoins the moment readiness passes again. This is the gate for rollouts (a new pod gets no traffic until it is genuinely ready) and for transient overload (a pod under GC pressure can shed traffic without dying). Make readiness reflect real readiness: the app has bound its port, run migrations, and confirmed its critical dependencies are reachable.

Liveness asks is this container still alive, or is it wedged? When it fails (past the threshold), Kubernetes restarts the container. This exists for one job: recovering from unrecoverable states — a deadlock, an event loop that stopped turning, a memory leak that hung the process. Liveness is a sledgehammer. If a slow-but-healthy app, or a temporary dependency blip, trips it, you get the restart loop from the hook.

Startup asks has this container finished booting yet? It runs first; while it is still failing, the liveness and readiness probes are held off entirely. Once startup passes once, it never runs again and the other two take over. This is the correct shield for slow-starting apps — legacy JVMs, anything with a long warm-up — so you can keep liveness aggressive for running failures without it murdering the app during boot.

containers:
- name: api
  image: registry.example.com/api:1.4.0
  startupProbe:            # boot shield: up to 30 × 10s = 5 min to come alive
    httpGet: { path: /healthz, port: 8080 }
    periodSeconds: 10
    failureThreshold: 30
  readinessProbe:          # gate traffic; failing removes from Service, no restart
    httpGet: { path: /ready, port: 8080 }
    periodSeconds: 5
    failureThreshold: 3
  livenessProbe:           # restart if wedged; failing kills + restarts the container
    httpGet: { path: /healthz, port: 8080 }
    periodSeconds: 10
    failureThreshold: 3
Why this works

Two probe traps bite seniors. First, liveness pointing at a dependency: if your /healthz checks the database and the database has a 30-second hiccup, every pod’s liveness fails at once and Kubernetes restarts your entire fleet — turning a brief dependency blip into a self-inflicted cascading outage. Liveness should test only the process itself; let readiness reflect dependencies. Second, reusing one endpoint for both: if /healthz does heavy dependency checks and you wire liveness to it, you have quietly made liveness depend on the world. Keep a cheap, local liveness endpoint and a richer readiness one.

requests and limits: scheduling versus the hard cap

Resources have two numbers per container, and they do completely different jobs.

requests are what the pod is guaranteed and what the scheduler uses for placement: Kubernetes will only put the pod on a node that has at least requests.cpu and requests.memory free (summed across the node). It is a reservation, not a ceiling. Set requests to the typical, steady-state usage of the container.

limits are the hard cap the container may not exceed at runtime — and what happens when it tries is the part people get wrong, because CPU and memory behave oppositely:

  • CPU over limit → throttled, not killed. CPU is compressible. Exceed limits.cpu and the kernel’s CFS scheduler simply gives the container fewer time-slices; the app keeps running but gets slow. An under-set CPU limit shows up as mysterious latency, not crashes.
  • Memory over limit → OOMKilled. Memory is incompressible — you cannot give a process “less” RAM than it has allocated. Exceed limits.memory and the kernel’s OOM killer terminates the container with exit code 137; you see OOMKilled and a restart. An under-set memory limit shows up as crashes, not slowness.
resources:
  requests:        # guaranteed + drives scheduling — set ≈ typical usage
    cpu: "250m"
    memory: "256Mi"
  limits:          # hard cap — protects the node from a runaway container
    cpu: "1"       # over → CFS throttling (slow, not killed)
    memory: "512Mi"  # over → OOMKilled (exit 137, container restarts)

The senior rule: set requests ≈ typical so the scheduler packs the node honestly, and set limits high enough to absorb spikes but low enough to protect the node. Then watch the two failure signals — recurring OOMKilled means your memory limit is too low (or there is a leak); sustained CPU throttling means your CPU limit is too low. Tune from real metrics, not guesses.

QoS classes decide who dies first

The relationship between requests and limits silently assigns each pod a Quality of Service class, and that class sets the eviction order when a node runs out of memory:

  • Guaranteed — every container has requests == limits for both CPU and memory. Evicted last. Use for latency-critical, stateful workloads.
  • Burstable — at least one request set, but not equal to limits (the common case). Evicted after BestEffort, ordered by how far over its requests it is using.
  • BestEffort — no requests or limits at all. First to be evicted, first to be OOM-killed under node pressure. Fine only for throwaway batch work.

Together, these three classes mean the scheduler is not the only thing that decides a pod’s fate — the kernel’s OOM killer is too, and it uses the class to pick who dies first. Without explicit requests and limits you get BestEffort by accident, which makes your pod the first casualty whenever a noisy neighbor spikes.

Knobrequestslimits
RoleGuaranteed reservation; drives schedulingHard runtime ceiling per container
Used by scheduler?Yes — pod only lands on a node with this much freeNo — enforced at runtime by the kernel
Over CPUn/a (request is a floor)CFS throttling — app slows, stays up
Over memoryn/aOOMKilled (exit 137) — container restarts
Set it to≈ typical steady-state usageHigh enough for spikes, low enough to protect the node
Pick the best fit

A slow-booting JVM service (25s warm-up) needs to recover from rare deadlocks without flapping on every restart. How do you configure its probes?

Quiz

A container is repeatedly terminated with exit code 137 under load, then restarted. What does that tell you?

Recall before you leave
  1. 01
    A healthy app is stuck in CrashLoopBackOff right after deploy. Walk through the probe misconfiguration that causes it and the exact fix.
  2. 02
    What is the difference between a container being OOMKilled and being throttled, and what does each tell you to change?
Recap

Kubernetes keeps pods healthy and right-sized with two independent tools, and the failures come from confusing the cases. Now when you see a CrashLoopBackOff on a freshly deployed service, your first question is: does the liveness probe have room for the actual boot time — and is it pointing at a dependency? There are three probes answering three questions: readiness (‘can it serve?’ — failing removes the pod from the Service’s endpoints but does not restart it, the gate for rollouts and shedding transient load), liveness (‘is it wedged?’ — failing restarts the container, for recovering deadlocks only), and startup (‘has it booted?’ — running first and holding liveness and readiness off until boot completes, the shield for slow-starting apps). The two pitfalls that take down healthy fleets are an over-aggressive liveness probe that CrashLoops a slow-but-healthy app, and a liveness probe pointed at a dependency that restarts every pod at once during a brief dependency blip — keep liveness cheap and local, let readiness reflect dependencies. On resources, requests are the guaranteed reservation the scheduler uses for placement (set them near typical usage), and limits are the hard runtime cap — but the two resources enforce oppositely: exceed the CPU limit and you are throttled by CFS (slow, not killed), exceed the memory limit and you are OOMKilled with exit code 137 (killed and restarted). The ratio of requests to limits assigns a QoS class — Guaranteed (requests == limits, evicted last), Burstable, or BestEffort (no limits, evicted first) — which sets the order pods die under node pressure. Watch for recurring OOMKills (memory limit too low) and sustained throttling (CPU limit too low) and tune from metrics, not guesses.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.