Spring Boot Actuator: health and metrics for production
When your Spring Boot application goes live, ops teams need visibility into what's happening under the hood—and they need it fast. Spring Boot Actuator provides built-in endpoints that expose health status, performance metrics, and runtime information without writing custom monitoring infrastructure. We'll walk through how to configure health probes for Kubernetes, export metrics to Prometheus, implement custom health checks for critical services, and lock down the management endpoints to prevent information leakage.
Why actuator matters in production
Before diving into configuration, let's establish the stakes. Production incidents often stem from silent failures—a database connection pool exhausted, a third-party API timing out, a cache server becoming unreachable. Your application might still start successfully and accept requests, but it's operating in a degraded state. Load balancers need to know this. Kubernetes orchestrators need to know this. Monitoring systems need quantifiable data.
Spring Boot Actuator solves this by exposing standardized endpoints that both machines and humans can consume. It's production readiness in one dependency, and it's been battle-tested across thousands of deployments.
Health endpoints: Liveness and readiness probes for Kubernetes and load balancers
Modern containerized deployments rely on health probes. Kubernetes distinguishes between two critical checks: liveness (is the process still running?) and readiness (is it safe to send traffic?). A pod can be alive but not ready if it's warming up or a critical downstream service is temporarily unavailable.
Spring Boot Actuator exposes separate endpoints for each probe. Here's how to enable them:
management:
endpoints:
web:
exposure:
include: health, metrics, prometheus
endpoint:
health:
probes:
enabled: true
show-details: when-authorized
show-components: when-authorized
health:
livenessState:
enabled: true
readinessState:
enabled: true
With this configuration, you get three health endpoints:
/actuator/health— aggregated health status (useful for dashboards)/actuator/health/liveness— returns UP if the JVM is alive, DOWN if it has detected a fatal issue/actuator/health/readiness— returns UP only if all critical components are operational
In your Kubernetes deployment manifest, wire these to probes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
containers:
- name: app
image: my-app:latest
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
Now Kubernetes automatically removes pods that fail the readiness probe from the load balancer, and restarts pods that fail liveness checks. Load balancers (AWS ELB, etc.) use the same pattern.
The distinction matters: a deployment with all pods "alive" but "not ready" scales to zero traffic gracefully instead of crashing. This buys your team time to investigate and recover.
Metrics with Micrometer: JVM, HTTP and datasource metrics exported to Prometheus
Raw health checks tell you if something is broken. Metrics tell you why and help predict failures before they happen. Spring Boot integrates Micrometer, a vendor-neutral metrics facade that collects JVM metrics, HTTP request metrics, database pool stats, and more.
Add the dependency:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Enable the Prometheus endpoint:
management:
endpoints:
web:
exposure:
include: health, metrics, prometheus
metrics:
export:
prometheus:
enabled: true
Visit /actuator/prometheus to see all metrics in Prometheus scrape format. You'll get dozens of metrics automatically:
- JVM memory:
jvm_memory_used_bytes,jvm_memory_max_bytes - HTTP requests:
http_server_requests_seconds(with buckets for latency percentiles) - Database connections:
jdbc_connections_active,jdbc_connections_idle - Threads:
jvm_threads_live,jvm_threads_peak - GC pauses:
jvm_gc_pause_seconds
To ingest these into Prometheus, add a scrape job:
scrape_configs:
- job_name: 'my-app'
static_configs:
- targets: ['localhost:8080']
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
From here, you build Grafana dashboards that alert on memory pressure, connection pool saturation, elevated p99 latency, and GC pause spikes. These dashboards become your early warning system.
Real example: a team noticed HTTP request latency drifting upward over several hours, captured in `http_server_requests_seconds`. They correlated it with rising `jdbc_connections_active` and discovered an N+1 query bug that was exhausting the pool. The metric gave them 6 hours to fix the issue before customers felt the impact.
Custom health checks: Implement HealthIndicator for critical dependencies
The default health indicators cover JVM, database, and message brokers. But what about your payment gateway, SMS provider, or internal API? Kubernetes won't know the pod is broken if those external dependencies fail.
Implement HealthIndicator for anything mission-critical:
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 paymentClient;
public PaymentGatewayHealthIndicator(PaymentGatewayClient paymentClient) {
this.paymentClient = paymentClient;
}
@Override
public Health health() {
try {
PaymentGatewayStatus status = paymentClient.getStatus();
if (status.isAvailable()) {
return Health.up()
.withDetail("provider", "Stripe")
.withDetail("latency_ms", status.getLatencyMs())
.withDetail("rate_limit_remaining", status.getRateLimitRemaining())
.build();
} else {
return Health.degraded()
.withDetail("reason", "Gateway returning errors")
.withDetail("error_rate", status.getErrorRate())
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.withException(e)
.build();
}
}
}
This health indicator now appears in the readiness probe response. If Stripe goes down, your pod becomes "not ready" immediately, Kubernetes removes it from traffic, and your ops team gets an alert.
Notice the Health.degraded() state—useful for situations where the service is partially broken (high error rate, slow responses) but not completely unavailable. You can configure Kubernetes to tolerate degraded state or not, depending on your SLAs.
Add timeout protection so a hung external service doesn't hang your readiness probe:
@Override
public Health health() {
try {
PaymentGatewayStatus status = CompletableFuture
.supplyAsync(() -> paymentClient.getStatus())
.get(2, TimeUnit.SECONDS); // timeout after 2 seconds
// ... rest of logic
} catch (TimeoutException e) {
return Health.down()
.withDetail("error", "Health check timed out")
.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
Lock it down: Expose only what you need and secure the rest
The actuator endpoints contain valuable information: database pool state, configured properties, environment variables, running threads. In the wrong hands, this becomes a reconnaissance vector for attackers.
First, restrict what endpoints are exposed:
management:
endpoints:
web:
exposure:
include: health, metrics, prometheus
exclude: env, configprops, beans, shutdown
endpoint:
health:
show-details: when-authorized # only show details to authenticated users
metrics:
enabled: true
Never expose env, configprops, or shutdown` to the public internet. Health, metrics, and prometheus are safe for public consumption (they don't leak sensitive data).
Second, enforce authentication. Place actuator endpoints behind your security layer:
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize ->
authorize
.requestMatchers(EndpointRequest.to("health", "metrics", "prometheus"))
.permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint())
.hasRole("ADMIN")
.anyRequest()
.permitAll()
);
return http.build();
}
}
With this config, health/metrics/prometheus are public (your Kubernetes probes and Prometheus scraper work without authentication), but anything else requires ADMIN role.
For production, run actuator on a separate management port invisible to the public internet:
management:
server:
port: 9090
endpoints:
web:
base-path: /actuator
Now your Kubernetes cluster, monitoring infra, and ops team hit port 9090 (internal only), while customer traffic hits port 8080. Clean separation.
Common mistakes
- Exposing everything: Setting `include: "*"` is a security anti-pattern. Restrict to health, metrics, and prometheus. Everything else should be authenticated and internal.
- Ignoring timeouts on external health checks: A health check that hangs for 30 seconds defeats its purpose. Always wrap external calls in a timeout (2-5 seconds).
- Not correlating health state with traffic decisions: Readiness probes are only useful if your load balancer actually respects them. Verify Kubernetes config is wired correctly.
- Custom health checks that do too much work: A health check runs every 5-10 seconds. Expensive operations (full DB scans, large API calls) will create a denial-of-service on your own infrastructure. Keep checks light.
- Forgetting to export metrics: Adding the micrometer dependency alone doesn't help if you don't scrape the endpoint. Configure Prometheus and test the scrape job.
Key takeaways
- Health probes (liveness and readiness) are mandatory in Kubernetes deployments. Wire them to
/actuator/health/livenessand/actuator/health/readiness. - Micrometer metrics exported to Prometheus enable early detection of resource exhaustion, latency drift, and error spikes before they become production incidents.
- Implement
HealthIndicatorfor any critical external dependency (payment gateways, auth services, etc.). Add timeout protection to prevent cascading failures. - Restrict actuator endpoint exposure: public for health/metrics/prometheus, authenticated for everything else. Run the management port on an internal network if possible.
- Test your health checks in staging. Verify that Kubernetes actually removes traffic when readiness fails and that your monitoring system scrapes metrics correctly.






Comments
Loading comments…