Kubernetes Running Your Containers in Kubernetes Created: 17 Jul 2026 Updated: 17 Jul 2026

Kubernetes Pod Lifecycle and Phases

A Pod in Kubernetes is not a static thing — it goes through a well-defined lifecycle from creation to termination. Understanding this lifecycle is essential because it directly affects how your application starts up, handles requests, and shuts down gracefully.

If you have ever seen a Pod stuck in Pending, wondered why a rolling update drops requests, or needed to drain in-flight HTTP connections before a Pod dies, the answer lies in the Pod lifecycle.

In this article, you will learn every phase a Pod can be in, the lifecycle hooks Kubernetes provides (postStart and preStop), the terminationGracePeriodSeconds setting, and how all of these work together to enable zero-downtime deployments — especially for .NET applications.

Core Concepts

Step 1: Pod Phases

Every Pod has a status.phase field that tells you where it is in its lifecycle. There are exactly five phases:

PhaseMeaning
PendingThe Pod has been accepted by the cluster, but one or more containers are not yet running. This includes time spent waiting for scheduling (finding a Node), downloading container images, and waiting for init containers to complete.
RunningThe Pod has been bound to a Node and all containers have been created. At least one container is running, starting, or restarting.
SucceededAll containers in the Pod have terminated successfully (exit code 0) and will not be restarted. This is typical for Jobs and batch workloads.
FailedAll containers have terminated, and at least one container exited with a non-zero exit code or was killed by the system.
UnknownThe state of the Pod cannot be determined — usually because communication with the Node where the Pod is running has been lost.

Key point: A Pod in the Running phase is not necessarily ready to receive traffic. The Pod might still be booting up your .NET application. That is why Kubernetes uses separate readiness probes to determine when a Pod should be added to a Service's endpoint list.

Step 2: Container States

Within a running Pod, each container has its own state. You can see it with kubectl describe pod under the State field:

StateMeaning
WaitingThe container is not yet running. It might be pulling the image, waiting for a ConfigMap, or stuck in a CrashLoopBackOff.
RunningThe container is executing without issues.
TerminatedThe container has finished execution — either successfully or with an error. The exitCode field tells you which.

Step 3: Pod Conditions

Pod phases give a high-level view, but Pod conditions give you more detail. You can see them with kubectl describe pod or kubectl get pod -o yaml:

ConditionMeaning
PodScheduledThe Pod has been assigned to a Node.
InitializedAll init containers have completed successfully.
ContainersReadyAll containers in the Pod are ready.
ReadyThe Pod is ready to serve traffic and should be added to the endpoints of matching Services. This is the condition that Services actually check.

Step 4: The Startup Sequence

When Kubernetes creates a Pod, the following happens in order:

  1. Scheduling: The kube-scheduler picks a Node for the Pod. The Pod is in Pending phase.
  2. Image pull: The kubelet on the selected Node pulls the container image (if not already cached). Still Pending.
  3. Init containers: If the Pod has init containers, they run one by one in order. Each must succeed before the next starts. Still Pending.
  4. Main containers start: All main containers start simultaneously. The Pod moves to Running.
  5. postStart hook: If defined, the postStart handler runs immediately after each container starts. The container is not marked as Running until the hook completes.
  6. Startup probe: If defined, no other probes run until the startup probe succeeds. This protects slow-starting applications.
  7. Liveness and readiness probes: Once the startup probe passes (or if there is no startup probe), liveness and readiness probes begin.
  8. Ready: When the readiness probe passes, the Pod's Ready condition becomes True and the Pod is added to Service endpoints. Traffic starts flowing.

Step 5: Lifecycle Hooks — postStart and preStop

Kubernetes provides two lifecycle hooks that let you run code at specific points:

HookWhen It RunsCommon Use Cases
postStartImmediately after the container is created. Runs in parallel with the container's entrypoint — there is no guarantee it runs before the entrypoint.Warm up caches, register the instance with a service registry, write a log entry.
preStopImmediately before the container is terminated. Runs before SIGTERM is sent to the container process.Drain connections, deregister from load balancer, flush buffers, complete in-flight requests.

Both hooks support two handler types:

  1. exec — Run a command inside the container.
  2. httpGet — Send an HTTP GET request to a specified endpoint and port.

Important: If a postStart hook fails, the container is killed. If a preStop hook hangs, it is bounded by terminationGracePeriodSeconds.

Step 6: The Termination Sequence

