DevOpsIndex

CrashLoopBackOff: Debugging Runbook

CrashLoopBackOff means the container starts, crashes (exits non-zero), Kubernetes restarts it, it crashes again — and Kubernetes applies exponential backoff between restarts (10s → 20s → 40s → 80s → 160s → 5min cap). It will keep retrying indefinitely.

It is not a single cause — it's a symptom. The exit code tells you where to look first.

Step 1: Get the exit code and last logs

# See restart count and status
kubectl get pod <pod> -n <ns>

# Last crash reason + exit code
kubectl describe pod <pod> -n <ns>
# Look for: Last State, Exit Code, Reason (OOMKilled / Error / Completed)

# Logs from the crashed container (not the current one)
kubectl logs <pod> -n <ns> --previous

# If multi-container pod
kubectl logs <pod> -n <ns> -c <container-name> --previous

Step 2: Diagnose by exit code

Exit Code Meaning Where to look
1 App error / panic / unhandled exception --previous logs — missing env var, failed DB connect, bad config
2 Misuse of shell / script error Entrypoint/command misconfigured
137 SIGKILL — OOM killed by kernel Memory limit too low, memory leak
139 Segfault Corrupt binary, wrong arch (arm vs amd64)
143 SIGTERM not handled — graceful shutdown timeout App ignores SIGTERM, preStop hook too short
125 Docker/container runtime error Bad image, missing binary in container
126 Permission denied on entrypoint File not executable, wrong user
127 Entrypoint binary not found Typo in command, wrong base image

Step 3: Work through the cause tree

CrashLoopBackOff
├── Exit 137 (OOMKilled)
│   ├── memory.limit too low → increase resources.limits.memory
│   ├── memory leak → profile with pprof, check goroutine count
│   └── JVM heap not set → add -Xmx flag
│
├── Exit 1 (App panic/error)
│   ├── Missing env var → kubectl describe pod, check envFrom / env
│   ├── Can't connect to DB/Redis on startup → wrong SERVICE_NAME, wrong port
│   ├── Failed DB migration (runs in same container) → separate init container
│   ├── Config file not found → ConfigMap not mounted, wrong mountPath
│   └── Secret not found → Secret doesn't exist in namespace
│
├── Exit 127 (binary not found)
│   ├── Wrong command/args in Deployment spec
│   └── Multi-stage build forgot to copy the binary
│
├── Exit 1 but logs are empty
│   ├── App crashes before logger initializes → add stderr logging early
│   ├── Init container failing → kubectl logs <pod> -c <init-container>
│   └── readinessProbe killing pod before app fully starts
│
└── Liveness probe killing healthy pod
    ├── initialDelaySeconds too short → app not ready when probe fires
    ├── Probe endpoint wrong → 404 returns, pod killed
    └── timeoutSeconds too low → slow startup looks like failure

Common contributors and fixes

1. Missing / wrong environment variable

kubectl describe pod <pod> -n <ns> | grep -A5 "Environment"
# Or check what the app is actually seeing:
kubectl exec <pod> -n <ns> -- env | grep DB_

# Fix: check ConfigMap and Secret refs in Deployment
# Common mistake: referencing a Secret that doesn't exist in this namespace
env:
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: db-secret      # does this Secret exist in the same namespace?
      key: password
kubectl get secret db-secret -n <ns>   # 404 here = pod will CrashLoop

2. Can't connect to dependency on startup

App tries to connect to DB/Redis/external API during init() or startup, fails, panics.

# Test connectivity from inside a debug pod in same namespace
kubectl run debug --rm -it --image=busybox -n <ns> -- sh
wget -qO- http://my-service:5432   # or nc -zv my-service 5432

Fix: Add startup retry logic — don't fail fast on first connection attempt. Or use an init container to wait for the dependency:

initContainers:
- name: wait-for-db
  image: busybox
  command: ['sh', '-c', 'until nc -z postgres-svc 5432; do echo waiting; sleep 2; done']

3. OOMKilled — memory limit too low

kubectl describe pod <pod> | grep -A3 "Last State"
# Last State: Terminated  Reason: OOMKilled

# Check current memory usage before the crash
kubectl top pod <pod> -n <ns>

# Check limits
kubectl get pod <pod> -o jsonpath='{.spec.containers[0].resources}'

Fix:

resources:
  requests:
    memory: "256Mi"
  limits:
    memory: "512Mi"   # increase this, or remove limit to use node memory

For Go apps: GOGC env var controls GC aggressiveness. GOMEMLIMIT (Go 1.19+) sets a soft memory ceiling before GC kicks in aggressively:

env:
- name: GOMEMLIMIT
  value: "450MiB"   # slightly below the k8s limit

4. Liveness probe killing the pod

Pod shows CrashLoopBackOff but logs look fine — the liveness probe is the killer.

kubectl describe pod <pod> | grep -A10 "Liveness"
# Events: Liveness probe failed: ... Killing container with id...
# Fix: give app time to start before first probe
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30    # wait 30s before first check
  periodSeconds: 10
  failureThreshold: 3        # need 3 consecutive failures before kill
  timeoutSeconds: 5          # give it 5s to respond

5. Init container failing silently

# Init containers show separately
kubectl get pod <pod> -n <ns>
# Init:0/1 means init container hasn't completed

kubectl logs <pod> -n <ns> -c <init-container-name>
kubectl logs <pod> -n <ns> -c <init-container-name> --previous

6. Wrong image / wrong arch

kubectl describe pod <pod> | grep -A5 "Events"
# exec format error → image built for wrong arch (amd64 image on arm node or vice versa)

