Liveness, Readiness, and Startup Probes in Kubernetes

1. Overview

When running applications on Kubernetes, the cluster doesn’t automatically know whether your pod is “alive” or “ready to receive traffic” — it has to ask through probes. This article explains the differences between liveness, readiness, and startup probes, with examples of Spring Boot Actuator configuration and Deployment YAML. After reading, you’ll be able to configure probes correctly for a real service like OrderService without causing unnecessary pod restart loops.

2. Why probes are necessary

By default, Kubernetes only knows whether the process inside the container is still running (container status Running) or not. It doesn’t know whether the application inside is deadlocked, has lost database connection, or is still starting up.

Consequences without probes:

  • Pod hangs (deadlock) but still receives traffic → requests time out en masse.
  • Pod just started, hasn’t finished connecting to the database, but is already added to the Service’s endpoint list → 500 errors.

Three types of probes solve three different problems:

Probe Answers the question When it fails
Liveness Is the container still alive? Kubernetes restarts the container
Readiness Is the container ready to receive traffic? Kubernetes removes the pod from the Service endpoint (doesn’t restart)
Startup Has the container finished starting up? Kubernetes waits, temporarily disables liveness/readiness

3. Liveness probe

Liveness probe answers: “Is this process in a state where it can’t recover on its own?”. If the probe fails more than failureThreshold times, kubelet kills and restarts the container.

3.1. Adding Spring Boot Actuator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management.endpoint.health.probes.enabled=true
management.endpoints.web.exposure.include=health,info

The line management.endpoint.health.probes.enabled=true enables two sub-endpoints: /actuator/health/liveness and /actuator/health/readiness, which match Kubernetes’s expectations.

3.2. Declaring in a Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  template:
    spec:
      containers:
        - name: order-service
          image: nonladev/order-service:1.0
          ports:
            - containerPort: 8080
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 20
            periodSeconds: 10
            failureThreshold: 3
  • initialDelaySeconds: 20: wait 20s after the container starts before beginning to probe.
  • periodSeconds: 10: call the probe every 10 seconds.
  • failureThreshold: 3: fail 3 times in a row before considering it dead → restart.

Note: Liveness probe should only check “is the application still running”, not check external dependencies (DB, Redis, etc.). If the database dies and liveness fails because of it, Kubernetes will restart the pod repeatedly without solving anything — this is a very common mistake.

4. Readiness probe

Readiness probe answers: “Is the pod ready to handle requests?”. When it fails, the pod is removed from the Service’s endpoints — traffic no longer reaches it, but the container is not restarted.

4.1. Custom readiness indicator

Suppose OrderService needs to connect to PaymentGateway before it’s ready:

@Component
public class PaymentGatewayReadinessIndicator implements HealthIndicator {

    private final PaymentGatewayClient paymentGatewayClient;

    public PaymentGatewayReadinessIndicator(PaymentGatewayClient paymentGatewayClient) {
        this.paymentGatewayClient = paymentGatewayClient;
    }

    @Override
    public Health health() {
        if (paymentGatewayClient.isConnected()) {
            return Health.up().withDetail("gateway", "connected").build();
        }
        return Health.down().withDetail("gateway", "unreachable").build();
    }
}

To add this indicator to the readiness group (without affecting liveness), declare the group:

management.endpoint.health.group.readiness.include=readinessState,paymentGateway
  • health() returns Health.down() when PaymentGatewayClient can’t connect — the readiness probe will fail, and the pod won’t receive traffic until the gateway is ready.
  • Spring automatically names the bean paymentGatewayReadinessIndicator as the key paymentGateway in the health response.

4.2. Testing the indicator

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <scope>test</scope>
</dependency>
class PaymentGatewayReadinessIndicatorTest {

    @Test
    void shouldReportDownWhenGatewayNotConnected() {
        PaymentGatewayClient client = mock(PaymentGatewayClient.class);
        when(client.isConnected()).thenReturn(false);

        PaymentGatewayReadinessIndicator indicator = new PaymentGatewayReadinessIndicator(client);
        Health health = indicator.health();

        assertThat(health.getStatus()).isEqualTo(Status.DOWN);
        assertThat(health.getDetails()).containsEntry("gateway", "unreachable");
    }
}
  • mock(PaymentGatewayClient.class) simulates an external dependency without requiring a real connection.
  • assertThat(health.getStatus()) confirms the status is DOWN when the gateway hasn’t connected — the expected readiness behavior.

