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()returnsHealth.down()whenPaymentGatewayClientcan’t connect — the readiness probe will fail, and the pod won’t receive traffic until the gateway is ready.- Spring automatically names the bean
paymentGatewayReadinessIndicatoras the keypaymentGatewayin 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 isDOWNwhen 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: 5is 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
startupProbeusesfailureThreshold: 30andperiodSeconds: 10→ allows the app to start for a maximum of 300s (30 × 10s) before being considered failed.- After
startupProbepasses for the first time, Kubernetes switches to monitoringlivenessProbewith 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:
- Container starts, Kubernetes immediately begins calling
startupProbe(noinitialDelaySecondsbecause startup handles the waiting itself). - During this time
livenessProbeandreadinessProbeare not yet running. - When
startupProbereturns HTTP 200 for the first time, Kubernetes stops the startup probe entirely and begins running liveness and readiness probes in parallel. - Readiness fails → pod is removed from the Service, traffic no longer reaches it, but the container stays alive.
- 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). initialDelaySecondstoo small for slow-starting apps: liveness probe fails while the app is loading, causing endless restart loops. Solution: usestartupProbeinstead of arbitrarily increasinginitialDelaySeconds.- Forgetting
periodSeconds×failureThresholdis 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
OrderServiceis 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.