The Symptom: kubectl logs Returns Nothing
Your pod is stuck cycling through CrashLoopBackOff. You run kubectl logs <pod-name> expecting a stack trace, an error message, anything. Instead you get an empty response, or a single blank line. This is where most guides tell you to "check the logs" and move on — which is useless advice when the logs are the problem.
An empty log stream during a crash loop almost always means one of three things: the container is dying before your application's logging layer even initializes, something outside the application (the kubelet, a probe, the OOM killer) is terminating the process before it can flush any output, or you're looking at the wrong container instance entirely because a new one already replaced the crashed one.
This article walks through how to actually diagnose that empty-log scenario using exit codes, --previous logs, and ephemeral debug containers — not just the generic "describe the pod" advice.
Step 1: Stop Trusting kubectl logs Alone
By the time you notice CrashLoopBackOff, Kubernetes has probably already restarted the container at least once. Running kubectl logs without flags shows you the current container instance, which might have only existed for a fraction of a second and produced nothing yet.
Always pull logs from the terminated instance first:
kubectl logs <pod-name> --previous
If that's also empty, you're not dealing with a logging configuration issue — you're dealing with a container that dies before it writes anything to stdout/stderr at all. That's a meaningfully different problem than "my app crashed and logged an error I can't find."
Step 2: Read the Exit Code Like a Diagnosis, Not a Footnote
Run this immediately after confirming empty logs:
kubectl describe pod <pod-name>
Look at the Last State section under the container status. The Exit Code field is the single most informative piece of data you have when logs give you nothing. Here's how to actually read it:
- Exit Code 0 — The process exited cleanly, on its own. If it's still crash-looping, your entrypoint script or main process is finishing successfully and then Kubernetes restarts it because it expects a long-running process. Common with scripts that run a task and exit instead of a server that blocks. Check our breakdown on why a docker container exits with code 0 but shows no error for the exact mechanics.
- Exit Code 1 — A generic uncaught application error. Usually the app tried to log something, but if you're seeing empty logs alongside exit code 1, the crash is happening during startup, before your logging framework or stdout buffer has initialized. Node.js apps crashing on a missing environment variable before the logger loads are a textbook case.
- Exit Code 137 — This is
128 + 9, meaning the process receivedSIGKILL. In Kubernetes this is almost always an OOMKill. The kernel killed the process instantly and violently — there is no graceful shutdown, no flush of log buffers, nothing. This is the single most common cause of a genuinely empty log stream during a crash loop. - Exit Code 143 — This is
128 + 15, meaningSIGTERM. This is a normal termination request, often from a liveness probe failure or a pod being evicted/rescheduled. If your app doesn't have a signal handler forSIGTERM, it dies immediately with whatever was in its log buffer unflushed — which can also look like empty logs.
If you take one thing from this article, take this: 137 and 143 are not application bugs, they're external kills. Debugging your application code for a 137 is wasted time. You need to look at memory limits and node conditions instead.
Step 3: Confirm OOMKill vs Liveness Probe Kill
Both 137 and 143 can produce zero useful logs, but the fix for each is completely different, so you need to tell them apart before doing anything else.
Check the Reason field alongside the exit code in kubectl describe pod:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
If Reason says OOMKilled, your container hit its memory limit. The kubelet's cgroup enforcement kills the process the instant it crosses the boundary — there's no window for your app to log "running low on memory" because there usually isn't one. Fixes here are either raising the memory limit, fixing an actual memory leak in the app, or checking if a JVM/Node heap size is misconfigured relative to the container limit (a very common mistake: setting a 512Mi container limit while the JVM defaults to using up to 25% of host memory it can see, not the cgroup limit).
If you instead see Reason: Error with exit code 143 and no OOMKilled flag, check kubectl describe pod for probe failure events near the bottom of the output:
Warning Unhealthy Liveness probe failed: HTTP probe failed with statuscode: 500
This tells you Kubernetes itself decided the container was unhealthy and terminated it — the app might be running fine from a process standpoint but failing to respond correctly on the liveness endpoint, maybe because it's still initializing when the probe fires too early. A liveness probe with too short an initialDelaySeconds on a slow-starting Java or .NET app is one of the most common self-inflicted crash loops in production, and it produces almost no application-level logs because the app was never actually broken.
Step 4: When Even --previous Gives You Nothing — Use an Ephemeral Debug Container
Sometimes the container dies so fast — during image extraction, before the entrypoint even runs, or due to a broken volume mount — that there's genuinely nothing in any log buffer to retrieve. This is where ephemeral debug containers earn their place.
kubectl debug -it <pod-name> --image=busybox --target=<container-name>
This attaches a temporary container into the same pod's namespaces without restarting the pod, letting you inspect the filesystem, check mounted volumes, verify environment variables, and even try running the target binary manually to see what actually happens:
kubectl debug <pod-name> -it --image=busybox --share-processes --copy-to=debug-pod
The --copy-to variant is especially useful for crash-looping pods, since it creates a copy of the pod you can modify — for example, overriding the command to sleep 3600 so the container stays alive long enough for you to shell in and run the actual startup command by hand, watching exactly where it fails.
Step 5: Rule Out the Red Herrings
A few things that look like the root cause but usually aren't:
- Restart count resetting to zero after you check — this just means the backoff timer reset because the pod ran long enough (usually 10 minutes) without crashing again; it doesn't mean it's fixed.
ImagePullBackOffmasquerading as CrashLoopBackOff — check theEventssection, not just the status field, to confirm the container actually started before crashing versus never starting at all.- Sidecar containers crashing while the main container looks fine — in multi-container pods, always check
kubectl get pod <pod-name> -o jsonpath='{.status.containerStatuses[*].name}'to see which container is actually the one restarting.
Catching This Before It Hits Production
Empty logs during a crash loop are usually a visibility gap, not a mystery you need clever tooling to solve every time. If you're running Prometheus with Grafana already, alerting on kube_pod_container_status_last_terminated_reason for OOMKilled specifically will catch memory-limit crashes before they turn into a support ticket, since the metric exposes the exit reason without you needing to manually run describe after the fact.
The underlying lesson here is that CrashLoopBackOff is a status, not a diagnosis. The actual diagnosis lives in the exit code and termination reason, and those are available even when the log stream is completely silent.