hands-on labKubernetes in your browser

Stuck-Pending Triage Day: Diagnose Any GPU Pod That Won't Run

The capstone NCA-AIIO operations lab. Walk the five-stage pod lifecycle (admission → scheduling → image pull → runtime → readiness), learn which `kubectl describe` field signals each stuck point, and finish by fixing four broken GPU pods, each broken at a different stage.

time
40 min
checked steps
5
level
Intermediate
setup
None

$29.99a month with Pro

Pro opens this lab and every other one. Cancel any time.

The job

It's 2 a.m. Pager fires. Four GPU pods are stuck Pending, but each one is stuck at a different stage of the pod lifecycle, and the symptom (STATUS: Pending) looks identical from kubectl get pods. This capstone teaches the systematic diagnostic walk every NCA-AIIO platform engineer needs: which event message means admission rejected, vs scheduler couldn't fit, vs kubelet can't pull the image, vs runtime can't start the container. By the end you'll be able to look at any stuck pod and place it in the chain in 30 seconds.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    The five-gate pod lifecycle and where pods get stuck

    Every pod has to clear five gates between kubectl apply and answering its first request.

    read this step below ↓

  2. 2

    Gate 1: Admission rejection (LimitRange max)

    The first gate every pod hits is API server admission.

  3. 3

    Gate 2: Scheduling stuck (FailedScheduling)

    The pod cleared admission (the API server accepted the spec, it's persisted in etcd, you can see it with kubectl get pods), but the kube-scheduler can't place it on any node.

  4. 4

    Gate 3: Image pull stuck (ImagePullBackOff)

    The pod cleared admission AND the scheduler, .spec.nodeName is set, so it's been placed on a node, but the kubelet can't pull the container image.

  5. 5

    Capstone: four broken GPU pods, four different gates

    You're on call.

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: The five-gate pod lifecycle and where pods get stuck

The five-gate pod lifecycle and where pods get stuck

Every pod has to clear five gates between kubectl apply and answering its first request. A pod that's "stuck" is wedged at exactly one of them. Knowing which gate is the difference between fixing it in 30 seconds and chasing the wrong layer for 30 minutes.

kubectl apply ─► [1. Admission ]─► [2. Scheduling ]─► [3. Image pull ]─► [4. Runtime startup ]─► [5. Readiness ]─► serving
                  API server       kube-scheduler       kubelet              kubelet                kubelet
                                                        (CRI client)         (CRI runtime)          (probes)

                  rejected:        Pending              ImagePullBackOff     ContainerCreating      Running 0/1
                  Forbidden        FailedScheduling     ErrImagePull         (CrashLoopBackOff       (Ready never
                                                                              if container exits)    flips True)

The five gates

1. Admission — the API server validates the request

The API server runs every PodSpec through admission controllers before it's even persisted to etcd. Common rejections:

  • ResourceQuota — Forbidden: exceeded quota: requests.cpu, requested: 5, used: 1, limited: 2
  • LimitRange — Forbidden: maximum cpu usage per Container is 2, but limit is 5
  • Pod Security Standards — Forbidden: hostNetwork=true is not allowed
  • Kyverno / OPA admission policies — image registry not in allowlist

Diagnostic: kubectl apply returns the rejection synchronously. The pod never appears in kubectl get pods because it was rejected before persistence. If you don't see your pod, the bug is here.

2. Scheduling — kube-scheduler picks a node

The pod exists in etcd with .spec.nodeName: "". The scheduler walks the node list looking for one that satisfies:

  • requested resources (nvidia.com/gpu, cpu, memory) ≤ node Allocatable
  • nodeSelector and nodeAffinity match the node's labels
  • pod's tolerations cover the node's taints
  • podAntiAffinity doesn't conflict with already-placed pods

Diagnostic: Pod is Pending, .spec.nodeName is empty. kubectl describe pod <name> Events section has a Warning FailedScheduling event with the specific reason: "0/1 nodes are available: 1 Insufficient nvidia.com/gpu", "didn't match Pod's node affinity/selector", "node(s) had untolerated taint X".

3. Image pull — kubelet downloads the container image

Once scheduled, the kubelet is responsible for the rest. First step: pull the image (skipped if already present, cached by digest).

  • Tag doesn't exist → 404 Not Found → ErrImagePull → ImagePullBackOff
  • Private registry, no creds → 401 Unauthorized → ditto
  • Docker Hub rate limit → 429 Too Many Requests → ditto
  • DNS / firewall → dial tcp ... connection refused

Diagnostic: Pod is ContainerCreating (or directly ImagePullBackOff after several failed pulls). .spec.nodeName is set. kubectl describe pod Events show Failed to pull image "X": ... with the underlying HTTP/network error. kubectl logs returns nothing because no container has started yet.

4. Runtime startup — CRI runtime creates the container

Once the image is local, the kubelet asks the container runtime (containerd / CRI-O) to create the sandbox and the container. Failures here:

  • runtimeClassName references unknown handler → failed to create pod sandbox: ... no runtime for "nvidia" is configured
  • securityContext violations → runAsNonRoot set but image runs as root
  • PVC not bound → MountVolume.SetUp failed for volume "X": persistentvolumeclaim "X" not found
  • OCI hook failures (NVIDIA Container Runtime, AppArmor, SELinux) → various per-runtime errors

Diagnostic: Pod is ContainerCreating indefinitely. kubectl describe pod Events show "Failed to create pod sandbox" or "MountVolume.SetUp failed". The events are emitted by the kubelet, NOT the scheduler.

5. Readiness — probes gate the Ready=True transition

The container is running, but it isn't Ready until its readinessProbe passes. Until Ready=True, the Service excludes it from Endpoints. Failures here:

  • No probe → Ready=True immediately (often wrong; see the rolling-update lab)
  • Probe timeout → Readiness probe failed: connection refused
  • Probe path wrong → Readiness probe failed: HTTP probe failed with statuscode: 404

Diagnostic: Pod is Running 0/1. kubectl describe pod Events show Warning Unhealthy Readiness probe failed: .... Container itself is alive — kubectl logs works.

The 30-second triage flowchart

$ kubectl get pod <name>

  Not in list?              → Gate 1 (Admission). `kubectl apply` returned the error;
                              re-apply and read the kubectl output.

  STATUS: Pending           → Gate 2 (Scheduling). describe pod → Events → FailedScheduling.

  STATUS: ContainerCreating → Gate 3 or 4. describe pod → Events:
       message has "pull" → Gate 3 (Image)
       else                → Gate 4 (Runtime / volume / sandbox)

  STATUS: ImagePullBackOff  → Gate 3.

  STATUS: Running but 0/1   → Gate 5 (Readiness). describe pod → Events → Unhealthy.

  STATUS: CrashLoopBackOff  → Gate 4-5 boundary. Container started and crashed.
                              → kubectl logs <name> --previous (the last crashed container's logs)
                              → describe pod for OOMKilled signals

Read your lab cluster's facts

Run the step's command (or click Run). It dumps the cluster's Allocatable, namespace quota, RuntimeClasses, StorageClasses, and PriorityClasses. These are the facts you'll use to recognize what should be in the next four steps' broken pods.

Why this matters in practice: the gate framework is universal — it applies to every pod on every Kubernetes cluster, GPU or not. NCA-AIIO frames triage questions as "given this kubectl output, name the gate" — and once you can place the gate, the fix follows directly.

what you get

  • A Kubernetes cluster and terminal in your browser
  • A check at the end of every step
  • Hints and the full solution
  • Nothing to install or configure

before you start

  • Completed earlier NCA-AIIO labs (resource-requests-limits, gpu-operator-chain, priority-preemption, storage-pvc, workload-controllers)
  • Comfortable reading `kubectl describe pod` events and tracing the pod lifecycle

Exam domains covered

AI Infrastructure & OperationsWorkload Management

Part of the GPU and Kubernetes track

Lab 11 of 20. GPU scheduling on your own Kubernetes cluster, the GPU Operator, CUDA kernels and profiling.

Questions about this lab

What's the difference between Pending and ContainerCreating?

Pending means the scheduler hasn't placed the pod on a node, either because admission rejected it (quota/limits), no node satisfies the pod's requirements (nodeSelector / capacity / taints), or it's still being evaluated. ContainerCreating means the scheduler placed it (you'll see .spec.nodeName set), but the kubelet can't yet start the container, most often image pull failure, volume attach failure, or a runtime handler issue (runtimeClassName references something the kubelet doesn't know about). The right diagnostic differs: for Pending, read kubectl describe pod events for the scheduler's reason; for ContainerCreating, read the kubelet events on the same describe output and the per-container State blocks.