# Fix: build multi-arch image
docker buildx build --platform linux/amd64,linux/arm64 -t my-org/app:v1 --push .

Quick checklist

□ kubectl logs <pod> --previous          → read the actual crash message
□ kubectl describe pod <pod>             → exit code, events, probe config
□ kubectl get events -n <ns> --sort-by=.lastTimestamp  → cluster-level events
□ exit code 137?                         → OOMKilled, check memory limits
□ exit code 1, empty logs?               → init container? probe killing it?
□ env vars correct?                      → describe pod, check secret/configmap exists
□ can pod reach its dependencies?        → debug pod with nc/wget
□ liveness probe initialDelaySeconds?    → might be firing too early
□ init containers healthy?               → kubectl logs -c <init-container>
□ image right arch?                      → exec format error in events

Prevention: Set resources.limits.memory on all containers — OOMKills become predictable, not random. Use GOMEMLIMIT for Go apps. Add startup probes with failureThreshold: 30 so slow-starting apps don't get killed by liveness. In CI: run kubectl apply --dry-run=server to catch missing secrets/configmaps before deploy. Use init containers for dependency checks instead of fast-failing in the main container.


Pod Stuck in Pending

Pod is created but never scheduled — it's sitting in the scheduler queue with no node assigned.

kubectl describe pod <pod> -n <ns>
# Look at Events section at the bottom — FailedScheduling with reason

Cause tree

Event message Root cause Fix
0/3 nodes are available: 3 Insufficient cpu No node has enough CPU Scale up node group, lower requests, or check if requests are over-specified
0/3 nodes are available: 3 Insufficient memory No node has enough memory Same as above
0/3 nodes are available: 3 node(s) had taint... that the pod didn't tolerate Missing toleration Add toleration for the taint
0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector nodeAffinity/nodeSelector mismatch Check labels on nodes vs pod spec
0/3 nodes are available: 3 node(s) had untolerated taint + 0 didn't match affinity Both issues at once Fix both
unbound immediate PersistentVolumeClaims PVC not bound to a PV See PVC stuck section below
didn't match pod anti-affinity rules Hard anti-affinity unsatisfiable Not enough nodes, or switch to soft
# Check actual node capacity vs allocatable
kubectl describe nodes | grep -A5 "Allocated resources"

# Check what's consuming resources
kubectl top nodes
kubectl top pods -A --sort-by=memory

# Check if cluster autoscaler is blocked
kubectl logs -n kube-system -l app=cluster-autoscaler | tail -50

Prevention: Set accurate resources.requests — oversized requests cause artificial pending. Use Cluster Autoscaler with proper minSize/maxSize and ensure node groups cover all required taints/labels. Add podAntiAffinity with preferredDuringScheduling (not required) to avoid unsatisfiable constraints. Test scheduling with kubectl apply --dry-run=server.


All Replicas Scheduled on One Node — Node Dies — Full Outage

Symptom: A Deployment with replicas: 3 (or any N > 1) has all replicas running on the same node. That node dies (hardware fault, kernel panic, host-level EC2 issue). All N pods disappear simultaneously — a full outage, not a partial degradation.

kubectl get pods -l app=<name> -o wide
# NAME            NODE
# my-app-abc123   node-a   ← all 3 rows show node-a — this is the exposure, even before node-a dies
# my-app-def456   node-a
# my-app-ghi789   node-a

Why this happens even though it "shouldn't": the default Kubernetes scheduler has no anti-affinity behavior by default. Its Score plugins (LeastAllocated/ImageLocality — see scheduler-internals.md) optimize for bin-packing and resource fit, not spread. Nothing in the default algorithm prevents multiple replicas of the same Deployment from landing on the same node — if that node scores highest each time (common right after a batch of pods is created together, or on a lightly-loaded cluster), the scheduler will legitimately place all of them there. This is not a bug; it's the absence of a rule you have to add explicitly.

flowchart TD
    A["Deployment: 3 replicas,<br/>no anti-affinity/spread configured"] --> B["Scheduler places pod 1<br/>--> Node A (best score)"]
    B --> C["Scheduler places pod 2<br/>--> Node A again<br/>(still best score, no rule against it)"]
    C --> D["Scheduler places pod 3<br/>--> Node A again"]
    D --> E["Node A dies<br/>(hardware fault / kernel panic / host issue)"]
    E --> F["All 3/3 pods gone<br/>simultaneously = FULL OUTAGE"]
    F --> G["vs. spread across 3 nodes:<br/>losing Node A only drops to 2/3 —<br/>degraded, not down"]

Cause tree

Root cause Fix
No topologySpreadConstraints or podAntiAffinity on the Deployment Add hard spread constraint keyed on kubernetes.io/hostname
Spread constraint set to ScheduleAnyway (soft) Switch to DoNotSchedule (hard) for mission-critical services
Not enough schedulable nodes to satisfy a hard spread Add Cluster Autoscaler/Karpenter capacity, or the hard constraint just leaves pods Pending
PodDisruptionBudget missing A drain/eviction can re-collapse an already-spread set of pods back onto fewer nodes
Replica count too low (e.g. replicas: 2) Losing 1 of 2 is a 50% capacity loss even with perfect spread — use >= 3 for mission-critical

The fix — make single-node concentration structurally impossible

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  template:
    spec:
      topologySpreadConstraints:
      # Spread across NODES — the direct fix for "3/3 on one node"
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: DoNotSchedule    # hard — refuse to co-locate beyond skew 1
        labelSelector:
          matchLabels: {app: my-app}
      # Spread across AZs too — protects against a whole-AZ failure, not just one node
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels: {app: my-app}

