How to Write Custom Health Checks in Spring Boot Actuator
I deployed my Spring Boot application to Kubernetes and set up liveness and readiness probes. Everything looked fine until I realized Kubernetes was restarting my pods even when a critical external API was down. The health endpoint returned “UP” because my database connection was healthy, but my application was functionally broken.
The problem? Spring Boot’s built-in health indicators only check infrastructure like databases, Redis, and MongoDB. They don’t know about my application’s specific health requirements.
The Problem
I had this setup in my application.yml:
management: endpoints: web: exposure: include: health endpoint: health: show-details: alwaysWhen I hit /actuator/health, I got:
{ "status": "UP", "components": { "db": {"status": "UP"}, "diskSpace": {"status": "UP"}, "ping": {"status": "UP"} }}But my application depended on an external payment API that was down. The health endpoint said everything was fine, but payment requests were failing. Kubernetes had no way to know the application was degraded.
First Attempt: Check in Controller
I initially tried to check the API health in a controller:
@RestControllerpublic class PaymentController {
@GetMapping("/pay") public ResponseEntity<String> pay() { if (!paymentService.isApiAvailable()) { return ResponseEntity.status(503).body("Payment API unavailable"); } // process payment }}This was wrong. The endpoint returned errors, but Kubernetes liveness/readiness probes check /actuator/health, not my business endpoints. The pods stayed “healthy” and continued receiving traffic they couldn’t handle.
The Solution: Custom Health Indicator
Spring Boot Actuator uses the HealthIndicator interface for health checks. I needed to implement my own to check the payment API.
Basic Implementation
@Componentpublic class PaymentApiHealthIndicator implements HealthIndicator {
private final PaymentService paymentService;
public PaymentApiHealthIndicator(PaymentService paymentService) { this.paymentService = paymentService; }
@Override public Health health() { if (paymentService.isApiAvailable()) { return Health.up().build(); } return Health.down().build(); }}After adding this class, the health endpoint now included my custom check:
{ "status": "DOWN", "components": { "db": {"status": "UP"}, "diskSpace": {"status": "UP"}, "paymentApi": {"status": "DOWN"}, "ping": {"status": "UP"} }}The naming convention is automatic: PaymentApiHealthIndicator becomes “paymentApi” in the health response.
Adding Details for Debugging
The basic version worked, but I wanted more information when checking health:
@Componentpublic class PaymentApiHealthIndicator implements HealthIndicator {
private final PaymentService paymentService;
public PaymentApiHealthIndicator(PaymentService paymentService) { this.paymentService = paymentService; }
@Override public Health health() { try { ApiHealthStatus status = paymentService.checkHealth();
if (status.isHealthy()) { return Health.up() .withDetail("apiEndpoint", status.getEndpoint()) .withDetail("responseTime", status.getResponseTime() + "ms") .withDetail("lastCheck", status.getTimestamp()) .build(); } else { return Health.down() .withDetail("error", status.getErrorMessage()) .withDetail("lastSuccessfulCheck", status.getLastSuccess()) .build(); } } catch (Exception e) { return Health.down() .withDetail("error", "Health check failed: " + e.getMessage()) .withException(e) .build(); } }}Now the health response included useful debugging information:
{ "status": "DOWN", "components": { "paymentApi": { "status": "DOWN", "details": { "error": "Connection timeout after 5000ms", "lastSuccessfulCheck": "2026-03-28T09:30:00Z" } } }}Why This Works
Spring Boot automatically discovers all beans implementing HealthIndicator and includes them in the aggregated health response. The overall status is determined by a status aggregator:
- If any indicator returns DOWN, the overall status is DOWN
- The default order is DOWN < OUT_OF_SERVICE < UP < UNKNOWN
This means Kubernetes can now make informed decisions about pod health based on your application’s actual requirements.
Handling Exceptions Properly
I initially made a mistake. When my health check threw an exception, I expected it to return DOWN. But I saw this in the logs:
Health check failed for paymentApi: java.net.ConnectException: Connection refusedThe health endpoint returned DOWN, but the error handling wasn’t clean. The best practice is to catch exceptions inside the health() method:
@Overridepublic Health health() { // This can throw and cause unclear error messages if (paymentService.isApiAvailable()) { return Health.up().build(); } return Health.down().build();}@Overridepublic Health health() { try { if (paymentService.isApiAvailable()) { return Health.up() .withDetail("status", "available") .build(); } return Health.down() .withDetail("reason", "API unavailable") .build(); } catch (Exception e) { return Health.down() .withDetail("error", e.getMessage()) .withException(e) .build(); }}Configuring for Kubernetes Liveness and Readiness
Spring Boot 2.3+ supports separate liveness and readiness probes. I configured my custom indicator to be part of the readiness group:
management: endpoint: health: show-details: when-authorized probes: enabled: true group: liveness: include: ping readiness: include: db,paymentApiNow Kubernetes can use:
/actuator/health/liveness- Is the app running? (restart if not)/actuator/health/readiness- Can the app serve traffic? (stop sending traffic if not)
When my payment API is down, the readiness probe fails and Kubernetes stops routing traffic to that pod, but doesn’t restart it unnecessarily.
Reactive Health Indicators for WebFlux
When I moved to Spring WebFlux, I needed a different approach. The blocking HealthIndicator doesn’t work well with reactive stacks.
@Componentpublic class ReactivePaymentApiHealthIndicator implements ReactiveHealthIndicator {
private final WebClient webClient;
public ReactivePaymentApiHealthIndicator(WebClient.Builder webClientBuilder) { this.webClient = webClientBuilder .baseUrl("https://api.payment.com") .build(); }
@Override public Mono<Health> health() { return webClient.get() .uri("/health") .retrieve() .bodyToMono(String.class) .map(response -> Health.up() .withDetail("response", response) .build()) .onErrorResume(e -> Mono.just(Health.down() .withDetail("error", e.getMessage()) .build())) .timeout(Duration.ofSeconds(5)); }}The key difference: return Mono<Health> instead of Health, and use reactive operators like map and onErrorResume.
Common Mistakes to Avoid
1. Slow Health Checks
I initially called a slow API in my health check:
@Overridepublic Health health() { // This blocks for 10+ seconds when API is slow boolean available = paymentService.isApiAvailable(); // ...}This caused the entire health endpoint to hang. Kubernetes would timeout and restart pods unnecessarily. I added a timeout:
@Overridepublic Health health() { try { boolean available = paymentService .isApiAvailableWithTimeout(2, TimeUnit.SECONDS); // ... } catch (TimeoutException e) { return Health.down() .withDetail("error", "Health check timeout") .build(); }}2. Exposing Sensitive Information
I initially included API keys in health details:
return Health.up() .withDetail("apiKey", paymentService.getApiKey()) // DON'T DO THIS .build();This is a security risk. Health endpoints are often accessible to monitoring systems. Only include non-sensitive diagnostic information.
3. Using Unknown Status Inappropriately
I tried using Health.unknown() for transient failures:
if (isTransientFailure()) { return Health.unknown().build(); // This doesn't affect overall status}But unknown() status doesn’t affect the aggregated health. Use down() with details explaining the nature of the failure:
if (isTransientFailure()) { return Health.down() .withDetail("type", "transient") .withDetail("retryAfter", "30s") .build();}Testing Custom Health Indicators
I wrote a test to verify my health indicator works correctly:
@SpringBootTestclass PaymentApiHealthIndicatorTest {
@Autowired private HealthIndicator paymentApiHealthIndicator;
@Test void shouldReturnUpWhenApiIsAvailable() { Health health = paymentApiHealthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails()).containsKey("apiEndpoint"); }
@Test void shouldReturnDownWhenApiIsUnavailable( @MockBean PaymentService paymentService) { when(paymentService.isApiAvailable()).thenReturn(false);
Health health = paymentApiHealthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN); }}Summary
To implement custom health checks in Spring Boot Actuator:
- Create a class implementing
HealthIndicator - Annotate with
@Componentfor automatic detection - Return
Health.up()orHealth.down()with optional details - Handle exceptions inside the
health()method - Use
ReactiveHealthIndicatorfor WebFlux applications - Configure health groups for Kubernetes liveness/readiness probes
The naming convention is automatic: XyzHealthIndicator becomes “xyz” in the health response. This lets you monitor what actually matters for your application, not just databases and caches.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments