Why the pod dies: three probes, three questions
Kubernetes has three probes, and they answer different questions. The startup probe asks whether the container has finished starting; while it is failing, the other two are not run at all. The liveness probe asks whether the process is broken beyond recovery, and a failure restarts the container. The readiness probe asks whether this pod should receive traffic now, and a failure only removes it from the Service endpoints.
A Spring Boot service killed during startup almost always has a liveness probe running from the first few seconds. The embedded server is not listening until late in the context refresh, so every liveness check gets a refused connection, the failure threshold is reached, and the kubelet restarts a perfectly healthy JVM. The restart begins the 50 seconds again, which is how a slow start becomes a permanent loop. The general Kubernetes diagnosis of that loop is covered in debugging CrashLoopBackOff; the Spring side is what follows.
Step one: a startup probe and the right Actuator groups
Since Spring Boot 2.3, Actuator models application availability as two states, LivenessState and ReadinessState, and exposes them as health groups at /actuator/health/liveness and /actuator/health/readiness. Through Boot 3.5 these groups are enabled automatically only when Boot detects it is running on Kubernetes, and management.endpoint.health.probes.enabled=true turns them on elsewhere; since Boot 4.0 they are enabled by default everywhere. Liveness becomes correct once the context has refreshed. Readiness only switches to accepting traffic after the application and command-line runners have been called, so a slow warm-up runner keeps the pod out of the load balancer, which is what you want.
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 5
failureThreshold: 24
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
Derive the budget from evidence. Take the slowest start you have seen across real pods, not a laptop, and roughly double it. The point of the startup probe over a larger initialDelaySeconds on liveness is that the delay is paid only once, at startup, and liveness then detects a hung process at its normal speed for the rest of the pod's life.
Do not aim liveness at plain /actuator/health. That endpoint aggregates every health indicator, including the database and any downstream clients, so a database blip fails liveness on every replica at once and Kubernetes restarts the whole fleet into an outage it cannot fix. Dependencies belong in readiness, if anywhere.
Step two: measure where the 50 seconds goes
With the loop broken, you can look at the time instead of guessing. Spring Framework's ApplicationStartup abstraction records steps such as bean instantiation and context phases. Install a BufferingApplicationStartup on the SpringApplication before it runs, and the Actuator startup endpoint returns the recorded steps with their durations.
public static void main(String[] args) {
SpringApplication app = new SpringApplication(OrderServiceApplication.class);
app.setApplicationStartup(new BufferingApplicationStartup(4096));
app.run(args);
}
The usual findings are unglamorous. Database migrations run on every replica as part of startup. A cache is warmed eagerly by loading a large table. A client tries to reach a dependency with a long connect timeout and retries. Hibernate validates a large schema. Component scanning covers far more packages than it needs to. Sort the steps by duration and the first three normally account for most of the time.
Check the pod's CPU allocation as well. JVM startup is CPU-heavy, with class loading and JIT compilation competing for cores, and a CPU limit sized for steady-state traffic throttles exactly that phase. A service that starts in 12 seconds on a developer machine and 50 in the cluster is often being throttled rather than doing more work.
Lazy initialisation makes the number lie
spring.main.lazy-initialization=true is the fix candidates most often reach for, because the startup figure drops immediately. It achieves that by not creating beans until something asks for them. The work has not disappeared; it has moved onto the first requests, which now pay for bean creation, connection pools and proxy generation while a user waits.
Worse, it breaks the readiness signal you just configured. The context refreshes quickly, readiness reports ready, the pod joins the Service, and the first wave of traffic hits beans that do not exist yet. A misconfigured bean that would have failed startup now fails on the first request that touches it, after the old pods have already been terminated. Lazy initialisation has legitimate uses, mainly in development, but as a production answer to probe kills it trades a visible problem for a hidden one.
Why startup time still matters after the kills stop
A startup probe stops the restarts, but the 50 seconds is still your scale-out latency, your recovery time after a node failure and the length of every rolling update multiplied by the replica count. That is the argument for the second step being real work rather than a ticket that never gets picked up: moving migrations into a separate job, making warm-up asynchronous behind readiness, and right-sizing CPU during startup.
Stop the restarts with a startup probe and separate liveness and readiness groups, then measure startup before changing it. Any fix that makes readiness turn green sooner without making the service ready sooner is not a fix.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).