maxSkew: 1 with topologyKey: kubernetes.io/hostname caps the difference in pod count between the most- and least-loaded node at 1. With 3 replicas and 3+ available nodes, this forces one pod per node — losing any single node now drops you to 2/3, not 0/3.

Older/equivalent mechanism — pod anti-affinity (still common in existing manifests; topologySpreadConstraints is the modern preferred API):

spec:
  template:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels: {app: my-app}
            topologyKey: kubernetes.io/hostname   # hard: never co-locate two replicas on the same node

Why DoNotSchedule (hard), not ScheduleAnyway (soft), for mission-critical services: with the soft form, the scheduler prefers spreading but will still co-locate replicas if it has to — silently reintroducing the exact single-point-of-failure this is meant to prevent, with no warning at all. The hard form instead leaves a pod Pending if it truly can't satisfy the spread — which surfaces as a visible, alertable scheduling problem you can fix by adding capacity, instead of a silent landmine that only detonates when the node actually dies.

This must be paired with cluster capacity, or it just creates Pending pods instead:

□ At least as many schedulable nodes as replicas (3 replicas needs >= 3 nodes with free capacity)
□ Cluster Autoscaler / Karpenter can add nodes if the hard constraint currently can't be satisfied
□ PodDisruptionBudget (minAvailable) so a voluntary drain doesn't undo the spread by
  evicting 2 of 3 already-spread pods back onto the same remaining node
□ replicas >= 3 for mission-critical services — with 2 replicas, losing 1 node is
  already a 50% capacity loss even with perfect spread

Detecting this before (or after) it bites you

# Where are a Deployment's replicas actually scheduled right now?
kubectl get pods -l app=my-app -o wide

# Fleet-wide audit: flag any app with >1 replica but only 1 distinct node in use
kubectl get pods -A -o json | jq -r '
  .items | group_by(.metadata.labels.app) |
  map({app: .[0].metadata.labels.app, nodes: (map(.spec.nodeName) | unique)}) |
  map(select((.nodes | length) == 1)) '

Prevention: Treat topologySpreadConstraints as a default, not opt-in, for every Deployment with more than 1 replica that matters for availability — enforce it fleet-wide via a Kyverno/OPA mutating policy so teams can't accidentally ship without it. Re-run the jq audit periodically (or wire it into a policy-as-code check), since a Deployment can drift into single-node concentration again after a later reschedule/drain even if it was correctly spread at initial rollout.


ImagePullBackOff / ErrImagePull

Container image can't be pulled. ErrImagePull is the first attempt; ImagePullBackOff is repeated failure with backoff.

kubectl describe pod <pod> -n <ns>
# Events: Failed to pull image "...", reason in the message

Cause tree

Error message Cause Fix
unauthorized: authentication required No pull secret / wrong creds Add imagePullSecrets to pod spec
not found / manifest unknown Tag doesn't exist docker pull <image> locally to verify
no basic auth credentials ECR token expired (12hr TTL) Refresh ECR token, or use IRSA + ECR pull-through
exec format error Wrong arch (arm image on amd64 node) Build multi-arch image
connection refused / timeout Node can't reach registry Check node's internet access / NAT GW / VPC endpoint for ECR
ImagePullBackOff on private registry imagePullSecrets missing or wrong namespace Secret must be in same namespace as pod
# Test pulling manually on the node
kubectl debug node/<node-name> -it --image=busybox
# inside: docker pull <image> or crictl pull <image>

# Check if imagePullSecret exists in the right namespace
kubectl get secret regcred -n <ns>

# For ECR: check node IAM role has ecr:GetAuthorizationToken
aws iam simulate-principal-policy \
  --policy-source-arn <node-role-arn> \
  --action-names ecr:GetAuthorizationToken

Prevention: Use image digest pinning (image: my-app@sha256:abc123) instead of mutable tags in production — guarantees the same image always. Set up ECR pull-through cache or mirror to avoid rate limits. For ECR: use IRSA on the node role with ecr:GetAuthorizationToken + ecr:BatchGetImage. Test image pulls in CI before deploying.


Pod Stuck in Terminating

kubectl delete pod was run but pod stays in Terminating state indefinitely.

kubectl describe pod <pod> -n <ns>
# Look for: finalizers, volumes not unmounting, preStop hook hanging

Causes

1. Finalizer not being removed

kubectl get pod <pod> -n <ns> -o json | jq '.metadata.finalizers'
# If a controller crashed and never removed the finalizer, pod hangs

# Force remove finalizer (only if you're sure the controller is gone)
kubectl patch pod <pod> -n <ns> -p '{"metadata":{"finalizers":[]}}' --type=merge

2. preStop hook hanging

# preStop has a hard deadline of terminationGracePeriodSeconds (default 30s)
# If hook runs longer, pod is force killed after the grace period
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 5"]  # must complete within grace period

3. Volume not unmounting (PVC)

kubectl describe pod <pod> | grep -A5 "Volumes"
# "Unable to unmount volumes" in events = storage driver issue

# Force delete as last resort (may leave volume in bad state)
kubectl delete pod <pod> -n <ns> --grace-period=0 --force

4. Node is NotReady / unreachable

# If node went offline, pods on it stay Terminating until node comes back or is deleted
kubectl get node <node>
kubectl delete node <node>   # removes the node object, pods get rescheduled

Prevention: Avoid finalizers unless necessary — they're the #1 cause of stuck Terminating pods. Set terminationGracePeriodSeconds to a realistic value (preStop duration + drain time + buffer). For node failures: enable NonGracefulNodeShutdown feature gate (GA in 1.28) so stuck Terminating pods are auto-cleaned up.


