SupportWriter space ↗
← Back to the journal
Spring Boot

Production-ready Spring Boot with Actuator: health checks, metrics, and info

Use Spring Boot Actuator to expose health checks, Kubernetes probes, application info, and Micrometer metrics, and learn how to keep those endpoints secure.

DPutu Adi Guna Permana · 05 Oct 2026 · 6 min read

What production readiness means

Getting an application to run is only half the job. Once it is deployed, operators need answers to questions like: Is it alive? Can it reach the database? How many requests per second is it handling? How long do they take? Which version is running?

Spring Boot Actuator answers these questions with a set of ready-made endpoints. Combined with Micrometer, it also feeds metrics into monitoring systems such as Prometheus, Datadog, or New Relic. The examples target Spring Boot 3.x or later.

Adding Actuator

Add the Spring Boot Actuator dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Start the app and open http://localhost:8080/actuator. You will see a list of links. By default, only the health endpoint is exposed over HTTP. That is a deliberate, safe default: other endpoints can reveal sensitive details.

Choosing which endpoints to expose

Expose endpoints explicitly in application.yml:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true

Commonly used endpoints:

Endpoint Purpose
/actuator/health overall status plus details for each component
/actuator/info build version, Git commit, custom app info
/actuator/metrics list of metrics and their current values
/actuator/prometheus metrics in Prometheus format
/actuator/loggers view and change log levels at runtime
/actuator/env resolved configuration (sensitive)

Avoid include: "*" in production. Endpoints such as env, heapdump, and threaddump are extremely useful while debugging, but they can expose secrets and internal state.

Health checks

/actuator/health aggregates health indicators. Spring Boot registers them automatically for the technologies on your classpath: the database (db), disk space, Redis, RabbitMQ, Kafka, Elasticsearch, mail servers, and more.

{
  "status": "UP",
  "components": {
    "db": { "status": "UP", "details": { "database": "PostgreSQL", "validationQuery": "isValid()" } },
    "diskSpace": { "status": "UP", "details": { "free": 52428800000, "threshold": 10485760 } },
    "ping": { "status": "UP" }
  }
}

With show-details: when-authorized, anonymous callers only see {"status":"UP"}, while authenticated users with the right role see the full breakdown.

Writing a custom health indicator

If your app depends on an external API, report its state too:

@Component
class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final PaymentClient paymentClient;

    PaymentGatewayHealthIndicator(PaymentClient paymentClient) {
        this.paymentClient = paymentClient;
    }

    @Override
    public Health health() {
        try {
            Duration latency = paymentClient.ping();
            return Health.up().withDetail("latencyMs", latency.toMillis()).build();
        } catch (Exception ex) {
            return Health.down().withDetail("error", ex.getClass().getSimpleName()).build();
        }
    }
}

The bean name determines the component name, so this one appears as paymentGateway in the health response. Keep health checks fast and cheap: they may be called every few seconds by a load balancer.

Liveness and readiness probes

Container platforms such as Kubernetes distinguish between two questions:

  • Liveness: is the process healthy, or should it be restarted?
  • Readiness: can it accept traffic right now?

With probes.enabled: true (enabled automatically when the app detects it is running on Kubernetes), Actuator exposes:

/actuator/health/liveness
/actuator/health/readiness

A Kubernetes deployment can then use them:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 30
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080

Do not add external dependencies such as the database to the liveness group. If the database goes down, restarting every application instance will not fix it and may make the outage worse. External dependencies belong in readiness, if anywhere:

management:
  endpoint:
    health:
      group:
        readiness:
          include: readinessState,db

Application info

The info endpoint tells you exactly what is deployed. Enable the build and Git contributors in your build:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>build-info</goal>
            </goals>
        </execution>
    </executions>
</plugin>
<plugin>
    <groupId>io.github.git-commit-id</groupId>
    <artifactId>git-commit-id-maven-plugin</artifactId>
</plugin>

You can also add static values and enable the Java runtime contributor:

management:
  info:
    env:
      enabled: true
    java:
      enabled: true
info:
  app:
    name: Shop API
    team: platform

During an incident, being able to confirm "this pod runs commit a1b2c3d" saves a lot of guesswork.

Metrics with Micrometer

Actuator uses Micrometer, a vendor-neutral metrics facade. Out of the box you get JVM memory and garbage collection, CPU, HTTP request timings (http.server.requests), database connection pool usage, cache statistics, and more.

Inspect one metric:

curl "http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/api/orders&tag=status:200"

Custom business metrics

Technical metrics tell you the app is running. Business metrics tell you it is working. Inject a MeterRegistry and record what matters:

@Service
class CheckoutService {

    private final Counter ordersCreated;
    private final Timer paymentTimer;

    CheckoutService(MeterRegistry registry) {
        this.ordersCreated = Counter.builder("shop.orders.created")
                .description("Number of orders created")
                .register(registry);
        this.paymentTimer = Timer.builder("shop.payment.duration")
                .description("Time spent calling the payment gateway")
                .register(registry);
    }

    Order checkout(Cart cart) {
        Order order = paymentTimer.record(() -> chargeAndCreateOrder(cart));
        ordersCreated.increment();
        return order;
    }
}

Use tags for dimensions such as paymentMethod or region, but keep their values to a small, fixed set. Tagging with user IDs or order IDs creates unbounded time series and can overwhelm your monitoring system.

Exporting to Prometheus

Add the Prometheus registry:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

Prometheus can now scrape /actuator/prometheus, and you can build Grafana dashboards and alerts on top of it, for example alerting when the 95th percentile of http.server.requests exceeds 500 ms.

Securing Actuator

Treat management endpoints as internal tooling. Common strategies:

  1. Separate port. Serve Actuator on a port that is not exposed publicly:

    management:
      server:
        port: 9090
    
  2. Spring Security rules. Allow only health probes publicly and require a role for the rest:

    http.authorizeHttpRequests(auth -> auth
            .requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
            .requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("OPS")
            .anyRequest().authenticated());
    
  3. Network policy. Restrict access at the load balancer or with Kubernetes network policies.

Using more than one layer is a good idea.

Changing log levels at runtime

With the loggers endpoint exposed (and secured), you can turn on debug logging for one package without a restart:

curl -X POST http://localhost:9090/actuator/loggers/com.example.shop.payment \
     -H "Content-Type: application/json" \
     -d '{"configuredLevel": "DEBUG"}'

Remember to switch it back to INFO afterwards.

Exercise

Add a shop.cart.abandoned counter tagged with reason (timeout, payment_failed, user_cancelled), expose metrics on a separate management port, and write a readiness group that includes your custom payment gateway health indicator. Then check the result with curl.

← Explore more notes