When Kubernetes needs to terminate a Pod (due to a rolling update, scale-down, node drain, or manual deletion), the following happens in order:

  1. Pod marked for termination: The Pod's state is set to Terminating. The Pod is removed from Service endpoints — no new traffic is routed to it.
  2. preStop hook runs: If defined, the preStop handler executes. This gives your application time to finish in-flight requests.
  3. SIGTERM sent: After the preStop hook completes (or immediately if there is no hook), Kubernetes sends a SIGTERM signal to the main process (PID 1) in each container.
  4. Grace period countdown: The terminationGracePeriodSeconds timer starts (default: 30 seconds). Your application should catch SIGTERM and shut down cleanly within this window.
  5. SIGKILL: If the container is still running after the grace period expires, Kubernetes sends SIGKILL — a forceful, immediate kill that cannot be caught or ignored.

For .NET developers: The ASP.NET Core hosting model already handles SIGTERM by default. When the process receives SIGTERM, IHostApplicationLifetime.ApplicationStopping is triggered and Kestrel stops accepting new connections while completing in-flight requests. The default ShutdownTimeout in Kestrel is 30 seconds, which aligns with Kubernetes' default grace period.

Step 7: terminationGracePeriodSeconds

This field controls how long Kubernetes waits between sending SIGTERM and sending SIGKILL. The default is 30 seconds.

spec:
terminationGracePeriodSeconds: 45

How to choose the right value:

  1. Too short: Your application does not have time to drain connections → clients get connection resets → data loss in background tasks.
  2. Too long: Rolling updates and node drains take forever because Kubernetes waits for each Pod to terminate.
  3. Rule of thumb: Set it to your application's maximum expected request duration plus a small buffer. For a typical .NET web API with 10-second max requests, 30–45 seconds is usually enough.

Step 8: The preStop + Sleep Pattern

There is a subtle timing issue during Pod termination: Kubernetes removes the Pod from Service endpoints and sends the preStop hook concurrently, but endpoint removal is eventually consistent. Some kube-proxy or Ingress controllers might still route traffic to the terminating Pod for a few seconds.

A common workaround is to add a short sleep in the preStop hook. This keeps the container alive long enough for endpoint updates to propagate:

lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]

This 10-second sleep gives kube-proxy, Ingress controllers, and DNS caches time to stop sending traffic before the application begins its shutdown sequence.

Step 9: Restart Policies

The restartPolicy field on a Pod spec determines what happens when a container exits:

PolicyBehaviorUsed By
AlwaysAlways restart the container, regardless of exit code. This is the default.Deployments, ReplicaSets, DaemonSets
OnFailureRestart only if the container exits with a non-zero exit code.Jobs, CronJobs
NeverNever restart. Once the container exits, it stays terminated.Debugging, one-time tasks

When a container keeps crashing, Kubernetes applies exponential backoff: it waits 10s, 20s, 40s, ... up to 5 minutes before each restart attempt. This is the infamous CrashLoopBackOff state.

Hands-On: Kubernetes Commands

Check Pod Phase

kubectl get pod notification-api -o jsonpath='{.status.phase}'

View Pod Conditions

kubectl get pod notification-api -o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\n"}{end}'

View Container States

kubectl describe pod notification-api

Look for the State, Last State, and Restart Count fields under each container.

Watch Pod Status Changes in Real Time

kubectl get pod notification-api --watch

Check Events for a Pod

kubectl events --for pod/notification-api

Events show the sequence: Scheduled → Pulling → Pulled → Created → Started → (Killing if terminated).

See Why a Pod Failed

kubectl get pod notification-api -o jsonpath='{.status.containerStatuses[0].state.terminated.reason}'
kubectl get pod notification-api -o jsonpath='{.status.containerStatuses[0].state.terminated.exitCode}'

View Previous Container Logs (After Crash)

kubectl logs notification-api --previous

Force Delete a Stuck Pod

kubectl delete pod notification-api --grace-period=0 --force

Warning: Force deletion skips the graceful shutdown sequence. Use it only when a Pod is stuck in Terminating and will not go away.

Step-by-Step Example

Scenario 1: Observing Pod Lifecycle Phases

We will create a single Pod running a .NET 10 Notification API and observe it through all lifecycle phases.

1. Create the Pod with Lifecycle Hooks

apiVersion: v1
kind: Pod
metadata:
name: notification-api
labels:
app: notification-api
spec:
terminationGracePeriodSeconds: 45
containers:
- name: notification-api
image: myregistry.azurecr.io/notification-api:1.0
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_URLS
value: "http://+:8080"
- name: SMTP_HOST
value: "smtp.internal.svc.cluster.local"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Pod started at $(date) >> /var/log/lifecycle.log"]
preStop:
httpGet:
path: /shutdown
port: 8080
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 15