Node NotReady

kubectl get nodes
# NAME        STATUS      ROLES    AGE
# node-1      NotReady    <none>   2d

kubectl describe node <node-name>
# Look at: Conditions, Events, kubelet logs

Cause tree

Node NotReady
├── kubelet stopped
│   └── ssh to node: systemctl status kubelet
│       journalctl -u kubelet -n 100
│
├── Disk pressure (DiskPressure=True)
│   ├── Node full of logs/images → kubelet evicts pods
│   └── Fix: kubectl drain + increase disk, or add imagePrunner CronJob
│       docker system prune / crictl rmi --prune
│
├── Memory pressure (MemoryPressure=True)
│   ├── System processes consuming memory
│   └── Fix: kubectl drain + investigate, check for memory leak in DaemonSets
│
├── PID pressure (PIDPressure=True)
│   ├── Too many processes (fork bomb, runaway threads)
│   └── Fix: find the pod: kubectl top pods --sort-by=cpu
│
├── Network unreachable
│   ├── CNI plugin crashed → pods can't get IPs
│   └── kubectl logs -n kube-system -l k8s-app=aws-node (VPC CNI)
│       kubectl logs -n kube-system -l app=calico-node
│
└── Cloud provider issue (EKS)
    └── EC2 instance health check failing → terminate + replace node
        aws ec2 describe-instance-status --instance-id <id>
# Cordon + drain to safely remove workloads before investigating
kubectl cordon <node>
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data

# Check node events
kubectl get events -n default --field-selector involvedObject.name=<node>

Prevention: Monitor node conditions with kube_node_status_condition Prometheus metric. Alert on DiskPressure=True before pods are evicted. Add image GC thresholds in kubelet config (imageGCHighThresholdPercent: 80). Use node health checks with AWS Auto Scaling Group health check integration so unhealthy nodes are automatically replaced.


High Latency / Slow Requests

Pod is Running, no crashes, but requests are slow.

Step 1 — isolate the layer

# Is it all pods or specific ones?
kubectl top pods -n <ns>    # CPU throttled? Memory pressure?

# Check if requests hit a specific pod (check per-pod metrics in Grafana/Datadog)
# Is load balancer distributing unevenly?

Step 2 — CPU throttling (most common invisible cause)

# Check throttling metrics
kubectl top pod <pod> -n <ns>
# If CPU is at limit constantly → throttled

# In Prometheus:
# container_cpu_cfs_throttled_seconds_total — cumulative throttle time
# rate(container_cpu_cfs_throttled_periods_total[5m]) / rate(container_cpu_cfs_periods_total[5m])
# > 0.25 means 25%+ of scheduling periods throttled → very bad for latency

Fix: Either raise the CPU limit, or remove it entirely (keep only requests). CPU limits cause p99 latency spikes even when average CPU is low.

resources:
  requests:
    cpu: "500m"
  limits:
    cpu: "2000m"   # raise this, or remove limits entirely for latency-sensitive services

Step 3 — GC pauses (Go / JVM)

# Go: check GC stats via pprof
kubectl port-forward pod/<pod> 6060:6060
curl http://localhost:6060/debug/pprof/heap > heap.prof
go tool pprof heap.prof

# Check GC frequency
curl http://localhost:6060/debug/vars | jq '.memstats'
# NumGC high + PauseTotalNs high → GC pressure

Fix for Go: Set GOMEMLIMIT to give GC headroom before hitting k8s limit.

Step 4 — connection pool exhaustion

# Symptoms: latency spikes at high concurrency, logs show "connection wait timeout"
# App is waiting for a DB connection from the pool

# Check pool metrics if exposed, or:
kubectl exec <pod> -- cat /proc/<pid>/net/tcp | wc -l   # open TCP connections

Fix: increase pool size, or add read replicas / RDS Proxy.

Step 5 — DNS resolution slow

# DNS latency inside cluster
kubectl run dnstest --rm -it --image=busybox -- sh
time nslookup my-service.my-namespace.svc.cluster.local

# CoreDNS performance
kubectl top pods -n kube-system -l k8s-app=kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns | grep -i error

Fix: use fully qualified domain names (FQDN) to avoid search domain iteration, or tune CoreDNS replicas.

Prevention: Never set CPU limits on latency-sensitive services — use requests only. Alert on container_cpu_cfs_throttled_periods_total / container_cpu_cfs_periods_total > 0.25. Set dnsConfig.options: [{name: ndots, value: "2"}] on all pods. Use GOMEMLIMIT for Go services. Deploy NodeLocal DNSCache to eliminate DNS as a latency source.


PVC Stuck in Pending

kubectl get pvc -n <ns>
# NAME    STATUS    VOLUME   CAPACITY   STORAGECLASS
# data    Pending                       gp3

kubectl describe pvc data -n <ns>
# Events tell you why
Event Cause Fix
no persistent volumes available No PV matches (static provisioning) Create a PV or switch to dynamic
storageclass not found Wrong storageClassName kubectl get storageclass — check name
waiting for first consumer StorageClass has volumeBindingMode: WaitForFirstConsumer Normal — PVC binds when pod is scheduled
failed to provision volume CSI driver error / IAM permissions Check CSI driver pod logs
exceeded quota ResourceQuota on namespace kubectl describe quota -n <ns>
# Check CSI driver (EBS on EKS)
kubectl logs -n kube-system -l app=ebs-csi-controller -c ebs-plugin | tail -50

