What is actually happening
The kubelet starts your container. The container exits — with an error or without one. The kubelet restarts it. If it exits again, it waits before the next attempt, and that wait doubles: 10 seconds, 20, 40, 80, up to a ceiling of five minutes.
That "wait longer each time" is the BackOff in the name. When you see it in `kubectl get pods`, the pod may have gone minutes without attempting anything, so the last log line you see can be from a while ago.
One detail that confuses people: a container that exits successfully also lands in this state. If your command does its job and exits with code 0, Kubernetes restarts it anyway, because a Deployment expects a process that does not finish. That is not an application failure, it is a container designed for the wrong object.
The commands, in order
First the events and the previous state. `kubectl describe pod <name>` shows the Events section at the bottom and, higher up, `Last State` with the exit code and reason. That alone is often enough.
Then, and this is the part most people skip: `kubectl logs <name> --previous`. Without `--previous` you are reading the current container, which probably has not started yet or is already dead. With `--previous` you read the logs of the attempt that failed, which is where the cause is.
If the pod has several containers, add `-c <container>`. And if you need the full sequence, `kubectl get events --sort-by=.lastTimestamp` in the namespace orders everything that happened.
The exit code tells you which family of problem it is
137 means the process received SIGKILL. In the vast majority of cases it is the kernel killing it over memory: the container went past its limit. `kubectl describe` confirms it with `Reason: OOMKilled`. That is a different problem and has its own page.
143 is SIGTERM: something asked it to shut down cleanly. Usually a liveness probe that declared the container unhealthy and triggered the restart, not an application failure as such.
1 or 2 is the application failing on its own: a missing environment variable, a secret that is not mounted, a database that does not answer at startup, an invalid config file. The logs with `--previous` say which.
0 is the container that exited cleanly and should not have exited at all. Check the `command` and the `entrypoint`.
127 is "command not found", almost always a mistyped `command` or a binary that does not exist in that image.
Liveness probes kill more healthy services than they fix
A very common and hard-to-see cause: the application is fine, but it takes longer to start than the liveness probe tolerates. The probe fails, the kubelet kills the container, and the cycle repeats forever.
You recognise it because the logs show no application error and the exit code is 143. `describe` shows `Liveness probe failed` events.
The right fix is not a longer liveness timeout: it is a `startupProbe`, which exists precisely to allow startup time without loosening later supervision. Raising the liveness `initialDelaySeconds` works but leaves the service unsupervised for that entire window.
If it worked before, a change caused it
This is the part most guides leave out. When a service that was stable for weeks enters CrashLoopBackOff, the application did not corrupt itself: something changed. A new image, an edited ConfigMap, a rotated Secret, a memory limit lowered in a commit last week, a renamed variable.
The practical problem is that this information is scattered. The image is in the Deployment, the commit in the repository, the ConfigMap in another object, and the deployment time in your CI history — if retention has not deleted it.
Rebuilding that sequence by hand in the middle of an incident is what stretches time to recovery, the MTTR. Not because the diagnosis is hard, but because gathering the data takes longer than solving it.
How Moonin shortens that part
Moonin keeps a revision history per service: which image, which commit, what changed against the previous revision, and what time it was deployed. When a pod enters CrashLoopBackOff, the question "what changed before this" has an answer on one screen instead of four.
It also records restarts and relates them to the revision that was active, so a service that has been restarting since Tuesday is distinguishable from one that started ten minutes ago.
It installs as one Helm chart per cluster and requires no code instrumentation: service-to-service traces come from eBPF in the node kernel. That agent needs node privileges to read kernel traffic; access to the Kubernetes API is read-only.
What it does not do, so there is no surprise: it does not restart anything, does not roll back deployments and takes no action on its own. It tells you what changed; the rollback is yours.
When a change is not the cause
If the pod enters CrashLoopBackOff on only some nodes, look at the node rather than the deployment: memory pressure, a full disk, a different container runtime version, or a different architecture if you have mixed arm64 and amd64 nodes.
If every service depending on the same database enters the state at once, the problem is in the external dependency and no deployment history will show it.
And if the pod never started at all, check that the state really is CrashLoopBackOff and not ImagePullBackOff or Pending: those are different problems with different causes, and confusing them costs you the first half hour.
See the history of your own cluster
The demo is open and needs no form, running on a real cluster with sample data: you can look at how the revision history and restarts appear before installing anything.
See the revision history in the open demo · How Moonin is billed per Compute Unit