4.3. Declaring readiness in a Deployment

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
  failureThreshold: 3
  successThreshold: 1
  • successThreshold: 1: only one successful probe is needed to consider it ready again (unlike liveness, you typically don’t need to set this field since the default is already 1).
  • periodSeconds: 5 is shorter than liveness because readiness needs to respond quickly to changes in dependency status.

5. Startup probe

For slow-starting applications (loading large caches, running database migrations), if you only use liveness with a fixed initialDelaySeconds, you have to guess a large enough number — easy to get wrong and wasteful of wait time on every subsequent restart.

Startup probe solves this: while the startup probe hasn’t passed, Kubernetes temporarily disables liveness and readiness probes, preventing the app from being killed unfairly during legitimate startup.

startupProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  failureThreshold: 30
  periodSeconds: 10
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  periodSeconds: 10
  failureThreshold: 3
  • startupProbe uses failureThreshold: 30 and periodSeconds: 10 → allows the app to start for a maximum of 300s (30 × 10s) before being considered failed.
  • After startupProbe passes for the first time, Kubernetes switches to monitoring livenessProbe with a smaller threshold (3 times × 10s = 30s) — reacting quickly to real issues during stable operation.

Note: If your application starts quickly (in a few seconds, like a simple REST API without loading large caches), you don’t need to declare startupProbe — adding it only makes the YAML longer without clear benefits.

6. Combining all three in a Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: nonladev/order-service:1.0
          ports:
            - containerPort: 8080
          startupProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            failureThreshold: 30
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            periodSeconds: 10
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            periodSeconds: 5
            failureThreshold: 3

Workflow when a new pod is created:

  1. Container starts, Kubernetes immediately begins calling startupProbe (no initialDelaySeconds because startup handles the waiting itself).
  2. During this time livenessProbe and readinessProbe are not yet running.
  3. When startupProbe returns HTTP 200 for the first time, Kubernetes stops the startup probe entirely and begins running liveness and readiness probes in parallel.
  4. Readiness fails → pod is removed from the Service, traffic no longer reaches it, but the container stays alive.
  5. Liveness fails → container is killed, Kubernetes creates a new container (same pod), the process repeats from step 1.

Check actual probe status using kubectl describe pod:

kubectl describe pod order-service-7d9f8c6b5-xk2pq
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True

Events:
  Type     Reason     Age   From     Message
  Warning  Unhealthy  2m    kubelet  Readiness probe failed: HTTP probe failed with statuscode: 503

The Readiness probe failed line indicates readiness is failing — check the application logs or call /actuator/health/readiness directly to see which details are DOWN.

7. Common mistakes

  • Using the same endpoint for liveness and readiness: when an external dependency (DB, cache) dies, both probes fail → pod restarts repeatedly (crash loop) even though the main application is healthy. Clearly separate /actuator/health/liveness (checking only internal state) and /actuator/health/readiness (checking dependencies too).
  • initialDelaySeconds too small for slow-starting apps: liveness probe fails while the app is loading, causing endless restart loops. Solution: use startupProbe instead of arbitrarily increasing initialDelaySeconds.
  • Forgetting periodSeconds × failureThreshold is too large: when a pod actually fails, it takes minutes to be detected, affecting SLA.
  • Readiness probe calling a non-critical downstream service: for example, calling a logging service, if that service goes down then the entire OrderService is wrongly considered not-ready, even though its main functionality works fine.

8. Conclusion

Three types of probes serve three distinct purposes: startupProbe protects the application during slow startup, livenessProbe detects deadlocks so Kubernetes can self-restart, and readinessProbe orchestrates traffic based on actual dependency status. The most important principle: don’t let liveness probe depend on external systems — that turns a downstream issue into your service’s crash loop. Configuring these three probes correctly helps your cluster heal itself smoothly, significantly reducing downtime during deployments or infrastructure issues.

kubernetesspring-bootdevopshealth-checkmicroservices