# Check storage classes
kubectl get storageclass

# Check if IAM role for CSI has ebs:CreateVolume permission

Prevention: Use WaitForFirstConsumer volumeBindingMode on StorageClasses — prevents PVCs binding to the wrong AZ before the pod is scheduled. Set ResourceQuota for storage to prevent runaway PVC creation. Test CSI driver RBAC with kubectl auth can-i in staging before deploying new clusters.


Resource Quota / LimitRange Blocking Pods

# Pod fails to create with "exceeded quota" or "must specify limits"
kubectl describe quota -n <ns>
kubectl describe limitrange -n <ns>

# Example output:
# Resource         Used    Hard
# requests.cpu     1800m   2000m    ← close to limit
# limits.memory    3Gi     4Gi

LimitRange sets defaults and min/max for containers in a namespace. If a pod has no resources set and LimitRange requires it, the pod is rejected.

# See what defaults are being injected
kubectl get limitrange -n <ns> -o yaml

Prevention: Set LimitRange defaults in every namespace so pods without resource specs still get sane defaults. Alert on kube_resourcequota{type="used"} / kube_resourcequota{type="hard"} > 0.80 to catch quota exhaustion before it blocks deployments. Document quota allocations per team in runbooks.


Quick Reference: All Pod States

Status Meaning First command
Pending Not scheduled yet kubectl describe pod → Events: FailedScheduling
Init:0/1 Init container running/failed kubectl logs <pod> -c <init-container>
PodInitializing Init done, main container starting Normal, wait
Running but not Ready Readiness probe failing kubectl describe pod → probe config + app logs
CrashLoopBackOff Container crashing repeatedly kubectl logs --previous, check exit code
OOMKilled Memory limit exceeded Increase limits.memory, check for leak
Error Container exited non-zero once kubectl logs <pod>
Completed Container exited 0 (Job/init) Normal for Jobs
Terminating Delete issued, waiting grace period Check finalizers, preStop hook
ImagePullBackOff Can't pull image kubectl describe pod → registry/auth/tag
ErrImageNeverPull imagePullPolicy: Never + image missing Push image to node or change policy
NodeLost / Unknown Node unreachable kubectl get node, check cloud console

Pod Running But Requests Failing Silently

Symptom: Pod is Running and Ready, but requests return errors or wrong data. Readiness probe passes but the app is broken inside.

flowchart TD
    A[Pod Running + Ready] --> B[curl inside pod]
    B --> C{Response OK?}
    C -- No --> D[Check app logs]
    D --> E{Error in logs?}
    E -- Yes --> F["Fix app / config"]
    E -- No --> G["Check probe endpoint<br/>vs real health path"]
    G --> H["Check env vars / ConfigMap"]
    H --> I[Check upstream dependency]
    C -- Yes --> J["Issue is external<br/>ingress/svc layer"]

Commands:

# exec into pod and curl the app directly
kubectl exec -it <pod> -- curl -v http://localhost:<port>/healthz

# check app logs
kubectl logs <pod> --tail=100

# check readiness probe config
kubectl describe pod <pod> | grep -A 10 "Readiness"

# check env vars
kubectl exec -it <pod> -- env | grep -i db
kubectl exec -it <pod> -- env | grep -i api

Expected output snippet:

Readiness:  http-get http://:8080/ready delay=5s timeout=1s period=10s
# If /ready returns 200 but app is broken, the probe path is wrong

Root Cause Table:

Root Cause Fix
Readiness probe checks wrong path Update probe to check real health endpoint
Missing/wrong env var or ConfigMap Verify envFrom / env in pod spec
Upstream DB/service unavailable Check upstream pod/service connectivity
App bug returns 200 on errors Fix application logic

Prevention: Separate liveness from readiness probes — readiness should check actual app health (e.g. DB ping), liveness should only check if the process is hung. Use /readyz for readiness (checks dependencies) and /livez for liveness (just checks process). Add integration tests in CI that deploy to staging and verify the probe endpoints return correct status codes.


DNS Resolution Failures Inside Pods

Symptom: Pod gets NXDOMAIN or connection timeout when resolving service names. nslookup my-svc fails inside the pod.

sequenceDiagram
    participant P as Pod
    participant CD as CoreDNS
    participant S as Service
    participant E as Endpoints

    P->>CD: nslookup my-svc
    CD-->>P: NXDOMAIN? Check CoreDNS pods
    CD->>S: Resolve my-svc.default.svc.cluster.local
    S-->>CD: Service not found? Check svc exists
    S->>E: Look up backends
    E-->>S: Empty? Check pod labels + readiness
    CD-->>P: Returns ClusterIP

Commands:

# test DNS from inside pod
kubectl exec -it <pod> -- nslookup my-svc
kubectl exec -it <pod> -- nslookup my-svc.default.svc.cluster.local

# check resolv.conf
kubectl exec -it <pod> -- cat /etc/resolv.conf
# expect: search default.svc.cluster.local svc.cluster.local cluster.local

# is CoreDNS running?
kubectl get pods -n kube-system -l k8s-app=kube-dns

# CoreDNS logs
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50

Root Cause Table:

Root Cause Fix
CoreDNS pod down/crashlooping Restart CoreDNS pods; check its ConfigMap
Service doesn't exist in namespace kubectl get svc — create if missing
Wrong search domain in resolv.conf Check dnsConfig in pod spec
NetworkPolicy blocks pod→CoreDNS (port 53) Allow egress to kube-dns on UDP/TCP 53