Apply the Pod:

kubectl apply -f notification-api-pod.yaml

2. Watch the Pod Transition Through Phases

In a separate terminal, watch the Pod in real time:

kubectl get pod notification-api --watch

You will see it move from PendingContainerCreatingRunning:

NAME READY STATUS RESTARTS AGE
notification-api 0/1 Pending 0 0s
notification-api 0/1 ContainerCreating 0 1s
notification-api 0/1 Running 0 3s
notification-api 1/1 Running 0 8s

Notice the 0/1 to 1/1 transition — that is when the readiness probe passed and the Pod became Ready.

3. Inspect Pod Conditions

kubectl get pod notification-api -o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}{.lastTransitionTime}{"\n"}{end}'

Expected output:

PodScheduled True 2026-04-12T10:00:01Z
Initialized True 2026-04-12T10:00:01Z
ContainersReady True 2026-04-12T10:00:08Z
Ready True 2026-04-12T10:00:08Z

4. Verify the postStart Hook

kubectl exec notification-api -- cat /var/log/lifecycle.log

You should see the timestamp logged by the postStart hook.

5. Delete the Pod and Observe Termination

kubectl delete pod notification-api

If you are watching with --watch, you will see:

notification-api 1/1 Terminating 0 5m
notification-api 0/1 Terminating 0 5m45s

During the Terminating state, Kubernetes: (1) removed the Pod from Service endpoints, (2) called the preStop hook (HTTP GET to /shutdown), (3) sent SIGTERM, and (4) waited up to 45 seconds before SIGKILL.

Scenario 2: Graceful Shutdown in a Deployment

In production, you use Deployments rather than bare Pods. Here we add the preStop sleep pattern to ensure zero-downtime rolling updates.

6. Deploy with preStop Sleep

apiVersion: apps/v1
kind: Deployment
metadata:
name: notification-api
labels:
app: notification-api
spec:
replicas: 2
selector:
matchLabels:
app: notification-api
template:
metadata:
labels:
app: notification-api
spec:
terminationGracePeriodSeconds: 45
containers:
- name: notification-api
image: myregistry.azurecr.io/notification-api:1.0
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_URLS
value: "http://+:8080"
- name: SMTP_HOST
value: "smtp.internal.svc.cluster.local"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
---
apiVersion: v1
kind: Service
metadata:
name: notification-api-service
spec:
type: ClusterIP
selector:
app: notification-api
ports:
- port: 80
targetPort: 8080

Apply the Deployment:

kubectl apply -f notification-api-deployment.yaml

7. Trigger a Rolling Update

Simulate updating the image to trigger the termination sequence on old Pods:

kubectl set image deployment/notification-api notification-api=myregistry.azurecr.io/notification-api:2.0

Watch the rollout:

kubectl rollout status deployment/notification-api

During the rolling update, each old Pod goes through the full termination sequence: endpoint removal → preStop sleep (10s) → SIGTERM → graceful shutdown. The 10-second sleep ensures that kube-proxy has time to update its iptables rules before the application starts shutting down. Meanwhile, the new Pod must pass its readiness probe before the next old Pod is terminated.

8. Verify the Timeline

kubectl events --for deployment/notification-api

You will see events showing the Deployment scaling up the new ReplicaSet and scaling down the old one, confirming the graceful transition.

Summary

  1. Pods go through five phases: Pending, Running, Succeeded, Failed, and Unknown.
  2. Within a Pod, each container has its own state: Waiting, Running, or Terminated.
  3. Pod conditions (PodScheduled, Initialized, ContainersReady, Ready) provide granular status — the Ready condition determines when a Pod receives traffic.
  4. postStart hook runs immediately after container creation (in parallel with the entrypoint). preStop hook runs before SIGTERM during termination.
  5. terminationGracePeriodSeconds is the window between SIGTERM and SIGKILL. Default is 30 seconds. Set it based on your application's shutdown time.
  6. The preStop + sleep pattern (e.g., sleep 10) protects against traffic being routed to a terminating Pod due to endpoint propagation delay.
  7. restartPolicy controls container restart behavior: Always (default for Deployments), OnFailure (Jobs), or Never.
  8. ASP.NET Core handles SIGTERM natively via IHostApplicationLifetime.ApplicationStopping — Kestrel drains connections automatically within its ShutdownTimeout.


Share this lesson: