
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: May 20, 2025
In this tutorial, we introduce the Spring Boot Actuator. We’ll cover the basics first, then discuss in detail what’s available in Spring Boot.
We’ll learn how to use, configure, and extend this monitoring tool in Spring Boot and WebFlux, taking advantage of the reactive programming model.
Spring Boot Actuator has been available since April 2014, together with the first Spring Boot release.
Beginning with the release of Spring Boot 2, Actuator has been redesigned, and new exciting endpoints have been added.
We split this guide into three main sections:
In essence, Actuator brings production-ready features to our application.
Monitoring our app, gathering metrics, and understanding traffic or the state of our database becomes trivial with this dependency.
The main benefit of this library is that we can get production-grade tools without actually having to implement these features ourselves.
The actuator mainly exposes operational information about the running application — health, metrics, info, dump, env, etc. It uses HTTP endpoints or JMX beans to enable us to interact with it.
Once this dependency is on the classpath, several endpoints are available for us out of the box. As with most Spring modules, we can easily configure or extend it in many ways.
We need to add the spring-boot-actuator dependency to our package manager to enable the Spring Boot Actuator.
In Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Note that this remains valid regardless of the Boot version, as versions are specified in the Spring Boot Bill of Materials (BOM).
The Spring Boot Actuator keeps its fundamental intent but simplifies its model, extends its capabilities, and incorporates better defaults.
The Actuator retains its core purpose while streamlining its design, expanding its functionality, and enhancing its default settings.
First, this version becomes technology-agnostic. It also simplifies its security model by merging it with the application one.
Among the various changes, it’s important to keep in mind that some of them are breaking. This includes HTTP requests and responses as well as Java APIs.
Lastly, the latest version now supports the CRUD model as opposed to the old read/write model.
The Actuator is now technology-agnostic, whereas previous versions were tied to specific frameworks and APIs. Its model is designed to be pluggable and extensible, achieving flexibility without relying on any particular framework.
Hence, with this new model, we can take advantage of MVC and WebFlux as an underlying web technology.
Moreover, forthcoming technologies could be added by implementing the right adapters.
Finally, JMX remains supported to expose endpoints without any additional code.
Unlike in previous versions, Actuator comes with most endpoints disabled.
Thus, the only two available by default are /health and /info.
If we want to enable all of them, we could set management.endpoints.web.exposure.include=*. Alternatively, we can list endpoints that should be enabled.
Also, Actuator now shares the security config with the regular App security rules, dramatically simplifying the security model.
So, if we are using Spring Security in our project, then we will have to tweak Actuator security rules to allow actuator endpoints.
We could just add an entry for /actuator/**:
@Bean
public SecurityFilterChain securityWebFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated())
.build();
}
We can find further details on the Actuator official docs.
Also, all Actuator endpoints are now placed under the /actuator path by default.
Same as in the previous version, we can tweak this path using the new property management.endpoints.web.base-path.
Let’s examine the available endpoints. Some have been added, others removed, and several have been restructured:
Spring Boot adds a discovery endpoint that returns links to all available actuator endpoints. This will facilitate discovering actuator endpoints and their corresponding URLs.
By default, this discovery endpoint is accessible through the /actuator endpoint.
Therefore, if we send a GET request to this URL, it’ll return the actuator links for the various endpoints:
{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"features-arg0": {
"href": "http://localhost:8080/actuator/features/{arg0}",
"templated": true
},
"features": {
"href": "http://localhost:8080/actuator/features",
"templated": false
},
"beans": {
"href": "http://localhost:8080/actuator/beans",
"templated": false
},
"caches-cache": {
"href": "http://localhost:8080/actuator/caches/{cache}",
"templated": true
},
// truncated
}
As shown above, the /actuator endpoint reports all available actuator endpoints under the _links field.
Moreover, if we configure a custom management base path, then we should use that base path as the discovery URL.
For instance, if we set the management.endpoints.web.base-path to /mgmt, we should request the /mgmt endpoint to see the list of links.
Quite interestingly, when the management base path is set to /, the discovery endpoint is disabled to prevent the possibility of a clash with other mappings.
Just like in the previous version, we can add custom indicators easily. Opposite to other APIs, the abstractions for creating custom health endpoints remain unchanged. However, a new interface, ReactiveHealthIndicator, has been added to implement reactive health checks.
Let’s have a look at a simple custom reactive health check:
@Component
public class DownstreamServiceHealthIndicator implements ReactiveHealthIndicator {
@Override
public Mono<Health> health() {
return checkDownstreamServiceHealth().onErrorResume(
ex -> Mono.just(new Health.Builder().down(ex).build())
);
}
private Mono<Health> checkDownstreamServiceHealth() {
// we could use WebClient to check health reactively
return Mono.just(new Health.Builder().up().build());
}
}
A handy feature of health indicators is that we can aggregate them as part of a hierarchy.
So, following the previous example, we could group all downstream services under a downstream-services category. This category would be healthy as long as every nested service was reachable.
Check out our article on health indicators for a more in-depth look.
Spring Boot now allows health indicators to be organized into groups, enabling consistent configuration to be applied across all members within a group.
For example, we can create a health group named custom by adding this to our application.properties:
management.endpoint.health.group.custom.include=diskSpace,ping
This way, the custom group contains the diskSpace and ping health indicators.
Now, if we call the /actuator/health endpoint, it will tell us about the new health group in the JSON response:
{"status":"UP","groups":["custom"]}
With health groups, we can see the aggregated results of a few health indicators.
In this case, if we send a request to /actuator/health/custom:
{"status":"UP"}
Then, we can configure the group to show more details via application.properties:
management.endpoint.health.group.custom.show-components=always
management.endpoint.health.group.custom.show-details=always
Now, if we send the same request to /actuator/health/custom, we’ll see more details:
{
"status": "UP",
"components": {
"diskSpace": {
"status": "UP",
"details": {
"total": 499963170816,
"free": 91300069376,
"threshold": 10485760
}
},
"ping": {
"status": "UP"
}
}
}
It’s also possible to show these details only for authorized users:
management.endpoint.health.group.custom.show-components=when_authorized
management.endpoint.health.group.custom.show-details=when_authorized
We can also have a custom status mapping.
For instance, instead of an HTTP 200 OK response, it can return a 207 status code:
management.endpoint.health.group.custom.status.http-mapping.up=207
We’re telling Spring Boot to return a 207 HTTP status code if the custom group status is UP.
In Spring Boot, the in-house metrics were replaced with Micrometer support, so we can expect breaking changes. If our application used metric services such as GaugeService or CounterService, they would no longer be available.
Also, Spring Boot provides an autoconfigured MeterRegistry bean, allowing us to interact directly with Micrometer for metrics.
Furthermore, Micrometer is now part of the Actuator’s dependencies, so we should be good to go as long as the Actuator dependency is in the classpath.
Moreover, the response from the /metrics endpoint has been entirely revamped:
{
"names": [
"jvm.gc.pause",
"jvm.buffer.memory.used",
"jvm.memory.used",
"jvm.buffer.count",
// ...
]
}
As we can see, the metrics list no longer includes direct values. To access specific metric values, we can now navigate to the desired metric, such as /actuator/metrics/jvm.gc.pause, to retrieve a detailed response:
{
"name": "jvm.gc.pause",
"measurements": [
{
"statistic": "Count",
"value": 3.0
},
{
"statistic": "TotalTime",
"value": 7.9E7
},
{
"statistic": "Max",
"value": 7.9E7
}
],
"availableTags": [
{
"tag": "cause",
"values": [
"Metadata GC Threshold",
"Allocation Failure"
]
},
{
"tag": "action",
"values": [
"end of minor GC",
"end of major GC"
]
}
]
}
Now, metrics are much more thorough, including different values and some associated metadata.
The /info endpoint remains unchanged. As before, we can add Git details using the respective Maven or Gradle dependency:
<dependency>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
</dependency>
Likewise, we could also include build information, including name, group, and version, using the Maven or Gradle plugin:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
As we pointed out previously, we can create custom endpoints. However, Spring Boot has redesigned how to achieve this to support the new technology-agnostic paradigm.
Let’s create an Actuator endpoint to query, enable, and disable feature flags in our application:
@Component
@Endpoint(id = "features")
public class FeaturesEndpoint {
private Map<String, Feature> features = new ConcurrentHashMap<>();
@ReadOperation
public Map<String, Feature> features() {
return features;
}
@ReadOperation
public Feature feature(@Selector String name) {
return features.get(name);
}
@WriteOperation
public void configureFeature(@Selector String name, Feature feature) {
features.put(name, feature);
}
@DeleteOperation
public void deleteFeature(@Selector String name) {
features.remove(name);
}
public static class Feature {
private Boolean enabled;
// [...] getters and setters
}
}
To get the endpoint, we need a bean. In our example, we’re using @Component for this. Also, we need to decorate this bean with @Endpoint.
The path of our endpoint is determined by the id parameter of @Endpoint. In our case, it’ll route requests to /actuator/features.
Once ready, we can start defining operations using:
When we run the application with the previous endpoint in our application, Spring Boot will register it.
A quick way to verify this is to check the logs:
[...].WebFluxEndpointHandlerMapping: Mapped "{[/actuator/features/{name}],
methods=[GET],
produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}"
[...].WebFluxEndpointHandlerMapping : Mapped "{[/actuator/features],
methods=[GET],
produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}"
[...].WebFluxEndpointHandlerMapping : Mapped "{[/actuator/features/{name}],
methods=[POST],
consumes=[application/vnd.spring-boot.actuator.v2+json || application/json]}"
[...].WebFluxEndpointHandlerMapping : Mapped "{[/actuator/features/{name}],
methods=[DELETE]}"[...]
In the previous logs, we can see how WebFlux is exposing our new endpoint. If we switch to MVC, it’ll simply delegate that technology without having to change any code.
Also, we have a few important considerations to keep in mind with this new approach:
Let’s imagine we want to make sure the production instance of our application is never a SNAPSHOT version.
We decided to do this by changing the HTTP status code of the Actuator /info endpoint that returns this information. If our app happened to be a SNAPSHOT, we would get a different HTTP status code.
We can easily extend the behavior of a predefined endpoint using the @EndpointExtension annotations or its more concrete specializations @EndpointWebExtension or @EndpointJmxExtension:
@Component
@EndpointWebExtension(endpoint = InfoEndpoint.class)
public class InfoWebEndpointExtension {
private InfoEndpoint delegate;
// standard constructor
@ReadOperation
public WebEndpointResponse<Map> info() {
Map<String, Object> info = this.delegate.info();
Integer status = getStatus(info);
return new WebEndpointResponse<>(info, status);
}
private Integer getStatus(Map<String, Object> info) {
// return 5xx if this is a snapshot
return 200;
}
}
In order to access the actuator endpoints using HTTP, we need to both enable and expose them.
By default, all endpoints but /shutdown are enabled. Only the /health and /info endpoints are exposed by default.
We need to add the following configuration to expose all endpoints:
management.endpoints.web.exposure.include=*
To explicitly enable a specific endpoint (such as /shutdown), we use:
management.endpoint.shutdown.enabled=true
To expose all enabled endpoints except one (for example, /loggers), we use:
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.exclude=loggers
The scheduledtasks endpoint provides information about the application’s scheduled tasks, including their type, target, and scheduling information.
Also, Spring Boot Actuator exposes metadata about scheduled tasks, such as the Last Execution Time, the Last Execution Status, and the Next Execution Time.
We can retrieve the scheduled tasks by using a GET request to /actuator/scheduledtasks:
$ curl 'http://localhost:8080/actuator/scheduledtasks' -i -X GET
The resulting response is similar to the following:
{
"cron": [
{
"runnable": {
"target": "com.baeldung.actuator.JobConfig.scheduleTaskUsingCronExpression"
},
"expression": "0 15 10 15 * ?",
"nextExecution": {
"time": "2025-01-15T06:45:00.000024100Z"
}
}
],
"fixedDelay": [
{
"runnable": {
"target": "com.baeldung.actuator.JobConfig.scheduleFixedDelayTask"
},
"initialDelay": 0,
"interval": 1000,
"lastExecution": {
"time": "2024-12-18T15:55:35.273062600Z",
"status": "SUCCESS"
},
"nextExecution": {
"time": "2024-12-18T15:55:36.272024100Z"
}
}
],
"fixedRate": [
{
"runnable": {
"target": "com.baeldung.actuator.JobConfig.scheduleFixedRateTask"
},
"initialDelay": 0,
"interval": 1000,
"lastExecution": {
"time": "2024-12-18T15:55:35.162062700Z",
"status": "SUCCESS"
},
"nextExecution": {
"time": "2024-12-18T15:55:36.160024100Z"
}
}
],
"custom": []
}
As we can see, the /actuator/scheduledtasks endpoint exposes extra information about scheduled tasks.
A Software Bill of Materials (SBOM) describes all the components, libraries, and dependencies used to build a software artifact. A crucial aspect of the SBOM is its role in identifying and managing security vulnerabilities in software applications.
To generate an SBOM for a Spring Boot application, we can use tools like CycloneDX, SPDX, and Syft. Spring Boot version 3.3.0 has built-in support for CycloneDX, making it a convenient choice for generating SBOMs.
Let’s start by importing the cyclonedx-maven-plugin plugin in the <plugins> section of the pom.xml:
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<executions>
<execution>
<phase>generate-resources</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
<configuration>
<projectType>application</projectType>
<outputDirectory>${project.build.outputDirectory}/META-INF/sbom</outputDirectory>
<outputFormat>json</outputFormat>
<outputName>application.cdx</outputName>
</configuration>
</plugin>
Spring Boot automatically manages the version of this plugin through its parent.
Now, we run the mvn package command. During the Maven build, Spring Boot generates an SBOM with the help of the CycloneDX plugin and includes the SBOM in the created JAR file. If we look at the JAR’s contents, we find the SBOM in META-INF/sbom/application.cdx.json.
We can also expose the SBOM via the actuator. For that, we need to enable the SBOM actuator endpoint, which by default isn’t exposed:
management.endpoints.web.exposure.include=sbom
Now, we’re ready to run the Spring Boot application. After the startup is complete, we can query the SBOM actuator endpoint:
curl http://localhost:8080/actuator/sbom
{"ids":["application"]}
This returns a list of all available SBOMs including application, JVM, operating system, and more. By default, there’s only one SBOM named application, describing our Spring Boot application:
curl -i http://localhost:8080/actuator/sbom/application
HTTP/1.1 200
Content-Type: application/vnd.cyclonedx+json
Content-Length: 301209
{
"bomFormat" : "CycloneDX",
"specVersion" : "1.5",
"serialNumber" : "urn:uuid:3842be09-b12e-45ed-8038-babb72a53750",
"version" : 1,
...
Here, we see our application’s content. It contains information about all the dependencies of our application with their hashes and licenses, website issue tracker URLs, and more.
Spring Boot Actuator 3.5.0-M1 introduces SSL metrics for monitoring SSL bundles, enabling proactive certificate management.
It provides two key metrics for SSL certificate monitoring: ssl.chains and ssl.chain.expiry. These metrics allow us to monitor certificate health and set up automated alerts to renew certificates before they expire, ensuring uninterrupted service.
To configure Spring Boot for SSL bundle metrics, we need to enable SSL/TLS and configure certificate bundles in our application.properties or application.yml file:
spring.ssl.bundle.jks.server.keystore.location=classpath:ssl/baeldung.p12
spring.ssl.bundle.jks.server.keystore.password=password
spring.ssl.bundle.jks.server.keystore.type=PKCS12
server.ssl.bundle=server
server.ssl.enabled=false
The metric ssl.chains counts the number of chains and their status (valid, expired, will-expire-soon, not-yet-valid).
We can retrieve ssl.chains by using a GET request to /actuator/metrics/ssl.chains:
$ curl 'https://localhost:8080/actuator/metrics/ssl.chains' -i -X GET
{
"name": "ssl.chains",
"measurements": [
{
"statistic": "VALUE",
"value": 1
}
],
"availableTags": [
{
"tag": "status",
"values": [
"valid",
"not-yet-valid",
"expired",
"will-expire-soon"
]
}
]
}
The measurements field indicates the total number of chains (1 in this case). The status tag lists possible states, allowing filtering by status.
For example, here’s the response on calling /actuator/metrics/ssl.chains?tag=status:valid to count valid chains:
{
"name": "ssl.chains",
"measurements": [
{
"statistic": "VALUE",
"value": 1
}
],
"availableTags": []
}
The metric ssl.chain.expiry tracks the number of seconds until expiry for each certificate chain.
We can retrieve ssl.chain.expiry by using a GET request to /actuator/metrics/ssl.chain.expiry:
$ curl 'https://localhost:8080/actuator/metrics/ssl.chain.expiry' -i -X GET
{
"name": "ssl.chain.expiry",
"description": "SSL chain expiry",
"baseUnit": "seconds",
"measurements": [
{
"statistic": "VALUE",
"value": 234819836
}
],
"availableTags": [
{
"tag": "chain",
"values": [
"baeldung"
]
},
{
"tag": "certificate",
"values": [
"6842f024"
]
},
{
"tag": "bundle",
"values": [
"server"
]
}
]
}
The measurements field shows the time until expiration in seconds (234819836 seconds, roughly 2,717 days). Tags like chain, certificate, and bundle provide context, enabling filtering by specific chains or bundles (e.g., /actuator/metrics/ssl.chain.expiry?tag=chain:baeldung).
In this article, we talked about Spring Boot Actuator. We began by defining what Actuator means and what it does for us.
Next, we focused on Spring Boot Actuator, discussing how to use it, tweak it, and extend it. We also discussed the important security changes we can find in this new iteration. Then, we discussed some popular endpoints and how they have changed as well.
Lastly, we demonstrated how to customize and extend Actuator.