Prevention: Deploy NodeLocal DNSCache DaemonSet — each node caches DNS locally, eliminating CoreDNS as a SPOF. Set ndots: 2 in pod dnsConfig to reduce DNS round trips. Alert on coredns_dns_responses_total{rcode="SERVFAIL"} and CoreDNS OOMKilled. Scale CoreDNS replicas proportional to cluster size (1 replica per 16 nodes minimum).


NetworkPolicy Blocking Traffic

Symptom: Pod A can't reach Pod B or a Service, but both are Running. No obvious error — just connection refused or timeout.

flowchart TD
    A[Pod A --> Pod B fails] --> B[kubectl exec -- curl pod-b]
    B --> C{Timeout or refused?}
    C -- Timeout --> D[Check NetworkPolicy exists]
    D --> E["Check ingress rules<br/>on destination pod"]
    E --> F{"Labels match<br/>podSelector?"}
    F -- No --> G["Fix policy selector<br/>or pod labels"]
    F -- Yes --> H["Check egress rules<br/>on source pod"]
    H --> I{"Egress allows<br/>dest port?"}
    I -- No --> J[Add egress rule]
    I -- Yes --> K["Check CNI plugin<br/>supports NetworkPolicy"]
    C -- Refused --> L["App not listening<br/>on that port"]

Commands:

# list all network policies in namespace
kubectl get networkpolicy -n <ns>

# inspect a policy
kubectl describe networkpolicy <policy-name> -n <ns>

# test connectivity from pod A
kubectl exec -it <pod-a> -- curl -v http://<pod-b-ip>:<port>

# use ephemeral debug container (k8s 1.23+)
kubectl debug -it <pod-a> --image=nicolaka/netshoot --target=<container>
# then: curl, nslookup, tcpdump

Root Cause Table:

Root Cause Fix
Ingress policy missing from rule Add podSelector matching source pod labels
Egress policy missing to rule Add egress rule for destination port
Pod labels don't match policy selector Align pod labels with podSelector
CNI doesn't enforce NetworkPolicy Switch to Calico/Cilium/Weave

Prevention: Adopt a default-deny-all NetworkPolicy per namespace and explicitly allowlist required traffic. Test policies with kubectl exec -- curl in staging before prod. Use Cilium's hubble observe --verdict DROPPED to see what's being blocked in real time. Include NetworkPolicy in service deployment manifests so policies travel with the service.


Service Not Routing to Pods (Empty Endpoints)

Symptom: Service exists and has a ClusterIP, but requests get connection refused or no backends respond.

flowchart TD
    A[kubectl get svc — svc exists] --> B[kubectl get endpoints my-svc]
    B --> C{Endpoints empty?}
    C -- Yes --> D["Compare svc selector<br/>vs pod labels"]
    D --> E{Labels match?}
    E -- No --> F[Fix labels or selector]
    E -- Yes --> G[Check pod Ready?]
    G --> H{Pod Ready=True?}
    H -- No --> I[Fix readiness probe]
    H -- Yes --> J["Check targetPort vs<br/>containerPort"]
    J --> K{Ports match?}
    K -- No --> L[Fix targetPort in svc]
    K -- Yes --> M["Check pod namespace<br/>matches svc namespace"]
    C -- No --> N["Check kube-proxy /<br/>iptables rules"]

Commands:

# check endpoints
kubectl get endpoints my-svc
# ENDPOINTS column should not be "<none>"

# describe the service to see selector
kubectl describe svc my-svc

# check pod labels
kubectl get pods --show-labels

# verify targetPort
kubectl get svc my-svc -o jsonpath='{.spec.ports[*].targetPort}'

Expected output snippet:

NAME      ENDPOINTS         AGE
my-svc    <none>            5m   ← problem: no backends

Root Cause Table:

Root Cause Fix
Service selector doesn't match pod labels Align spec.selector with pod labels
Pod not Ready (failing readiness probe) Fix readiness probe or app startup
targetPort ≠ containerPort Match targetPort to the port the app listens on
Pod in different namespace Service and pods must be in same namespace

Prevention: Use label conventions enforced by LimitRange/OPA — all pods must have app label. Write CI checks (conftest, kyverno) that validate Service selectors match Deployment labels before merge. Add a Prometheus alert on kube_endpoint_address_not_ready > 0 sustained for > 2 minutes.


HPA Not Scaling

Symptom: Load is high but HPA stays at minReplicas. kubectl get hpa shows <unknown> for current metrics.

flowchart TD
    A["kubectl get hpa —<br/>metrics unknown"] --> B[metrics-server running?]
    B --> C{"kubectl top pods<br/>works?"}
    C -- No --> D["Install / fix<br/>metrics-server"]
    C -- Yes --> E["Pod has resource<br/>requests set?"]
    E --> F{requests.cpu set?}
    F -- No --> G["Add resource requests<br/>to pod spec"]
    F -- Yes --> H[Custom metrics path?]
    H --> I["prometheus-adapter<br/>running?"]
    I --> J["Check HPA<br/>externalMetrics config"]
    J --> K["Check RBAC for<br/>metrics API"]
    K --> L["kubectl get apiservices<br/>v1beta1.metrics.k8s.io"]

Commands:

# check HPA status
kubectl get hpa
kubectl describe hpa <hpa-name>

# verify metrics-server is reachable
kubectl top pods -n <ns>
kubectl top nodes

# check metrics API registration
kubectl get apiservices | grep metrics

# check if resource requests are set on pods
kubectl get pod <pod> -o jsonpath='{.spec.containers[*].resources}'

Expected output snippet:

NAME   REFERENCE         TARGETS         MINPODS  MAXPODS  REPLICAS
web    Deployment/web    <unknown>/50%   2        10       2
# <unknown> means metrics pipeline is broken

