Kubernetes Pod Lifecycle
Pod phases and container states explained. Pending to Running to Succeeded, restart policy and backoff, termination and SIGTERM, and how to read a Pod's status.
A Pod has a coarse phase and each of its containers has a finer state. Reading both is how you turn "it is broken" into a specific thing to fix.
Phases
| Phase | Meaning |
|---|---|
Pending | Accepted by the API server but not yet running (being scheduled, or pulling images) |
Running | Bound to a node, all containers created, at least one running |
Succeeded | All containers exited 0 and will not restart |
Failed | All containers terminated, at least one in failure |
Unknown | The node stopped reporting |
Phases only move forward. A Pod never returns from Failed to Running. It is replaced by a new Pod, with a new name.
kubectl get pod <name> -o jsonpath='{.status.phase}'
Running does not mean healthy. A Pod showing Running with READY 0/1 is failing its readiness probe and receiving no traffic. The READY column, not the phase, is what tells you it is serving.
Container states
Each container is in one of three states, and this is where the detail lives:
kubectl get pod <name> -o jsonpath='{.status.containerStatuses[0].state}' | jq
{
"waiting": {
"reason": "CrashLoopBackOff",
"message": "back-off 40s restarting failed container=api"
}
}
| State | Common reasons |
|---|---|
Waiting | ContainerCreating, ImagePullBackOff, CrashLoopBackOff, CreateContainerConfigError |
Running | Started and not exited |
Terminated | Completed, Error, OOMKilled, ContainerCannotRun |
The lastState field holds the previous container instance, which is where you find out why something crashed:
kubectl get pod <name> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
OOMKilled
That one command distinguishes a memory limit problem from an application bug, and it is much faster than reading logs.
Startup order
- The Pod is created and is
Pendingwith no node assigned. - The scheduler picks a node and writes it to the Pod.
- The kubelet on that node pulls images and creates volumes.
- Init containers run to completion, in order. If one fails, it is retried per the restart policy and the app containers never start.
- App containers start. The startup probe, if present, suppresses liveness and readiness until it passes.
- Once readiness passes, the Pod's IP is added to matching Service endpoints.
A Pod stuck at Init:0/1 means an init container is failing or waiting:
kubectl logs <pod> -c <init-container-name>
restartPolicy
| Value | Behaviour | Used by |
|---|---|---|
Always | Restart on any exit, including 0 | Deployments, StatefulSets, DaemonSets (the default) |
OnFailure | Restart only on non-zero exit | Jobs, CronJobs |
Never | Never restart | One-shot Pods |
It applies to containers within a Pod, not to the Pod itself. A Pod is never restarted. It is replaced.
An important consequence: with restartPolicy: Always, a container that exits successfully is still restarted. A Pod meant to run a task to completion needs OnFailure or Never, which is why you use a Job rather than a Deployment for batch work.
CrashLoopBackOff is a symptom
The container starts, exits, and the kubelet restarts it with an increasing delay: 10s, 20s, 40s, doubling up to a five-minute cap. The backoff resets after the container runs successfully for 10 minutes.
It tells you the container keeps dying. It does not tell you why. The cause is in the logs of the run that failed:
kubectl logs <pod> --previous
kubectl describe pod <pod>
Typical causes, roughly in order:
- A missing environment variable or config file. The process exits immediately on startup
- A dependency unreachable at boot, where the application chooses to exit rather than retry
OOMKilledfrom a memory limit set too low- A liveness probe firing before the application has finished starting, killing it repeatedly
- A wrong command or entrypoint. Check
lastState.terminated.exitCode
Exit codes carry information: 137 is SIGKILL, usually OOM or a liveness kill; 143 is SIGTERM, a normal shutdown; 1 is a generic application error; 127 means the command was not found.
Termination
When a Pod is deleted, two things happen at the same time, and the race between them causes dropped requests:
- The Pod is removed from Service endpoints, which must propagate to kube-proxy on every node.
- The
preStophook runs if defined, then containers receiveSIGTERM.
After terminationGracePeriodSeconds, 30 by default, anything still running gets SIGKILL.
spec:
terminationGracePeriodSeconds: 45
containers:
- name: api
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
The preStop sleep delays SIGTERM long enough for endpoint removal to reach every node, so no new requests arrive at a container that is about to stop. Meanwhile the application should handle SIGTERM by refusing new connections and finishing in-flight work.
The grace period must exceed your longest normal request, or in-flight work is killed. A container that ignores SIGTERM entirely takes the full grace period on every single deploy, which is a common reason rollouts feel slow.
Eviction
The kubelet evicts Pods when a node runs low on memory or disk, in QoS order: BestEffort first, then Burstable, then Guaranteed.
kubectl get pods --field-selector status.phase=Failed
kubectl describe node <node> | grep -A5 Conditions
A Pod with no resource requests is BestEffort and first to go, which is a reason to set requests quite apart from any performance tuning.
Reading status quickly
kubectl get pods
kubectl describe pod <name> # events explain most of it
kubectl get pod <name> -o yaml | yq '.status' # the full picture
A compact view of restart reasons across a namespace:
kubectl get pods -o custom-columns=\
NAME:.metadata.name,\
PHASE:.status.phase,\
READY:.status.containerStatuses[0].ready,\
RESTARTS:.status.containerStatuses[0].restartCount,\
REASON:.status.containerStatuses[0].lastState.terminated.reason
Quick reference
| Task | Command |
|---|---|
| Phase | kubectl get pod <n> -o jsonpath='{.status.phase}' |
| Current container state | kubectl get pod <n> -o jsonpath='{.status.containerStatuses[0].state}' |
| Why it last died | kubectl get pod <n> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}' |
| Exit code | ...lastState.terminated.exitCode |
| Crashed logs | kubectl logs <n> --previous |
| Init container logs | kubectl logs <n> -c <init-container> |
| Events | kubectl describe pod <n> |
| Watch transitions | kubectl get pods -w |
Key takeaways
- Phase is coarse; container state carries the detail.
RunningwithREADY 0/1means readiness is failing. lastState.terminated.reasonnames the cause:OOMKilledin one command, without reading logs.- Init containers run to completion in order, and block the app containers if they fail.
restartPolicy: Alwaysrestarts a container even on a clean exit. Use a Job for work that finishes.CrashLoopBackOffis a symptom. The cause is inkubectl logs --previous.- Exit 137 is SIGKILL (often OOM), 143 is SIGTERM.
- Endpoint removal and
SIGTERMrace. ApreStopsleep plusSIGTERMhandling prevents dropped requests. - The grace period must exceed your longest request.
Check your understanding
A task that keeps rerunning
A Pod runs a migration script that completes successfully and exits 0, but Kubernetes keeps restarting it. The Pod was created directly with default settings. Why?
Reading an exit code
A container terminated with exit code 137 and its own logs end mid-request with no error. What most likely happened?
Frequently Asked Questions
What are the Kubernetes Pod phases?
Pending means the Pod is accepted but not yet running. Being scheduled, or pulling images. Running means it is bound to a node with at least one container running. Succeeded means all containers exited 0 and will not restart, and Failed means all terminated with at least one failure. Unknown means the node stopped reporting. Phases only move forward; a failed Pod is replaced rather than recovered. Note that Running says nothing about health. The READY column does that.
What does CrashLoopBackOff actually mean?
The container starts, exits, and the kubelet restarts it with a growing delay: 10 seconds, then 20, then 40, doubling to a five-minute cap, resetting after ten minutes of stable running. It is a symptom, not a diagnosis. Find the cause with kubectl logs <pod> --previous, which shows the run that failed rather than the one just starting, and kubectl describe pod for the events. The usual culprits are missing configuration, an unreachable startup dependency, an OOM kill, or a liveness probe firing before the application is ready.
What is the difference between restartPolicy values?
Always restarts the container on any exit, including a successful one, and is the default used by Deployments, StatefulSets and DaemonSets. OnFailure restarts only on a non-zero exit and suits Jobs. Never does not restart at all. The policy applies to containers within a Pod. A Pod itself is never restarted, only replaced. This is why a task that completes should run as a Job: under Always a successful exit is treated as something to correct.
What do exit codes 137 and 143 mean?
Codes above 128 encode the signal that terminated the process: 137 is 128 plus 9 for SIGKILL, and 143 is 128 plus 15 for SIGTERM. 143 usually indicates a normal shutdown during a deploy or scale-down. 137 means the process was killed outright, most often OOMKilled at its memory limit or terminated after failing a liveness probe. Check lastState.terminated.reason to tell those apart. SIGKILL cannot be caught, so the container never logs anything about it.
How do I make my application shut down gracefully?
Three things together. Handle SIGTERM in the application by refusing new connections and finishing in-flight requests rather than exiting immediately. Add a preStop hook that sleeps a few seconds, so removal from Service endpoints propagates to every node before the signal arrives. And set terminationGracePeriodSeconds above your longest normal request, because anything still running when it expires is SIGKILLed. Skipping the preStop delay is the usual reason a correctly-configured rolling update still drops a few connections.
Why is my Pod stuck in Init:0/1?
An init container is failing or still waiting. Init containers run to completion in order before any application container starts, so a failing one blocks the Pod indefinitely while being retried. Read its logs with kubectl logs <pod> -c <init-container-name>. The container name is required, since the main container has not started. Common causes are an init container waiting on a dependency that never becomes available, or a migration step failing against the database.