A health check in Spring Boot is one dependency away. Add spring-boot-starter-actuator, expose the endpoint, and you get GET /actuator/health that returns {"status":"UP"} with a 200 when everything's fine and a 503 when a dependency has failed. Most guides stop there. The harder questions are which checks run by default, how to add your own, how to split liveness from readiness, and what to actually point at that endpoint so a failure reaches you instead of your users.
How do I add a health check to a Spring Boot app?
Two steps: the dependency and one property.
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Gradle:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
Out of the box Actuator exposes only the health endpoint over HTTP, which is exactly what you want here. If your management.endpoints.web.exposure.include has been changed, set it back explicitly in application.properties:
management.endpoints.web.exposure.include=health
Then hit it:
curl -s http://localhost:8080/actuator/health
# {"status":"UP"}
That bare {"status":"UP"} with a 200 is the entire contract an external monitor needs. Everything below is about making the endpoint tell you more, and making sure something is actually watching it.
What does /actuator/health actually check by default?
By default the endpoint hides its internals and reports only an aggregate status. Turn details on to see what it's aggregating:
management.endpoint.health.show-details=always
{
"status": "UP",
"components": {
"db": { "status": "UP", "details": { "database": "PostgreSQL" } },
"diskSpace": { "status": "UP" },
"ping": { "status": "UP" }
}
}
Spring auto-configures a HealthIndicator for most things it detects on the classpath: db (runs a validation query against your DataSource), diskSpace (free space above a threshold), and ping (always UP, a baseline liveness signal), plus technology-specific indicators for Redis, MongoDB, RabbitMQ, Cassandra, Elasticsearch, mail, and others. Any single component reporting DOWN drags the top-level status to DOWN.
The status-to-HTTP mapping is the part that makes this monitorable at all:
UPandUNKNOWNmap to 200DOWNandOUT_OF_SERVICEmap to 503
So a plain HTTP check can treat the endpoint as pass or fail on the status code alone, without parsing the body.
How do I add a custom health check?
Implement HealthIndicator. Spring picks up any bean implementing it and folds it into the aggregate. The bean name becomes the component name with the HealthIndicator suffix dropped, so PaymentGatewayHealthIndicator shows up as paymentGateway:
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final PaymentGatewayClient client;
public PaymentGatewayHealthIndicator(PaymentGatewayClient client) {
this.client = client;
}
@Override
public Health health() {
if (client.isReachable()) {
return Health.up().withDetail("gateway", "reachable").build();
}
return Health.down().withDetail("gateway", "unreachable").build();
}
}
One caution that most examples skip: the indicator runs on every request to the endpoint. If a monitor polls /actuator/health every 30 seconds, your custom check runs every 30 seconds too. Keep the work cheap and give any network call a short timeout. A health indicator that blocks for 30 seconds on a dead dependency just turns your health endpoint into one more thing that hangs.
(On Spring Boot 4 these types moved to the org.springframework.boot.health.contributor package. The interface and the Health builder are otherwise unchanged.)
What's the difference between liveness and readiness in Spring Boot?
Spring models two separate questions, and conflating them is a common way to build a crash loop.
- Liveness: is the process alive, or is it wedged in a state that only a restart fixes?
- Readiness: can it serve traffic right this second, or should it be pulled from rotation for a moment while it warms a cache or waits out a brief downstream outage?
They're exposed as their own endpoints:
curl -s http://localhost:8080/actuator/health/liveness # {"status":"UP"}
curl -s http://localhost:8080/actuator/health/readiness # {"status":"UP"}
On Kubernetes these groups turn on automatically. Anywhere else, enable them explicitly:
management.endpoint.health.probes.enabled=true
The distinction matters because the two failures want opposite responses. A failed readiness check should stop new traffic (drop the instance from the load balancer) and then clear on its own. A failed liveness check means the app can't recover itself and should be killed and restarted. Wire them backwards, restarting on a readiness blip, and a brief downstream hiccup becomes a restart storm.
You can fold your own checks into a group:
management.endpoint.health.group.readiness.include=readinessState,paymentGateway
And you can flip availability from code when you know something the framework doesn't, for example while draining before a shutdown:
AvailabilityChangeEvent.publish(eventPublisher, this, ReadinessState.REFUSING_TRAFFIC);
Should I expose /actuator/health to the public internet?
Expose the status, not the story. show-details=always is fine in development and a quiet information leak in production: it hands anyone who curls the endpoint your dependency map, your internal component names, and the exact piece that's currently broken. The default is never. The usual production middle ground is to gate details behind auth:
management.endpoint.health.show-details=when-authorized
management.endpoint.health.roles=ADMIN
An external uptime monitor doesn't need the details anyway. It needs the 200-versus-503, which the plain endpoint already gives it. A clean setup is the bare /actuator/health (status only) reachable by your monitor, with the detailed view behind authentication for humans debugging an incident.
What should actually be monitoring the endpoint?
Here's the gap in most Spring Boot health-check tutorials: they get you to a working /actuator/health and stop, as if the endpoint monitors itself. It doesn't. Two things have to watch it from outside your process.
- An external HTTP check that hits the URL on a schedule and alerts on a 503 or a timeout. This catches the failure the in-process endpoint physically cannot report on its own: when the whole JVM is down, OOM-killed, or wedged,
/actuator/healthdoesn't return 503. It returns nothing. Only something outside the process notices the silence. - A heartbeat for anything scheduled. If your app runs
@Scheduledjobs or a nightly batch, the health endpoint says nothing about whether last night's run actually happened. That failure mode is silence, not a bad status code, so it needs a dead man's switch rather than an HTTP check.
This is the layer CanaryHub sits in. It polls your /actuator/health from outside, treats a 503 or a timeout as down, and pings you on Telegram, Slack, Discord, or email the moment it flips, plus heartbeat monitoring for the scheduled jobs the endpoint can't see. Point it at the plain status URL and both the "app is down" and the "endpoint stopped answering entirely" cases are covered.
Getting the endpoint right is the first half. Wiring something to watch it is the half that pages you at 3am instead of a customer emailing at 9. If your app also runs work on a schedule, the dead man's switch pattern for cron jobs covers the failures /actuator/health can't see.