Root Cause Table:

Root Cause Fix
metrics-server not installed Install via kubectl apply -f metrics-server.yaml
No resources.requests.cpu on pod Add CPU request to pod/deployment spec
v1beta1.metrics.k8s.io not registered Fix metrics-server or prometheus-adapter install
RBAC missing for metrics API Add ClusterRole for HPA to read metrics

Prevention: Always set resources.requests.cpu — HPA silently shows <unknown> without it. Install metrics-server as part of cluster bootstrap (not optional). For production: use KEDA with external metrics (queue depth, RPS) instead of CPU-only HPA — CPU is a lagging indicator of load.


Deployment Stuck in Rollout

Symptom: kubectl rollout status hangs. New pods won't start (CrashLoop/Pending) or old pods won't terminate.

flowchart TD
    A[kubectl rollout status hangs] --> B[New pods state?]
    B --> C{"Pending or<br/>CrashLoop?"}
    C -- Pending --> D["Check new pod events<br/>— scheduling issue"]
    C -- CrashLoop --> E["kubectl logs new-pod<br/>— app crash"]
    B --> F[Old pods state?]
    F --> G{Stuck Terminating?}
    G -- Yes --> H["Check preStop hook<br/>/ finalizers"]
    H --> I["kubectl patch to<br/>remove finalizer"]
    G -- No --> J["Check maxUnavailable<br/>/ maxSurge settings"]
    J --> K["Check PodDisruptionBudget<br/>blocking drain"]
    K --> L[kubectl get pdb]

Commands:

# check rollout status
kubectl rollout status deployment/<name>

# rollout history
kubectl rollout history deployment/<name>

# inspect new and old pods
kubectl get pods -l app=<name> --sort-by=.metadata.creationTimestamp

# describe a stuck pod
kubectl describe pod <new-pod>

# check PodDisruptionBudget
kubectl get pdb -n <ns>
kubectl describe pdb <pdb-name>

# rollback if needed
kubectl rollout undo deployment/<name>

Root Cause Table:

Root Cause Fix
New image crash / bad config Check logs; rollback with kubectl rollout undo
PDB blocks pod termination Temporarily scale PDB minAvailable or fix pod health
preStop hook hangs Fix hook or reduce terminationGracePeriodSeconds
maxUnavailable=0 + unschedulable nodes Fix node capacity or adjust rollout strategy

Prevention: Set a progressDeadlineSeconds (default 600s) — rollout auto-fails if new pods don't become Ready within that window, making CI pipelines fail fast. Always set a PDB with minAvailable: 1 so rollouts can't kill all replicas. Add a readiness probe that fails until the app is truly ready (not just started).


Webhook Admission Failures

Symptom: kubectl apply returns an admission webhook error. Resource can't be created or updated.

flowchart TD
    A["kubectl apply fails —<br/>webhook error"] --> B["API server calls<br/>MutatingWebhook"]
    B --> C["API server calls<br/>ValidatingWebhook"]
    C --> D{"Webhook pod<br/>Running?"}
    D -- No --> E["Fix webhook<br/>deployment"]
    D -- Yes --> F{"Cert valid?<br/>caBundle correct?"}
    F -- No --> G["Renew cert /<br/>update caBundle"]
    F -- Yes --> H{"failurePolicy<br/>= Fail?"}
    H -- Yes --> I["Webhook timeout<br/>or crash --> blocks"]
    H -- Ignore --> J["Webhook skipped<br/>on failure"]
    I --> K["Check webhook<br/>service + port"]

Commands:

# list webhook configs
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations

# inspect a webhook
kubectl describe validatingwebhookconfiguration <name>

# check webhook pod and service
kubectl get pods -n <webhook-ns>
kubectl get svc -n <webhook-ns>

# test webhook service reachability
kubectl exec -it <debug-pod> -- curl -k https://<webhook-svc>.<ns>.svc/validate

Root Cause Table:

Root Cause Fix
Webhook pod down Restart webhook deployment
TLS cert expired / wrong caBundle Rotate cert; update caBundle in webhook config
failurePolicy: Fail + unreachable webhook Set failurePolicy: Ignore temporarily or fix pod
Webhook rejects valid resource (logic bug) Fix webhook validation logic

Prevention: Set failurePolicy: Ignore on all non-critical webhooks (security webhooks can be Fail). Add namespaceSelector to exclude kube-system from webhook scope — prevents the webhook from blocking its own recovery. Use cert-manager to auto-rotate webhook TLS certs. Run webhook pods with PDB and 2+ replicas.


Node Disk Pressure Evicting Pods

Symptom: Pods suddenly evicted with reason: Evicted. Node shows DiskPressure=True condition.

flowchart TD
    A["kubectl get pods —<br/>status Evicted"] --> B["kubectl describe node<br/>DiskPressure=True?"]
    B --> C{Which threshold?}
    C -- nodefs --> D["df -h on node<br/>/ var/lib/kubelet"]
    C -- imagefs --> E["du on<br/>/var/lib/containerd"]
    D --> F{Logs filling disk?}
    F -- Yes --> G["Fix log rotation /<br/>reduce log verbosity"]
    F -- No --> H{Large image layers?}
    H -- Yes --> I["crictl rmi unused<br/>images"]
    H -- No --> J["Expand disk /<br/>add node storage"]
    E --> I

Commands:

# find evicted pods
kubectl get pods --field-selector=status.phase=Failed -A | grep Evicted

# describe node for disk pressure
kubectl describe node <node-name> | grep -A 5 "Conditions"

