On this page
A Pod is not static. From the moment you create it until it is gone, it moves through a well-defined lifecycle: it waits for a node, starts its containers, serves traffic and finally shuts down.
Think of a restaurant shift. Staff arrive and set up before the doors open, serve guests during service, and at closing time stop seating new guests but finish the meals already on the table. A well-behaved Pod does the same: it only takes traffic once it is ready, and it finishes in-flight work before it stops.
If you have seen a Pod stuck in Pending, wondered why a rolling update drops requests, or needed to drain HTTP connections before a Pod dies, the answer is in the Pod lifecycle. This lesson covers the phases, the postStart and preStop hooks and terminationGracePeriodSeconds, and how they combine for zero-downtime deployments of .NET applications.
What you will learn
- The five Pod phases, the three container states and the Pod conditions, and how they differ
- What happens, in order, when a Pod starts and when it is terminated
- How to use the
postStartandpreStoplifecycle hooks and chooseterminationGracePeriodSeconds - How restart policies and
CrashLoopBackOffwork - How to make an ASP.NET Core app shut down gracefully during a rolling update
Pod phases
Every Pod has a status.phase field that says, at a high level, where it is in its lifecycle. There are exactly five phases:
| Phase | Meaning |
|---|---|
Pending | The cluster accepted the Pod, but one or more containers are not running yet. This covers waiting for a node, downloading images and running init containers. |
Running | The Pod is bound to a node and all containers have been created. At least one container is running, starting or restarting. |
Succeeded | All containers exited successfully (exit code 0) and will not be restarted. Typical for Jobs and batch work. |
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 cannot be read, usually because the control plane lost contact with the node. |
Running phase may still be booting your .NET application. Kubernetes uses a separate readiness probe to decide when the Pod is added to a Service's endpoints and starts receiving traffic.You may also see words like ContainerCreating, CrashLoopBackOff or Terminating in the STATUS column of kubectl get pods. Those are not phases. kubectl shows the most useful detail it can find, often a container state reason.
Container states
Inside a Pod, each container has its own state. kubectl describe pod shows it under the State field of each container:
| State | Meaning |
|---|---|
Waiting | Not running yet. The Reason tells you why, for example ContainerCreating, ImagePullBackOff, CreateContainerConfigError (a missing ConfigMap or Secret) or CrashLoopBackOff. |
Running | The container is executing normally. |
Terminated | The container finished, successfully or not. The exitCode and reason fields tell you which. |
Pod conditions
Phases are a summary. Pod conditions give more detail as a list of true/false checks. You see them in kubectl describe pod or kubectl get pod -o yaml:
| Condition | Meaning |
|---|---|
PodScheduled | The Pod has been assigned to a node. |
PodReadyToStartContainers | The Pod sandbox and its network are set up (Kubernetes 1.29 and later). |
Initialized | All init containers have completed successfully. |
ContainersReady | Every container in the Pod is ready. |
Ready | The Pod can serve traffic and is added to the endpoints of matching Services. This is the condition Services actually check. |
The startup sequence
When Kubernetes creates a Pod, these steps happen in order:
- Scheduling. The kube-scheduler picks a node. The Pod is
Pending. - Image pull. The kubelet on that node pulls the container images, unless they are already cached. Still
Pending. - Init containers. If the Pod has init containers, they run one at a time, in order. Each must succeed before the next starts. Still
Pending. - Main containers start. The kubelet starts the main containers in the order they are listed, without waiting for one to become ready before starting the next. The Pod moves to
Running. - postStart hook. If defined, it runs right after its container starts. The container is not reported as
Runninguntil the hook finishes. - Startup probe. If defined, no other probe runs until it succeeds. This protects slow-starting apps.
- Liveness and readiness probes. They begin once the startup probe passes, or straight away if there is none.
- Ready. When the readiness probe passes, the
Readycondition becomesTrueand the Pod joins the Service endpoints. Traffic starts to flow.
Lifecycle hooks: postStart and preStop
Kubernetes gives you two hooks to run code at specific moments in a container's life:
| Hook | When it runs | Common uses |
|---|---|---|
postStart | Right after the container is created. It runs alongside the container's entrypoint, with no guarantee that it runs before the entrypoint. | Warm up caches, register the instance in a service registry, write a log entry. |
preStop | Right before the container is stopped. It runs before SIGTERM is sent to the container's process. | Drain connections, deregister from a load balancer, flush buffers, finish in-flight requests. |
A hook uses one of these handler types:
exec: runs a command inside the container. The image needs that command, and a shell if you call/bin/sh.httpGet: sends an HTTP GET request to a path and port on the container.sleep: pauses for a number of seconds, with no shell needed (Kubernetes 1.30 and later).
If a postStart hook fails, the container is killed and then restarted according to the restart policy. If a preStop hook fails, Kubernetes records a FailedPreStopHook event and carries on with the shutdown. A preStop hook that hangs is cut off by terminationGracePeriodSeconds.
The termination sequence
Kubernetes terminates a Pod for many reasons: a rolling update, a scale-down, a node drain or a manual kubectl delete. The sequence is always the same:
- Marked for termination. The Pod shows as
Terminatingand the grace period countdown (terminationGracePeriodSeconds, 30 seconds by default) starts. - Removed from endpoints, preStop runs. At the same time, the Pod is removed from Service endpoints so no new traffic is sent to it, and the
preStophook starts, if there is one. - SIGTERM. When the
preStophook finishes, or straight away if there is none, the kubelet sendsSIGTERMto the main process (PID 1) of each container. Your app should catch it and shut down cleanly. - SIGKILL. If a container is still running when the grace period runs out, it gets
SIGKILL: an immediate kill that cannot be caught or ignored.
preStop sleep, your app has about 35 seconds left after SIGTERM.What .NET does with SIGTERM
The ASP.NET Core host handles SIGTERM out of the box. When it arrives, IHostApplicationLifetime.ApplicationStopping fires, and Kestrel stops accepting new connections while it finishes the requests already in progress.
The host waits up to HostOptions.ShutdownTimeout for this, 30 seconds by default since .NET 6. That matches the Kubernetes default grace period, so if you add a preStop delay, raise terminationGracePeriodSeconds by at least the same amount.
ENTRYPOINT ["dotnet", "MyApi.dll"] in your Dockerfile. The shell form ENTRYPOINT dotnet MyApi.dll starts /bin/sh as PID 1, which does not forward the signal, so the app is killed with SIGKILL when the grace period ends.Choosing terminationGracePeriodSeconds
This Pod-level field sets the total time Kubernetes gives a Pod to stop, from the moment it is marked for termination until SIGKILL. The default is 30 seconds.
How to pick a value:
- Too short: the app cannot drain its connections. Clients see connection resets and background work is lost.
- Too long: rolling updates and node drains crawl, because Kubernetes waits for every Pod to stop.
- Rule of thumb: your longest expected request, plus any
preStopdelay, plus a small buffer. For a typical .NET web API with requests under 10 seconds, 30 to 45 seconds is usually enough.
The preStop sleep pattern
There is a subtle race during termination. Endpoint removal and the preStop hook start at the same time, but endpoint removal is eventually consistent. kube-proxy on every node and your Ingress controller need a few seconds to notice, and may keep sending requests to the terminating Pod in the meantime.
The common fix is a short sleep in the preStop hook. It keeps the app running normally while the endpoint change spreads, and only then lets SIGTERM start the shutdown:
On Kubernetes 1.30 and later you can use the built-in sleep action instead. It needs no shell, so it also works with chiseled .NET images that ship without /bin/sh:
Either way, the 10 seconds give kube-proxy, Ingress controllers and DNS caches time to stop routing traffic to the Pod before the application begins to shut down.
Restart policies
The Pod's restartPolicy field decides what happens when a container exits. It applies to every container in the Pod.
| Policy | Behavior | Used by |
|---|---|---|
Always | Always restart the container, whatever its exit code. This is the default. | Deployments, ReplicaSets, StatefulSets, DaemonSets (the only value they allow) |
OnFailure | Restart only if the container exits with a non-zero code. | Jobs and CronJobs |
Never | Never restart. A container that exits stays terminated. | Jobs, one-off tasks and debugging Pods |
Because of this, only Pods with OnFailure or Never ever reach the Succeeded or Failed phase. With Always, a stopped container is simply started again.
When a container keeps crashing, the kubelet waits longer before each restart: 10 seconds, then 20, 40 and so on, up to 5 minutes. The timer resets once the container runs fine for 10 minutes. While it waits, the Pod shows the infamous CrashLoopBackOff status.
Commands for inspecting the lifecycle
The examples use a Pod named notification-api, which you will create in the hands-on section.
Check the Pod phase
View the Pod conditions
View container states
Look for the State, Last State and Restart Count fields under each container.
Watch status changes in real time
Check the events for a Pod
Events show the sequence Scheduled, Pulling, Pulled, Created, Started, and Killing when the Pod is terminated.
See why a container stopped
If the container has already been restarted, the details of the crash are under lastState instead of state.
Read the logs of the previous container
Force delete a stuck Pod
Terminating that will not go away, and never as a routine way to delete Pods.Hands-on: watch a Pod's lifecycle
You will create a Pod running a .NET 10 Notification API and follow it through its lifecycle. The image is an example; use your own ASP.NET Core image that exposes /healthz/ready, /healthz/live and a /shutdown endpoint.
Step 1 — Create the Pod with lifecycle hooks
Save this as notification-api-pod.yaml:
The postStart hook writes to /tmp because the official .NET 8+ images run as a non-root user that cannot write to /var/log.
Apply it:
Step 2 — Watch the Pod move through its phases
In a separate terminal, watch the Pod:
Expected output:
Notice the change from 0/1 to 1/1. That is the moment the readiness probe passed and the Pod became Ready.
Step 3 — Inspect the Pod conditions
Expected output:
The order of the lines can differ between clusters, and older clusters do not show PodReadyToStartContainers. Compare the timestamps: the Pod was scheduled and initialized first, and became ready a few seconds later.
Step 4 — Verify the postStart hook
You should see the line with the timestamp written by the postStart hook.
Step 5 — Delete the Pod and watch it terminate
In the terminal running --watch you will see something like:
While the Pod was Terminating, Kubernetes removed it from Service endpoints and called the preStop hook (an HTTP GET to /shutdown) at the same time, then sent SIGTERM, and would have sent SIGKILL if the Pod was still running 45 seconds after the delete.
Hands-on: graceful shutdown in a Deployment
In production you run Pods through a Deployment rather than creating them by hand. Here you add the preStop sleep pattern so rolling updates do not drop requests.
Step 1 — Deploy with a preStop sleep
Save this as notification-api-deployment.yaml. It holds a Deployment with two replicas and a Service in front of them:
If your image has no shell, replace the exec block with sleep: { seconds: 10 } as shown earlier. Apply the file:
Step 2 — Trigger a rolling update
Change the image to start a rolling update. Every old Pod now goes through the termination sequence:
Follow the rollout until it finishes:
Each old Pod is removed from the endpoints, sleeps for 10 seconds in preStop, receives SIGTERM and shuts down gracefully. The sleep gives kube-proxy time to update its rules before the app stops. Meanwhile, each new Pod must pass its readiness probe before the next old Pod is terminated.
Step 3 — Verify the timeline
You will see ScalingReplicaSet events: the Deployment scales the new ReplicaSet up and the old one down, one step at a time.
preStop sleep plus a large enough grace period control how old Pods leave. Together they let you update a .NET API without dropping a single request.Clean up when you are done:
Summary
A Pod moves from Pending to Running and, for finite workloads, to Succeeded or Failed. Hooks, probes and the grace period let you control exactly how it starts taking traffic and how it stops.
- The five phases are
Pending,Running,Succeeded,FailedandUnknown. Each container also has its own state:Waiting,RunningorTerminated. - Pod conditions give the detail. The
Readycondition decides when a Pod receives Service traffic. postStartruns right after a container starts, alongside the entrypoint.preStopruns before SIGTERM during termination.terminationGracePeriodSeconds(default 30) is the total time from termination to SIGKILL, and it includes thepreStophook.- The
preStopsleep pattern covers the delay while endpoint removal spreads through the cluster. restartPolicyisAlways(the default, required by Deployments),OnFailureorNever. Repeated crashes lead toCrashLoopBackOff.- ASP.NET Core handles SIGTERM natively through
IHostApplicationLifetime.ApplicationStopping, and Kestrel drains connections withinHostOptions.ShutdownTimeout.
With these pieces in place, your Pods start cleanly, receive traffic only when ready and leave without dropping requests.