A pod is crashing continuously, and running kubectl logs <pod-name> returns absolutely nothing. How do you find out why it's dying?
Quick Answer
Empty logs almost always mean the container is crashing before its application ever writes to stdout/stderr — during process startup, a missing entrypoint binary, a failed dependency check, or an OOM kill that happens instantly — so the fix is to inspect kubectl describe pod for exit codes and events, check the previous container's logs with --previous since the current container may have already restarted into a fresh empty log stream, and look specifically at the exit code and reason (OOMKilled, Error, non-zero exit) rather than the log stream itself. If the container has a slow-starting sidecar or init container, check those logs separately, since the main container may never even reach the point of logging.
Detailed Answer
Imagine a security guard who reports 'the night was quiet, nothing to report' every single morning — except one morning, they're immediately reassigned before their shift even starts, replaced by a new guard who also has nothing to report yet. If you only ever ask the current guard, you'll never learn that something happened to the previous one, because the current guard genuinely has nothing to say — they just started. kubectl logs by default only shows the current container instance's log stream, and Kubernetes silently swaps in a brand new container instance on every restart within CrashLoopBackOff, meaning the 'empty logs' you're seeing may belong to a container that's only been alive for half a second.
Kubernetes was designed to treat containers as disposable and restart them automatically under RestartPolicy, prioritizing quick recovery over preserving forensic detail by default — this is why log data isn't retained centrally by the kubelet unless you've configured log aggregation, and why exit code and lifecycle information live in a completely separate data path (the container status the kubelet reports to the API server) from the actual stdout/stderr stream.
Internally, when a container exits, containerd captures its exit code and writes a termination reason to the pod's status.containerStatuses field, which the kubelet syncs back to the API server independent of whatever the application printed. A container can die with genuinely zero log output for several distinct reasons: the entrypoint binary itself is missing or not executable (a CMD/ENTRYPOINT typo or an image built for the wrong architecture), a startup dependency check fails before any logging framework initializes (a database connection string that's malformed enough to crash the process during config parsing), or — most commonly in production — the kernel's OOM killer terminates the process via SIGKILL before the application's own logging has a chance to flush, especially if logs are buffered rather than line-flushed.
At production scale, the actual diagnostic sequence is: kubectl describe pod to read the Last State section, which shows the exact exit code and reason (137 almost always means OOMKilled via SIGKILL, 1 is a generic application error, 127 means 'command not found' i.e. the entrypoint itself is broken); then kubectl logs --previous to fetch the log stream from the container instance before the current one, since that prior instance may have actually run long enough to log something meaningful even if the current one hasn't; and checking init container logs separately with kubectl logs <pod> -c <init-container-name>, since a stuck or failing init container prevents the main container from ever starting, which also presents as 'no logs' but for a completely different reason.
The non-obvious gotcha: a container can be OOMKilled due to a limit far below what monitoring dashboards suggest is 'normal' usage, because a short-lived memory spike during startup (loading a large config file, JIT-compiling, or connection pool warm-up) can exceed the memory limit for a fraction of a second — long enough to trigger the kernel OOM killer — even though steady-state usage looks nowhere near the configured limit on any graph, making the crash look inexplicable until someone checks exit code 137 specifically instead of trusting the averaged memory graphs.