Lifecycle Hooks and Resource Management
SummaryCovers the container lifecycle state machine, postStart and...
Covers the container lifecycle state machine, postStart and...
Covers the container lifecycle state machine, postStart and preStop hooks with exec and httpGet handlers, the interaction between preStop and terminationGracePeriodSeconds, resource requests vs. limits in CPU millicores and memory units, the three QoS classes, and what happens when containers exceed their resource boundaries.
Lifecycle Hooks and Resource Management
Every container in a Kubernetes Pod moves through a well-defined lifecycle. Understanding that lifecycle — and the hooks Kubernetes provides to inject behavior at key transitions — gives you control over what happens when containers start, run, and stop. Resource requests and limits add a second dimension: they determine how much CPU and memory the scheduler allocates to your containers and what happens when those containers try to consume more than allowed.
Container Lifecycle States
A container in a Pod exists in one of three states at any point in time. You can observe the current state with kubectl describe pod <name> and look at the State field under each container.
Waiting
The container is not yet running. It is waiting for something to complete — an image pull, a dependent init container, or a scheduling decision. The Waiting state includes a reason field that tells you what is happening:
ContainerCreating— the container runtime is pulling the image and setting up the container.CrashLoopBackOff— the container previously failed and Kubernetes is waiting before restarting it. The backoff interval doubles with each failure (10s, 20s, 40s…) up to a 5-minute maximum.ImagePullBackOff— the container runtime cannot pull the specified image, either because the image name is wrong, the tag does not exist, or the registry requires authentication that has not been configured.
Running
The container’s main process is executing. This is the steady state for long-running application containers. The Running state includes a startedAt timestamp. Any postStart hook has either already completed or is running concurrently with the main process (more on this below).
Terminated
The container’s process has exited. The Terminated state includes the exitCode, reason (such as Completed or OOMKilled), and startedAt/finishedAt timestamps. A container reaches Terminated when:
- Its main process exits voluntarily (exit code 0 for success, non-zero for error).
- It is killed by the system — usually because it exceeded its memory limit (
OOMKilled, exit code 137). - The Pod is being deleted and the container received SIGTERM followed by SIGKILL after the grace period expired.
Lifecycle Hooks
Kubernetes provides two hooks that let you inject behavior at lifecycle transitions: postStart (immediately after the container starts) and preStop (immediately before the container is terminated).
postStart
The postStart hook executes immediately after the container’s main process starts. “Immediately” is key — Kubernetes does not guarantee that the hook runs before the main process. They run concurrently. If the postStart hook fails (returns a non-zero exit code or the HTTP request fails), the container is killed and restarted according to the Pod’s restart policy.
Two handler types are supported:
exec — runs a command inside the container:
lifecycle:
postStart:
exec:
command:
- sh
- -c
- echo "Container started at $(date)" >> /var/log/lifecycle.log
httpGet — sends an HTTP GET request to the container:
lifecycle:
postStart:
httpGet:
path: /register
port: 8080
Common use cases for postStart:
- Register the container instance with a service registry.
- Write a startup marker file that other processes check.
- Warm up caches by pre-loading data before the readiness probe starts passing.
A critical constraint: Kubernetes does not send traffic to the container (via readiness gates) until the postStart hook completes. If your hook takes 30 seconds, the container is effectively unavailable for 30 seconds — even though the main process is running.
preStop
The preStop hook executes when Kubernetes decides to terminate a container — because the Pod is being deleted, a Deployment rollout is replacing it, or the node is being drained. The hook runs before the SIGTERM signal is sent to the container’s main process.
lifecycle:
preStop:
exec:
command:
- sh
- -c
- |
echo "Draining connections..."
sleep 15
echo "Drain complete."
This preStop hook gives the application 15 seconds to finish processing in-flight requests before SIGTERM arrives. Without it, SIGTERM arrives immediately when the Pod is deleted, and any requests currently being processed may be interrupted.
A more realistic example for a web server:
lifecycle:
preStop:
exec:
command:
- sh
- -c
- nginx -s quit
The nginx -s quit command tells nginx to stop accepting new connections and finish processing existing ones — a graceful shutdown. The preStop hook ensures this graceful signal is sent before Kubernetes escalates to SIGTERM.
The Termination Sequence
When Kubernetes decides to terminate a Pod, the following sequence executes:
- The Pod is set to
Terminatingstate. It is removed from Service endpoints — no new traffic is routed to it. - The
preStophook runs for each container that has one defined. - After the
preStophook completes (or its execution times out), SIGTERM is sent to the container’s main process (PID 1). - Kubernetes waits up to
terminationGracePeriodSeconds(default: 30 seconds) for the process to exit. - If the process has not exited after the grace period, SIGKILL is sent — a forceful, non-catchable kill signal.
The critical detail: terminationGracePeriodSeconds is the total time budget for both the preStop hook and the SIGTERM handling. If your preStop hook sleeps for 25 seconds and the application needs 10 seconds to shut down after SIGTERM, you need at least 35 seconds total. Set it in the Pod spec:
spec:
terminationGracePeriodSeconds: 45
containers:
- name: app
image: myapp:1.0
lifecycle:
preStop:
exec:
command:
- sh
- -c
- sleep 25
If terminationGracePeriodSeconds is too short, the container receives SIGKILL before it finishes cleaning up. Connections are dropped. Data may be lost. On the exam, if a task mentions graceful shutdown or connection draining, check the terminationGracePeriodSeconds value.
Complete Lifecycle Hooks Example
Here is a Pod manifest that demonstrates both hooks alongside the main application:
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
labels:
app: demo
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: nginx:1.25
ports:
- containerPort: 80
lifecycle:
postStart:
exec:
command:
- sh
- -c
- echo "Started" > /usr/share/nginx/html/health.txt
preStop:
exec:
command:
- sh
- -c
- nginx -s quit && sleep 10
When this Pod starts, the postStart hook writes a health marker file. When the Pod is terminated, the preStop hook sends nginx -s quit (graceful shutdown) and then waits 10 seconds to allow in-flight requests to complete. The terminationGracePeriodSeconds: 60 gives ample time for the preStop hook (≈10s), the SIGTERM handling by nginx, and any additional cleanup.
Resource Requests and Limits
Every container in a Pod can declare how much CPU and memory it needs (requests) and how much it is allowed to consume (limits). These two values serve different purposes and have different enforcement mechanisms.
Requests: Scheduling Guarantee
A resource request tells the scheduler the minimum resources the container needs to run. The scheduler uses requests to find a node with enough allocatable capacity. If no node has sufficient resources, the Pod stays in Pending.
resources:
requests:
cpu: 250m
memory: 128Mi
This container needs at least 250 millicores (0.25 of a CPU core) and 128 mebibytes (128 × 1024² bytes) of memory. The scheduler checks every node’s unallocated capacity and picks one where these resources are available. Once scheduled, the container is guaranteed at least this much — even if other containers on the same node are competing for resources.
Limits: Hard Ceiling
A resource limit sets the maximum resources the container can consume. Enforcement differs between CPU and memory:
- CPU limits are soft. When a container exceeds its CPU limit, it is throttled — the kernel reduces the container’s available CPU cycles. The container slows down but continues running. No crash, no restart.
- Memory limits are hard. When a container exceeds its memory limit, it is killed — the kernel’s OOM (Out of Memory) killer terminates the process. The container exits with code 137 and reason
OOMKilled. The kubelet then restarts it according to the Pod’s restart policy.
resources:
requests:
cpu: 250m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
This container is guaranteed 250m CPU and 128Mi memory (requests), but can burst up to 500m CPU and 256Mi memory (limits). CPU above 500m is throttled. Memory above 256Mi triggers OOMKill.
Units
CPU is measured in millicores. One core = 1000m. Common values:
| Value | Meaning |
|---|---|
100m | 0.1 CPU core (10% of one core) |
250m | 0.25 CPU core (25% of one core) |
500m | 0.5 CPU core (half a core) |
1 | 1 full CPU core |
1500m | 1.5 CPU cores |
You can write 0.5 instead of 500m — they are equivalent. On the exam, use whichever is clearer.
Memory is measured in bytes with SI or binary suffixes:
| Suffix | Meaning | Example |
|---|---|---|
Ki | Kibibyte (1024 bytes) | 256Ki = 262,144 bytes |
Mi | Mebibyte (1024² bytes) | 128Mi = 134,217,728 bytes |
Gi | Gibibyte (1024³ bytes) | 1Gi = 1,073,741,824 bytes |
K | Kilobyte (1000 bytes) | 256K = 256,000 bytes |
M | Megabyte (1000² bytes) | 128M = 128,000,000 bytes |
G | Gigabyte (1000³ bytes) | 1G = 1,000,000,000 bytes |
Use binary suffixes (Mi, Gi) for memory — they are the convention in Kubernetes documentation and avoid ambiguity.
QoS Classes
Kubernetes assigns every Pod one of three Quality of Service (QoS) classes based on how resource requests and limits are configured. The QoS class determines which Pods get evicted first when a node runs out of resources.
Guaranteed
Every container in the Pod has both requests and limits set, and they are equal:
resources:
requests:
cpu: 500m
memory: 256Mi
limits:
cpu: 500m
memory: 256Mi
Guaranteed Pods are the last to be evicted. They get exactly the resources they requested — no more, no less. The kernel reserves these resources and does not allow the container to burst above the limit (CPU is capped, memory is OOM-killed at exactly the limit). Use Guaranteed for critical workloads that must not be disrupted.
Burstable
At least one container has requests set, but requests and limits are not equal — or limits is not set at all:
resources:
requests:
cpu: 250m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
Burstable Pods are evicted after BestEffort Pods but before Guaranteed ones. They can consume more resources than requested (up to their limits), but this burst capacity is not guaranteed — it depends on what is available on the node.
BestEffort
No container in the Pod has any requests or limits set:
containers:
- name: app
image: myapp:1.0
# No resources block at all
BestEffort Pods are the first to be evicted when the node is under memory pressure. They can consume whatever resources are available, but the scheduler has no information about their needs and cannot make intelligent placement decisions. Avoid BestEffort in production. On the exam, a task that asks you to “ensure the Pod has the highest eviction priority” is asking for BestEffort (no resources block).
What Happens When Limits Are Exceeded
CPU: Throttling
When a container tries to use more CPU than its limit, the Linux kernel’s CFS (Completely Fair Scheduler) bandwidth control restricts the container’s CPU time. The container is not killed — it continues running but executes fewer instructions per second. From the application’s perspective, everything slows down: request latency increases, throughput drops, and background tasks take longer to complete.
You can detect CPU throttling by checking the container’s metrics. The container_cpu_cfs_throttled_seconds_total metric (exposed via cAdvisor) shows the total time the container has been throttled. Persistent throttling indicates the CPU limit is set too low for the workload.
Memory: OOMKilled
When a container’s resident memory exceeds its limit, the Linux kernel’s OOM killer terminates the container’s primary process. The container exits with code 137 (128 + 9, where 9 is the SIGKILL signal). kubectl describe pod shows the container’s last state with reason OOMKilled.
kubectl describe pod memory-hog
Output includes:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
The kubelet restarts the container according to the Pod’s restart policy. If the application consistently exceeds its memory limit, it enters CrashLoopBackOff — repeating the cycle of start, exceed limit, OOMKill, restart with increasing backoff delays.
To fix OOMKilled containers: either increase the memory limit or fix the memory leak in the application. On the exam, if a task gives you a Pod with OOMKilled status, check resources.limits.memory — it is probably set too low for the workload.
Complete Resource Management Example
This manifest demonstrates requests, limits, and lifecycle hooks working together:
apiVersion: v1
kind: Pod
metadata:
name: production-app
labels:
app: api
tier: backend
spec:
terminationGracePeriodSeconds: 45
containers:
- name: api-server
image: myapi:2.0
ports:
- containerPort: 8080
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
lifecycle:
postStart:
exec:
command:
- sh
- -c
- echo "API server started" >> /var/log/app/lifecycle.log
preStop:
exec:
command:
- sh
- -c
- |
echo "Shutting down gracefully..."
kill -SIGTERM 1
sleep 20
volumeMounts:
- name: log-volume
mountPath: /var/log/app
volumes:
- name: log-volume
emptyDir: {}
This Pod is classified as Burstable because requests and limits differ. The scheduler guarantees 250m CPU and 256Mi memory. The container can burst to 500m CPU and 512Mi memory. If it exceeds 512Mi memory, it is OOMKilled. If it exceeds 500m CPU, it is throttled.
The preStop hook sends SIGTERM to PID 1 (the main process) and then waits 20 seconds for graceful shutdown. The terminationGracePeriodSeconds: 45 allows sufficient time for the hook’s 20-second sleep, the application’s shutdown logic, and a safety margin before SIGKILL is sent.
Exam Strategy
Resource requests and limits appear in two task formats:
-
Set resources on a container — the task specifies exact values. Add the
resourcesblock to the container spec with the correct fields and units. Watch the unit capitalization:Mi(mebibytes), notmiorMB. -
Debug an eviction or OOMKill — a Pod is restarting or being evicted. Check
kubectl describe podforOOMKilledin the container’s last state, and verify theresources.limits.memoryvalue. For evictions, checkkubectl describe nodefor memory pressure conditions.
For lifecycle hooks, generate the Pod YAML with kubectl run --dry-run=client -o yaml, then add the lifecycle block manually. The hook nests inside spec.containers[*].lifecycle:
containers:
- name: app
image: myapp:1.0
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo started"]
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
Practice writing this structure from memory. The nesting — containers → lifecycle → preStop → exec → command — has five levels of indentation. Getting it wrong burns exam time on YAML debugging.