# debug node filesystem (k8s 1.23+)
kubectl debug node/<node-name> -it --image=ubuntu
# inside: df -h, du -sh /var/log/containers/*

# clean up evicted pods
kubectl delete pods --field-selector=status.phase=Failed -A

Expected output snippet:

Conditions:
  Type            Status  ...
  DiskPressure    True    kubelet has disk pressure

Root Cause Table:

Root Cause Fix
Container logs unbounded Configure logrotate or set --container-log-max-size
Unused container images Run crictl rmi --prune or configure image GC
emptyDir volumes growing large Add sizeLimit to emptyDir volume spec
Node disk too small Expand EBS volume or add larger node group

Prevention: Set kubelet --container-log-max-size=50Mi --container-log-max-files=3. Configure image GC thresholds (imageGCHighThresholdPercent: 80). Alert on node_filesystem_avail_bytes / node_filesystem_size_bytes < 0.15. Add sizeLimit to all emptyDir volumes. Use instance types with ≥ 100Gi root volumes for nodes.


etcd Slow / API Server Timeout

Symptom: kubectl commands hang or return timeout. All operations are slow. Cluster feels frozen.

flowchart TD
    A["kubectl commands slow<br/>or timeout"] --> B["kubectl get --raw /healthz"]
    B --> C{"API server<br/>healthy?"}
    C -- No --> D["Check API server pod<br/>logs + restarts"]
    C -- Yes --> E["Check etcd metrics<br/>wal_fsync_duration"]
    E --> F{"fsync > 10ms?"}
    F -- Yes --> G["etcd disk I/O issue<br/>— check IOPS"]
    G --> H["Move etcd to<br/>faster SSD / gp3"]
    F -- No --> I["etcd DB size large?<br/>etcdctl endpoint status"]
    I --> J{"DB > 4GB?"}
    J -- Yes --> K["etcdctl compact +<br/>etcdctl defrag"]
    J -- No --> L["Check etcd member<br/>health — split brain?"]

Commands:

# check API server health
kubectl get --raw /healthz
kubectl get --raw /readyz

# etcd health (run on etcd node or via pod)
etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
  --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
  endpoint health

# etcd status (shows DB size)
etcdctl endpoint status --write-out=table

# defrag etcd (do during low-traffic window)
etcdctl defrag --endpoints=https://127.0.0.1:2379

Root Cause Table:

Root Cause Fix
Slow disk (high WAL fsync latency) Move etcd to SSD; use gp3 with provisioned IOPS
etcd DB size > quota (default 2GB) Compact revisions + defrag; increase --quota-backend-bytes
etcd member unhealthy / quorum lost Recover failed member; restore from snapshot
Too many objects (large secrets/CMs) Audit and prune large resources

Prevention: Provision etcd on dedicated fast SSDs (io2 or local NVMe). Alert on etcd_disk_wal_fsync_duration_seconds_bucket{quantile="0.99"} > 0.01 (10ms p99). Set up automated daily compaction and defrag as a CronJob. Monitor etcd_mvcc_db_total_size_in_bytes — alert at 1.5GB (before hitting 2GB default quota). Run etcd on at least 3 nodes for quorum.


RBAC 403 — Pod Can't Call Kubernetes API

Symptom: App inside a pod gets 403 Forbidden when calling the Kubernetes API. ServiceAccount token is present but denied.

flowchart TD
    A["403 in app logs —<br/>k8s API call"] --> B["kubectl describe pod<br/>— which SA?"]
    B --> C{"Non-default SA<br/>attached?"}
    C -- No --> D["Create dedicated SA<br/>+ bind role"]
    C -- Yes --> E["kubectl get role /<br/>clusterrole exists?"]
    E --> F{"Role has<br/>needed verbs?"}
    F -- No --> G["Add missing rules<br/>to Role"]
    F -- Yes --> H["RoleBinding links<br/>SA to Role?"]
    H --> I{"Binding exists<br/>in right namespace?"}
    I -- No --> J["Create RoleBinding /<br/>ClusterRoleBinding"]
    I -- Yes --> K["kubectl auth can-i<br/>to verify"]

Commands:

# test permissions as the service account
kubectl auth can-i get pods \
  --as=system:serviceaccount:default:my-sa -n default

kubectl auth can-i list secrets \
  --as=system:serviceaccount:default:my-sa -n default

# check what SA the pod uses
kubectl get pod <pod> -o jsonpath='{.spec.serviceAccountName}'

# list role bindings in namespace
kubectl get rolebinding -n <ns>
kubectl get clusterrolebinding | grep my-sa

# describe the binding
kubectl describe rolebinding <name> -n <ns>
kubectl describe clusterrolebinding <name>

Expected output snippet:

no  ← kubectl auth can-i returns "no" → RBAC is the blocker

Root Cause Table:

Root Cause Fix
Default SA has no permissions Create Role + RoleBinding for a dedicated SA
RoleBinding in wrong namespace Re-create binding in the pod's namespace
Role missing required verb/resource Add verbs and resources to Role rules
ClusterRole needed but only Role created Use ClusterRole + ClusterRoleBinding for cluster-wide access

Prevention: Use least-privilege by default — never use cluster-admin for application service accounts. Audit existing bindings regularly: kubectl get clusterrolebinding -o json | jq '[.items[] | select(.subjects[]?.kind=="ServiceAccount")]'. Use Kyverno or OPA Gatekeeper policies to prevent broad RBAC grants in CI. Test RBAC with kubectl auth can-i --as=system:serviceaccount:ns:sa in staging.