Hire Me
← All Writing Kubernetes

Health Probes and Spring Boot Actuator — Getting Them Right

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.

The Three Probes

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.

Enabling Actuator Health Groups

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:

Custom Health Indicators

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.

Kubernetes Probe Configuration

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.

The initialDelaySeconds Trap

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.

Programmatic State Changes

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.