Why is my GPU pod stuck Pending with Insufficient nvidia.com/gpu?

The scheduler couldn't find a node with enough free nvidia.com/gpu capacity. Three layers to check: (1) the namespace's ResourceQuota, kubectl describe quota shows used vs hard caps; if requests.nvidia.com/gpu is at the cap, you'll get an admission rejection, not Pending, but the cap may rise to capacity (8 in this lab vs 4 node capacity). (2) the actual node capacity, kubectl get node -o jsonpath='{.items[0].status.allocatable.nvidia\.com/gpu}'; if other pods are holding all 4 GPUs, your pod is genuinely stuck. (3) preemption candidacy, your pod's PriorityClass determines whether the scheduler can evict lower-priority pods to make room; without a high enough priority, it just waits. The describe output's FailedScheduling event will name which constraint was violated.

How do I diagnose ImagePullBackOff?

kubectl describe pod <name> and read the events. The kubelet emits Failed to pull image "X": ... followed by a specific HTTP status: 404 Not Found means the tag doesn't exist (typo or never pushed), 401 Unauthorized means private registry auth failed (missing imagePullSecrets), 429 Too Many Requests means Docker Hub rate limit hit, connection refused / dial tcp means a network / DNS issue. For private registries, also kubectl get pod -o yaml | grep imagePullSecrets to confirm the secret reference is correct. ImagePullBackOff is the kubelet's backoff state after several failed attempts, fix the underlying issue and either the next backoff cycle picks it up or you kubectl delete pod to force a fresh attempt.

