The Lifecycle of a Pod: Phase, Condition and restartPolicy
In Article 17 we saw a pod come to life through the Event chain Scheduled → Pulled → Created → Started. That was the view from outside, over time. This article opens the deep-dive Pods section by looking inside a pod: what states it has, who decides those states, and why a pod that crashes constantly still reports Running. This is the vocabulary you read every day in kubectl get pods, so reading it closely once will save a lot of guesswork later.
Grasp this first: a pod's status is not a single value, but three layers stacked on top of each other, each at a different level of detail.
┌─ Pod phase ──────── coarse summary: Pending / Running / Succeeded / Failed
│ ▲ DERIVED from ↓
├─ Container state ── per-container detail: Waiting / Running / Terminated
│ (+ reason, exitCode)
└─ Pod conditions ─── True/False checklist: PodScheduled, Initialized,
ContainersReady, Ready
A common misunderstanding is to treat phase as the source of truth. In fact phase is the coarsest summary layer, derived from container state plus restartPolicy. To know what's really happening, you read the two lower layers.
Layer 1 — Pod phase
phase is one of five values, answering the coarse question "what stage of the lifecycle is the pod in":
- Pending — the pod has been accepted but no container is running yet (waiting to be scheduled, pulling an image, or setting up networking).
- Running — the pod is bound to a node and at least one container is running or starting/restarting.
- Succeeded — all containers have finished and exited with code 0, and will not restart.
- Failed — all containers have stopped and at least one exited with a non-zero code (or was killed by the system).
- Unknown — the pod's state could not be obtained, usually because contact with the node was lost.
Succeeded and Failed are two terminal states, meaning the pod has run out its life. But whether a pod ever reaches them depends on restartPolicy, which we'll come back to.
Layer 2 — Container state
Each container in a pod has its own state, much more detailed than phase, with three possibilities:
- Waiting — not running yet: pulling an image, or waiting between restarts. Has a
reason(e.g.CrashLoopBackOff,ImagePullBackOff). - Running — running, with a
startedAt. - Terminated — stopped, with a
reason(Completed,Error,OOMKilled...), anexitCode, and start/finish times.
When a container restarts, the previous state is kept in lastState, very useful for knowing why the most recent death happened, even when the container is currently running again.
Layer 3 — Pod conditions
conditions is a list of True/False items, like a progress checklist. Take a normally running pod and look:
kubectl get pod life-running -o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\n"}{end}'
PodReadyToStartContainers=True
Initialized=True
Ready=True
ContainersReady=True
PodScheduled=True
Read them in the order a pod passes through them as it starts up:
- PodScheduled — the scheduler has bound the pod to a node (Article 17).
- PodReadyToStartContainers — the pod's sandbox and network namespace are ready (CNI has provisioned networking).
- Initialized — all init containers have completed (this pod has no init containers so it's
Trueimmediately; init containers are the topic of Article 19). - ContainersReady — all containers in the pod are Ready.
- Ready — the pod is ready to receive traffic; this is the condition the endpoints controller looks at to decide whether to add the pod to a Service's EndpointSlice.
Distinguishing Ready from Running matters a lot: a container can be Running but not Ready (e.g. an app that starts slowly and hasn't passed its readiness probe). In that case the pod is running but the Service won't send traffic to it. The probe mechanism that drives Ready is the topic of Article 20; here just remember that Ready is the gate into a Service.
restartPolicy — what decides the terminal phase
restartPolicy (set on spec, applied to every container in the pod) decides whether the kubelet restarts a container when it exits. Three values:
| restartPolicy | container exits code 0 | container exits code ≠ 0 |
|---|---|---|
| Always (default) | restart | restart |
| OnFailure | no restart → pod Succeeded |
restart |
| Never | no restart → pod Succeeded |
no restart → pod Failed |
The key point: a pod only reaches a terminal state (Succeeded/Failed) when a container is not restarted. With Always, the container is always rebuilt, so the pod never naturally reaches Succeeded or Failed — it stays Running. That's why a Deployment (which always uses restartPolicy: Always) suits long-running services, while a Job (which uses OnFailure/Never) suits run-once tasks, the topic of Article 27.
Four real pods, four outcomes
The theory above packs into four pods. Create them all at once: a long-running service, a task that exits 0, a task that exits 1, and a container that crashes constantly.
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP
life-running 1/1 Running 0 85s 10.200.0.8
life-success 0/1 Completed 0 85s 10.200.1.9
life-failed 0/1 Error 0 85s 10.200.0.9
life-crash 0/1 CrashLoopBackOff 4 (67s ago) 2m44s 10.200.1.10
The STATUS column here is not phase; it's a friendly summary that kubectl assembles from container state. Digging into each pod reveals the three layers clearly.
life-running (restartPolicy: Always, runs forever) — phase=Running, container running, all conditions True. This is the normal healthy state.
life-success (restartPolicy: Never, exit 0):
kubectl get pod life-success -o jsonpath='phase={.status.phase}{"\n"}{.status.containerStatuses[0].state.terminated.reason} exit={.status.containerStatuses[0].state.terminated.exitCode}{"\n"}'
phase=Succeeded
Completed exit=0
The container exited cleanly, Never doesn't restart, so the pod reaches the terminal Succeeded state. kubectl shows Completed.
life-failed (restartPolicy: Never, exit 1):
kubectl get pod life-failed -o jsonpath='phase={.status.phase}{"\n"}{.status.containerStatuses[0].state.terminated.reason} exit={.status.containerStatuses[0].state.terminated.exitCode}{"\n"}'
kubectl get pod life-failed -o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\n"}{end}'
phase=Failed
Error exit=1
PodReadyToStartContainers=False
Initialized=True
Ready=False
ContainersReady=False
PodScheduled=True
Exit code 1, Never doesn't restart, the pod is Failed. Note the conditions: PodScheduled and Initialized are still True (the pod was bound and started), but Ready/ContainersReady flip to False — the pod can no longer serve.
life-crash (restartPolicy: Always, runs 2 seconds then exit 1):
kubectl get pod life-crash -o jsonpath='phase={.status.phase}{"\n"}restartCount={.status.containerStatuses[0].restartCount}{"\n"}lastTerminated={.status.containerStatuses[0].lastState.terminated.reason} exit={.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'
phase=Running
restartCount=4
lastTerminated=Error exit=1
The container dies again and again, but phase is still Running, because Always keeps restarting so the pod never touches a terminal state. The real information lives in two other places: restartCount keeps climbing, and lastState.terminated tells you about the most recent death (Error, exit 1). That's why you should not trust phase alone.
CrashLoopBackOff is not a phase
When a container keeps dying and restarting, the kubelet doesn't restart it immediately but waits an interval that grows by exponential backoff: 10 seconds, 20, 40... doubling each time, capped at 5 minutes. While waiting, the container is in the Waiting state with reason: CrashLoopBackOff. Catch exactly that moment:
kubectl get pod life-crash -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}'
CrashLoopBackOff
CrashLoopBackOff is therefore not a phase, nor an error in itself. It's a container state = Waiting with the reason "waiting between restarts," telling you the container has died many times and the kubelet is spacing out the restarts. The real cause must be found in lastState.terminated (the exit code) and kubectl logs --previous (the log of the run before it died). Confusing CrashLoopBackOff with a phase is one of the most common misunderstandings when you first start debugging pods.
🧹 Cleanup
Delete the four demo pods:
kubectl delete pod life-running life-success life-failed life-crash
They're all objects in the cluster — deleting them is clean and touches nothing on the node. Manifests at github.com/nghiadaulau/kubernetes-from-scratch, directory 18-pod-lifecycle.
Wrap-up
A pod's status is three layers: phase as a coarse summary, container state as detail, conditions as a checklist; and phase is derived from container state plus restartPolicy, not set directly. Grasp these three and reading kubectl get pods is no longer reading a label but reading a story: a crash-looping pod still reports Running because Always won't let it terminate; CrashLoopBackOff is a waiting state, not an error; Ready differs from Running and is the actual gate into a Service.
The two pieces we deliberately set aside are init containers (which affect the Initialized condition) and probes (which drive the Ready condition), the content of the next two articles. Article 19 covers init containers and sidecar containers: how to run preparation work before the main container starts, and how to run an auxiliary container alongside it for the lifetime of the pod.