How to wire Kubernetes liveness, readiness, and startup probes to Spring Boot Actuator health endpoints — and avoid the configuration mistakes that cause unnecessary pod restarts.
Running Spring Boot on Kubernetes without correctly configured health probes is one of the most common causes of availability problems I see on microservices deployments. Pods restart unexpectedly under load, traffic gets routed to instances that aren’t ready, slow-starting applications get killed before they finish booting. All of these stem from the same root cause: probes configured incorrectly, or not configured at all.
Spring Boot Actuator has had first-class support for Kubernetes health probes since 2.3. The wiring is straightforward once you understand what each probe does and why the distinction matters.
Liveness answers: “Is this process alive and worth keeping?” If liveness fails, Kubernetes kills the pod and starts a new one. Use this for truly broken states — deadlocks, unrecoverable errors, corrupted internal state. Do not make external dependencies part of liveness — if your database goes down, you don’t want all your pods restarted.
Readiness answers: “Is this instance ready to receive traffic?” If readiness fails, Kubernetes removes the pod from the Service load balancer. Use this for temporary conditions — warming up a cache, a downstream dependency being unavailable. Traffic pauses; the pod is not killed.
Startup answers: “Has the application finished starting up?” While startup hasn’t succeeded, liveness and readiness probes are paused. This is critical for slow-starting JVM applications — without a startup probe, Kubernetes applies liveness probes from the first second and may kill the pod before it’s finished loading.
Add the Actuator dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Enable the Kubernetes health groups in application.yml:
management:
endpoint:
health:
probes:
enabled: true
show-details: always
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
endpoints:
web:
exposure:
include: health, info, metrics
Spring Boot now exposes:
GET /actuator/health/liveness → {"status":"UP"} or {"status":"DOWN"}GET /actuator/health/readiness → {"status":"UP"} or {"status":"READY"} / {"status":"REFUSING_TRAFFIC"}Register a HealthIndicator bean to contribute to the readiness check:
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final BetfairApiClient apiClient;
@Override
public Health health() {
try {
apiClient.ping();
return Health.up().build();
} catch (Exception e) {
return Health.down()
.withDetail("reason", e.getMessage())
.build();
}
}
}
By default, custom HealthIndicator beans contribute to the top-level health and to readiness. If the external API is unavailable, readiness returns DOWN and Kubernetes stops sending traffic — the pod waits until connectivity is restored rather than returning errors to clients.
To control which group an indicator contributes to:
management:
health:
external-api:
enabled: true
endpoint:
health:
group:
readiness:
include: readinessState, externalApi
liveness:
include: livenessState
Keeping liveness minimal (just livenessState) prevents transient external failures from triggering pod restarts.
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: trading-service
image: trading-service:latest
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 0
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 3
timeoutSeconds: 3
The startup probe uses failureThreshold: 30 with periodSeconds: 10 — giving the application up to 5 minutes to start before liveness kicks in. Once startup succeeds, liveness and readiness probes take over with their tighter cadence.
Before startup probes existed, teams used initialDelaySeconds on liveness to prevent killing pods during startup. This is now the wrong approach: it’s a fixed delay that’s either too short (kills slow-starting pods) or too long (delays detection of real failures post-startup). Replace any initialDelaySeconds on liveness with a startup probe.
You can programmatically change the application’s liveness or readiness state:
@Component
@RequiredArgsConstructor
public class MarketDataService {
private final ApplicationContext context;
public void onCriticalFailure() {
// Signal that this pod should be killed and replaced
context.publishEvent(new LivenessStateChangedEvent(
context, LivenessState.BROKEN));
}
public void onMaintenanceMode() {
// Drain traffic without killing the pod
context.publishEvent(new ReadinessStateChangedEvent(
context, ReadinessState.REFUSING_TRAFFIC));
}
}
This is useful for implementing controlled maintenance windows or propagating critical internal failures to the infrastructure layer without hardcoding pod management logic into your business code.
Getting probe configuration right is one of those infrastructure-level investments that pays back in operational stability for the lifetime of the service. It takes thirty minutes to set up correctly and saves hours of incident investigation later.
If you’re deploying Spring Boot services to Kubernetes and want to get the operational configuration right, get in touch.