What's the difference between RuntimeClass nvidia not configured and missing nvidia.com/gpu request?

Two different failure shapes that both look like "no GPU access" but break at different stages. runtimeClassName: nvidia references a Kubernetes RuntimeClass; if the kubelet's runtime handler config doesn't have an nvidia entry, the kubelet rejects the pod at sandbox creation: failed to create pod sandbox: ... no runtime for "nvidia" is configured. The pod is ContainerCreating permanently. Different problem: the pod is missing resources.limits.nvidia.com/gpu: 1. Without that, the device plugin doesn't know to inject /dev/nvidia* mounts. The pod runs (sandbox is created via the default runtime), but nvidia-smi: command not found. Same ultimate symptom (no GPU), opposite location in the chain. NCA-AIIO tests this distinction directly, recognize them by the events vs the runtime logs.

What you'll learn

Every Kubernetes pod travels through five gates between kubectl apply and serving traffic: API-server admission, scheduler placement, image pull, container runtime startup, and readiness gating. A pod that's "Pending" or "ContainerCreating" is stuck at one of those five gates, but the right diagnostic command is different for each, and the failure event is buried in a different place in kubectl describe pod. This lab teaches the chain top-to-bottom, then drops you into a Triage Day where four broken GPU pods each break a different gate and you fix them by reading the cluster's response.

The diagnostic patterns covered are the most-frequent production tickets on any GPU cluster: ResourceQuota / LimitRange admission rejection, nodeSelector mismatch, ImagePullBackOff, RuntimeClass-handler-not-configured, missing PVCs, OOMKilled at runtime. Each maps to a specific kubectl describe line you can recognize at a glance, and once you can name the gate, the fix follows directly. NCA-AIIO frames these as "given this stuck pod, name the gate and the fix."