Spring Boot Actuator – Monitoring, Health Checks and Metrics
Spring Boot Actuator is a module of Spring Boot that helps you monitor, manage and observe your application while it runs, especially in production. It adds ready-made endpoints that tell you about the application’s health, metrics, configuration, environment and runtime behavior, without you writing any of that code yourself.
In this guide you will add Actuator to an online shop’s order service, step by step: checking health, writing your own health checks, setting up Kubernetes probes, recording business metrics, exporting them to Prometheus, changing log levels without a restart, building a custom endpoint, and locking everything down safely.
1. What Is Spring Boot Actuator?
Once an application is deployed, you need answers to simple questions: Is it running? Can it reach the database? How many requests per second is it handling, and how fast? Which configuration value is it actually using? Actuator answers these questions through a set of endpoints, small read-only (and sometimes writable) URLs such as /actuator/health and /actuator/metrics.
Think of Actuator as the dashboard of a car. The engine works without it, but the fuel gauge, speedometer and warning lights tell you what is happening inside and warn you before something goes wrong.
| Question | Endpoint that answers it |
|---|---|
| Is the app up, and can it reach its database? | /actuator/health |
| Which version is running? | /actuator/info |
| How fast are the HTTP requests? How much memory is used? | /actuator/metrics |
| Can I turn on debug logging without a restart? | /actuator/loggers |
| Which beans and auto-configurations were created? | /actuator/beans, /actuator/conditions |
| Which URLs does the app handle? | /actuator/mappings |
Actuator is built on Micrometer, a vendor-neutral metrics library, so the same numbers can be sent to Prometheus, Datadog, New Relic, CloudWatch and many other tools.
2. Adding Actuator to Your Project
Add the Actuator starter to your pom.xml. No version is needed when you use the Spring Boot parent or BOM.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
For Gradle, add this line to build.gradle:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
Start the application and open http://localhost:8080/actuator. You get a list of the endpoints that are currently available:
{
"_links": {
"self": { "href": "http://localhost:8080/actuator", "templated": false },
"health": { "href": "http://localhost:8080/actuator/health", "templated": false },
"health-path": { "href": "http://localhost:8080/actuator/health/{*path}", "templated": true }
}
}
Only health appears. That is on purpose: by default, Spring Boot exposes only the health endpoint over HTTP, because the others can reveal internal details. You choose what else to expose in section 4.
3. Built-in Endpoints at a Glance
Here are the endpoints you will use most often. Each one is reached at /actuator/{id}.
| ID | What it shows or does | Sensitive? |
|---|---|---|
health | Whether the app and its dependencies (database, disk, and so on) are working | Low (details can be hidden) |
info | Application name, version, build and Git details | Low |
metrics | Memory, CPU, HTTP request timings, custom business numbers | Medium |
prometheus | All metrics in Prometheus text format (needs an extra dependency) | Medium |
loggers | Shows and changes log levels at runtime | Medium |
env | Properties and their sources (values are masked by default) | High |
configprops | All @ConfigurationProperties beans and their values (masked by default) | High |
beans | Every bean in the application context | Medium |
conditions | Why each auto-configuration did or did not apply | Medium |
mappings | All @RequestMapping paths and their handler methods | Medium |
scheduledtasks | All @Scheduled jobs and their timing | Low |
caches | The caches the application uses; it can also clear them | Medium |
threaddump | A snapshot of every thread, useful for finding hangs | High |
heapdump | Downloads a full memory dump (can contain passwords and personal data) | Very high |
httpexchanges | Recent HTTP requests and responses (needs an HttpExchangeRepository bean) | High |
shutdown | Stops the application (turned off by default) | Very high |
Some endpoints appear only when the related technology is present, for example flyway, liquibase, quartz and sessions.
4. Exposing and Controlling Endpoints
4.1 Choosing which endpoints are reachable
An endpoint must be exposed before you can call it over HTTP. List the IDs you want in application.properties:
# Expose these endpoints over HTTP
management.endpoints.web.exposure.include=health,info,metrics,loggers
# Never expose these, even if "include" matches them
management.endpoints.web.exposure.exclude=env,beans
excludealways wins overinclude.include=*exposes everything. It is handy on your own machine but risky in production.- In YAML, the star must be quoted:
include: "*", because a bare*has a special meaning in YAML.
4.2 Controlling access to endpoints
Exposure decides whether an endpoint is reachable. Access decides whether it exists at all and which operations it allows. In Spring Boot 3.4 and later you can switch every endpoint off by default and turn on only the ones you need:
# Turn every endpoint off unless it is listed below
management.endpoints.access.default=none
# Allow reading only
management.endpoint.health.access=read-only
management.endpoint.info.access=read-only
management.endpoint.metrics.access=read-only
# Allow reading and changing log levels
management.endpoint.loggers.access=unrestricted
| Access value | Meaning |
|---|---|
none | The endpoint is off and cannot be called |
read-only | Only read operations (HTTP GET) are allowed |
unrestricted | Read, write (POST) and delete (DELETE) operations are allowed |
Older versions of Spring Boot use management.endpoints.enabled-by-default=false and management.endpoint.<id>.enabled=true for the same purpose.
4.3 Renaming an endpoint’s path
You can change the URL of a single endpoint without changing its ID:
# /actuator/health becomes /actuator/healthcheck
management.endpoints.web.path-mapping.health=healthcheck
5. The Health Endpoint
The health endpoint is the one you will use most. Load balancers, Kubernetes and monitoring tools call it to decide whether your application can receive traffic.
{
"status": "UP"
}
By default you only see the overall status. Spring Boot automatically adds health checks, called health indicators, for things it finds in your project: a DataSource, Redis, MongoDB, RabbitMQ, Kafka, disk space and more. To see each one, turn on the details:
# never (default) | when-authorized | always
management.endpoint.health.show-details=when-authorized
management.endpoint.health.roles=ADMIN
A user with the ADMIN role now sees a full report:
{
"status": "UP",
"components": {
"db": {
"status": "UP",
"details": { "database": "PostgreSQL", "validationQuery": "isValid()" }
},
"diskSpace": {
"status": "UP",
"details": { "total": 499963174912, "free": 91493904384, "threshold": 10485760, "exists": true }
},
"ping": { "status": "UP" }
}
}
5.1 Health statuses
| Status | Meaning | Default HTTP code |
|---|---|---|
UP | Everything works | 200 |
DOWN | A component is broken | 503 |
OUT_OF_SERVICE | Taken out of use on purpose, for example during maintenance | 503 |
UNKNOWN | The state could not be determined | 200 |
The overall status is the worst status among all components. If the database is DOWN, the whole application reports DOWN.
5.2 Turning off a health check you do not need
Sometimes a library is on the classpath but not really used, and its health check fails. You can switch off a single built-in indicator:
# Do not check the mail server
management.health.mail.enabled=false
6. Writing a Custom Health Check
Our order service calls an external payment gateway. If that gateway is unreachable, we want the health report to say so. Implement HealthIndicator and register it as a bean:
package com.example.shop.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import com.example.shop.payment.PaymentGatewayClient;
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final PaymentGatewayClient client;
public PaymentGatewayHealthIndicator(PaymentGatewayClient client) {
this.client = client;
}
@Override
public Health health() {
long start = System.currentTimeMillis();
try {
boolean reachable = client.ping();
long tookMs = System.currentTimeMillis() - start;
if (reachable) {
return Health.up()
.withDetail("responseTimeMs", tookMs)
.build();
}
return Health.down()
.withDetail("reason", "Gateway did not answer the ping")
.build();
} catch (Exception ex) {
// Health.down(ex) adds the error message to the details
return Health.down(ex).build();
}
}
}
The component name comes from the bean name without the HealthIndicator suffix, so it appears as paymentGateway:
"paymentGateway": {
"status": "UP",
"details": { "responseTimeMs": 42 }
}
Health and HealthIndicator, into new packages. The class names and code stay the same, so let your IDE fix the imports.Tips for custom health checks:
- Keep them fast. Use short timeouts on the client, for example one or two seconds. A slow check makes every health call slow.
- Never throw from
health(). Catch the error and returnHealth.down(ex). - Do not put secrets in details. Details may be shown to more people than you expect.
7. Health Groups and Kubernetes Probes
7.1 Health groups
A health group is a named subset of health checks with its own URL. This lets different tools ask different questions:
# /actuator/health/core checks only the database and disk
management.endpoint.health.group.core.include=db,diskSpace
management.endpoint.health.group.core.show-details=always
7.2 Liveness and readiness probes
Kubernetes asks two different questions about every container:
| Probe | Question | What happens on failure | URL |
|---|---|---|---|
| Liveness | Is the app stuck or broken beyond repair? | Kubernetes restarts the container | /actuator/health/liveness |
| Readiness | Can the app handle requests right now? | Kubernetes stops sending traffic to it until it is ready again | /actuator/health/readiness |
Spring Boot turns these probe groups on automatically when it detects Kubernetes. To use them anywhere else, such as on your own machine, enable them:
management.endpoint.health.probes.enabled=true
# Optional: add the database check to readiness
management.endpoint.health.group.readiness.include=readinessState,db
Then point Kubernetes at them in your deployment file:
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
7.3 Changing readiness from your code
You can tell Kubernetes that the application is temporarily busy, for example while a large cache is being reloaded:
package com.example.shop.catalog;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@Component
public class CacheReloader {
private final ApplicationEventPublisher publisher;
private final ProductCache productCache;
public CacheReloader(ApplicationEventPublisher publisher, ProductCache productCache) {
this.publisher = publisher;
this.productCache = productCache;
}
public void reload() {
// Stop receiving traffic while the cache is empty
AvailabilityChangeEvent.publish(publisher, this, ReadinessState.REFUSING_TRAFFIC);
try {
productCache.reloadAll();
} finally {
// Accept traffic again, even if the reload failed
AvailabilityChangeEvent.publish(publisher, this, ReadinessState.ACCEPTING_TRAFFIC);
}
}
}
8. The Info Endpoint
The info endpoint describes what is running. It is empty until you give it something to show.
8.1 Static information from properties
management.info.env.enabled=true
info.app.name=Order Service
info.app.description=Takes and tracks customer orders
info.app.team=checkout-team@example.com
# Add Java and operating system details
management.info.java.enabled=true
management.info.os.enabled=true
8.2 Build and Git details
Knowing exactly which build is deployed saves a lot of guessing. Ask the Spring Boot Maven plugin to generate build information:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Adds the Git branch and commit to /actuator/info -->
<plugin>
<groupId>io.github.git-commit-id</groupId>
<artifactId>git-commit-id-maven-plugin</artifactId>
</plugin>
{
"app": {
"name": "Order Service",
"description": "Takes and tracks customer orders",
"team": "checkout-team@example.com"
},
"build": {
"artifact": "order-service",
"version": "2.4.1",
"time": "2026-10-01T09:15:42Z"
},
"git": {
"branch": "main",
"commit": { "id": "4f9c2ab", "time": "2026-09-30T17:02:11Z" }
}
}
8.3 Adding your own information
package com.example.shop.info;
import java.util.Map;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;
@Component
public class FeatureFlagsInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("features", Map.of(
"newCheckout", true,
"giftCards", false));
}
}
9. Metrics with Micrometer
9.1 Built-in metrics
As soon as Actuator is added, Spring Boot records many metrics for you: JVM memory and garbage collection, CPU, threads, HTTP requests, database connection pools, caches and more. Expose the metrics endpoint and list the metric names:
# List every metric name
curl http://localhost:8080/actuator/metrics
# Look at one metric
curl http://localhost:8080/actuator/metrics/jvm.memory.used
# Narrow it down with tags: successful calls to /orders only
curl "http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/orders&tag=status:200"
{
"name": "http.server.requests",
"baseUnit": "seconds",
"measurements": [
{ "statistic": "COUNT", "value": 1520 },
{ "statistic": "TOTAL_TIME", "value": 63.84 },
{ "statistic": "MAX", "value": 0.412 }
],
"availableTags": [
{ "tag": "method", "values": ["GET", "POST"] },
{ "tag": "status", "values": ["200", "201", "400"] }
]
}
Here, 1,520 requests took 63.84 seconds in total, an average of about 42 ms each, and the slowest recent one took 412 ms.
9.2 The three main meter types
| Meter | What it measures | Example |
|---|---|---|
| Counter | A number that only goes up | Orders placed, failed payments |
| Gauge | A current value that goes up and down | Items in a queue, active shopping carts |
| Timer | How long something takes, and how often it happens | Checkout duration |
9.3 Recording your own business metrics
Technical metrics are useful, but business metrics often tell you first that something is wrong. Inject MeterRegistry and create your meters once, in the constructor:
package com.example.shop.order;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final OrderRepository repository;
private final MeterRegistry registry;
private final Counter ordersPlaced;
private final Timer checkoutTimer;
private final Map<String, Cart> activeCarts = new ConcurrentHashMap<>();
public OrderService(OrderRepository repository, MeterRegistry registry) {
this.repository = repository;
this.registry = registry;
this.ordersPlaced = Counter.builder("shop.orders.placed")
.description("Number of orders placed")
.register(registry);
this.checkoutTimer = Timer.builder("shop.checkout.duration")
.description("Time taken to complete a checkout")
.register(registry);
// A gauge reads the current size of the map whenever it is scraped
Gauge.builder("shop.carts.active", activeCarts, Map::size)
.description("Shopping carts currently open")
.register(registry);
}
public Order placeOrder(OrderRequest request) {
return checkoutTimer.record(() -> {
Order order = repository.save(Order.from(request));
ordersPlaced.increment();
activeCarts.remove(request.cartId());
return order;
});
}
public void recordFailedPayment(String paymentMethod) {
// A tag splits one metric into a few groups, such as card or wallet
registry.counter("shop.payments.failed", "method", paymentMethod).increment();
}
}
Your metrics now appear at /actuator/metrics/shop.orders.placed, /actuator/metrics/shop.checkout.duration and so on.
method=card has a handful of possible values, which is fine. Never use a user ID, order ID or email address as a tag value: every new value creates a new time series, which can use up memory and overload your monitoring system.9.4 Common tags for every metric
When many services send metrics to one place, add tags that say where each number came from:
management.metrics.tags.application=order-service
management.metrics.tags.environment=production
10. Exporting Metrics to Prometheus and Grafana
Actuator keeps only the latest values in memory. To store history and draw graphs, a monitoring system such as Prometheus collects (“scrapes”) the metrics every few seconds, and Grafana turns them into dashboards.
Step 1: add the Prometheus registry:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Step 2: expose the endpoint, and optionally turn on histograms so Prometheus can calculate response-time percentiles:
management.endpoints.web.exposure.include=health,info,prometheus
management.metrics.distribution.percentiles-histogram.http.server.requests=true
/actuator/prometheus now returns plain text in the format Prometheus understands:
# HELP shop_orders_placed_total Number of orders placed
# TYPE shop_orders_placed_total counter
shop_orders_placed_total{application="order-service",environment="production"} 128.0
Micrometer converts the dots in metric names to underscores and adds _total to counters, following Prometheus naming rules.
Step 3: tell Prometheus where to scrape:
scrape_configs:
- job_name: "order-service"
metrics_path: "/actuator/prometheus"
scrape_interval: 15s
static_configs:
- targets: ["order-service:8080"]
In Grafana, add Prometheus as a data source and build panels such as orders per minute: rate(shop_orders_placed_total[5m]) * 60.
11. Changing Log Levels at Runtime
A bug appears only in production, and the logs are at INFO. Instead of redeploying with more logging, change the level while the application runs.
# Check the current level for a package
curl http://localhost:8080/actuator/loggers/com.example.shop.order
# Switch it to DEBUG
curl -X POST http://localhost:8080/actuator/loggers/com.example.shop.order \
-H "Content-Type: application/json" \
-d '{"configuredLevel": "DEBUG"}'
# Go back to the configured default
curl -X POST http://localhost:8080/actuator/loggers/com.example.shop.order \
-H "Content-Type: application/json" \
-d '{"configuredLevel": null}'
{
"configuredLevel": "DEBUG",
"effectiveLevel": "DEBUG"
}
- The change affects only this running instance. Other instances, and this one after a restart, keep the level from your configuration.
- Remember to switch the level back. Debug logging in production produces a lot of output and can slow the application down.
- The
loggersendpoint needsunrestrictedaccess (the default unless you changed it) to allow thePOST.
12. Creating a Custom Endpoint
When the built-in endpoints are not enough, write your own. This endpoint lets an operator read and change feature flags at /actuator/features:
package com.example.shop.actuator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;
@Component
@Endpoint(id = "features")
public class FeaturesEndpoint {
private final Map<String, Boolean> features = new ConcurrentHashMap<>(
Map.of("newCheckout", true, "giftCards", false));
// GET /actuator/features
@ReadOperation
public Map<String, Boolean> all() {
return features;
}
// GET /actuator/features/{name} (returning null gives a 404)
@ReadOperation
public Boolean one(@Selector String name) {
return features.get(name);
}
// POST /actuator/features/{name} with body {"enabled": true}
@WriteOperation
public void update(@Selector String name, boolean enabled) {
features.put(name, enabled);
}
// DELETE /actuator/features/{name}
@DeleteOperation
public void remove(@Selector String name) {
features.remove(name);
}
}
Like any other endpoint, it must be exposed: management.endpoints.web.exposure.include=health,info,features.
| Annotation | HTTP method | Purpose |
|---|---|---|
@Endpoint(id = "...") | – | Marks the class as an endpoint available over HTTP and JMX |
@ReadOperation | GET | Returns data |
@WriteOperation | POST | Changes data; parameters are read from the JSON body |
@DeleteOperation | DELETE | Removes data |
@Selector | – | Takes a value from the URL path, such as {name} |
Use @WebEndpoint instead of @Endpoint if the endpoint should be available over HTTP only.
13. Securing Actuator Endpoints
Actuator can reveal private information and even change how your application behaves, so treat it like an admin panel. Never leave it open on a public server. Use several layers of protection together.
13.1 Using Spring Security
Add the security starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Then create a security configuration. EndpointRequest matches Actuator URLs even if you change the base path or port later:
package com.example.shop.config;
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.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Health and info are public
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
// Every other Actuator endpoint needs the ADMIN role
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ADMIN")
// The rest of the application needs a logged-in user
.anyRequest().authenticated()
)
// Allow POST to endpoints like /actuator/loggers from tools such as curl
.csrf(csrf -> csrf.ignoringRequestMatchers(EndpointRequest.toAnyEndpoint()))
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
This makes health and info public and limits everything else to users with the ADMIN role. Without the CSRF exception, POST requests from scripts would be rejected with 403 Forbidden.
13.2 Running Actuator on a different port
A very common approach is to put Actuator on a separate internal port that is not reachable from the internet:
management.server.port=9090
# Optional: listen only on the local network interface
management.server.address=127.0.0.1
Your application keeps serving customers on port 8080, while Actuator lives on 9090. Block port 9090 in your firewall or cloud security group, and let only your monitoring system reach it. Setting management.server.port=-1 turns off Actuator’s HTTP endpoints completely.
13.3 Changing the base path
management.endpoints.web.base-path=/manage
Endpoints now live at /manage/health, /manage/info and so on. This is not real security, only a small extra on top of the steps above.
13.4 Hiding sensitive values
The env and configprops endpoints show every value as ****** by default. If you need to see real values, show them only to authorized users:
# never (default) | when-authorized | always
management.endpoint.env.show-values=when-authorized
management.endpoint.configprops.show-values=when-authorized
14. Handy Configuration Cheat Sheet
A ready-to-use starting point for production. Copy it and remove what you do not need.
# Which endpoints are visible over HTTP
management.endpoints.web.exposure.include=health,info,metrics,prometheus,loggers
# Health details and Kubernetes probes
management.endpoint.health.show-details=when-authorized
management.endpoint.health.probes.enabled=true
# Info
management.info.env.enabled=true
management.info.git.mode=full
# Separate port and custom path
management.server.port=9090
management.endpoints.web.base-path=/manage
# Add common tags to all metrics
management.metrics.tags.application=my-shop-service
management.metrics.tags.environment=production
# Show configuration values only to authorized users
management.endpoint.env.show-values=when-authorized
management.endpoint.configprops.show-values=when-authorized
The same settings in YAML:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,loggers
base-path: /manage
endpoint:
health:
show-details: when-authorized
probes:
enabled: true
server:
port: 9090
metrics:
tags:
application: my-shop-service
environment: production
15. Testing Actuator Endpoints
15.1 Testing a health indicator as a plain unit test
A health indicator is an ordinary class, so you can test it without starting Spring:
package com.example.shop.health;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import com.example.shop.payment.PaymentGatewayClient;
class PaymentGatewayHealthIndicatorTest {
private final PaymentGatewayClient client = mock(PaymentGatewayClient.class);
private final PaymentGatewayHealthIndicator indicator = new PaymentGatewayHealthIndicator(client);
@Test
void reportsUpWhenGatewayAnswers() {
when(client.ping()).thenReturn(true);
Health health = indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsKey("responseTimeMs");
}
@Test
void reportsDownWhenGatewayThrows() {
when(client.ping()).thenThrow(new IllegalStateException("Connection timed out"));
Health health = indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
}
}
15.2 Testing endpoint security
Check that the public endpoints stay public and the private ones stay private:
package com.example.shop;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@AutoConfigureMockMvc
class ActuatorSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
void healthIsPublic() throws Exception {
mockMvc.perform(get("/actuator/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"));
}
@Test
void metricsRejectAnonymousUsers() throws Exception {
mockMvc.perform(get("/actuator/metrics"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "ADMIN")
void adminCanReadMetrics() throws Exception {
mockMvc.perform(get("/actuator/metrics"))
.andExpect(status().isOk());
}
}
MockMvc calls the application’s main port, so run these tests without a separate management.server.port, for example by leaving it out of your test configuration. The health test also expects every health check to be UP, so make sure the test database is available. See the Spring Boot Testing guide for more about @SpringBootTest and MockMvc.
16. Best Practices
- Only expose what you need. Avoid
include=*in production. - Secure everything except health and info. Even those can be locked down if needed.
- Use a separate management port and keep it off the public internet.
- Use
when-authorizedfor health details instead ofalways. - Add a common
applicationtag to your metrics so dashboards can tell your services apart. - Keep custom health checks fast. A slow check makes your health endpoint slow, and Kubernetes may think your app is dead.
- Keep external systems out of liveness checks. If your database is down, restarting your app will not fix it. Use readiness for that, if at all.
- Protect heap dumps and thread dumps. They can contain passwords, tokens and personal data.
- Record business metrics, such as orders and failed payments, not only CPU and memory.
- Keep tag values few. Never use IDs or emails as tag values.
- Publish build and Git information so you always know which version is running.
- Set up alerts, not just dashboards. Nobody watches Grafana all day.
17. Common Mistakes and Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
404 on /actuator/metrics | The endpoint is not exposed | Add it to management.endpoints.web.exposure.include |
404 on /actuator/health/liveness | Probes are only on by default in Kubernetes | Set management.endpoint.health.probes.enabled=true |
403 when sending a POST to /actuator/loggers | Spring Security’s CSRF protection blocks it | Ignore CSRF for Actuator requests, as in section 13.1 |
Health is DOWN but the app works | An unused library’s health check fails | Show details to find it, then fix it or set management.health.<name>.enabled=false |
| Pods restart again and again | Liveness probe checks the database, or the start-up delay is too short | Remove external checks from liveness; raise initialDelaySeconds or use a startup probe |
| Health endpoint is very slow | A custom health check has no timeout | Add short timeouts to the client inside the check |
env shows ****** everywhere | Values are masked by default | Set show-values=when-authorized and log in as an authorized user |
| Monitoring system runs out of memory | A tag uses IDs or emails, creating millions of time series | Use tags with a small, fixed set of values |
/actuator is open to the internet | Exposed endpoints with no security | Add Spring Security, a separate port and a firewall rule |
18. Summary
Spring Boot Actuator gives your application a built-in control panel. Add one starter, and you get a health endpoint that load balancers and Kubernetes understand, detailed metrics through Micrometer, runtime log-level changes, and build information that tells you exactly what is deployed. Extend it with your own health checks, business metrics and endpoints, send the metrics to Prometheus and Grafana, and always protect it with careful exposure, Spring Security and a separate management port. Set up this way, Actuator helps you spot problems early and fix them quickly.
Frequently Asked Questions (FAQ)
Q1. Does Spring Boot Actuator slow down my application?
Hardly at all. Most endpoints do nothing until someone calls them, and recording metrics with Micrometer costs only a tiny amount of time per request.
The things that can hurt performance are:
- Slow custom health checks that are called every few seconds by a load balancer.
- Heap dumps, which pause the application and write a very large file.
- Metrics with too many tag values, which use a lot of memory.
Avoid these, and Actuator’s cost is negligible.
Q2. Why does /actuator/health show only {"status":"UP"} without any details?
Because management.endpoint.health.show-details defaults to never. This hides which databases and services you use from anonymous callers.
# Show details only to logged-in users with the ADMIN role
management.endpoint.health.show-details=when-authorized
management.endpoint.health.roles=ADMIN
Use always only on a private network or your own machine.
Q3. What is the difference between exposing an endpoint and controlling its access?
| Setting | Question it answers |
|---|---|
management.endpoints.web.exposure.include | Can the endpoint be reached over HTTP? |
management.endpoint.<id>.access | Does the endpoint exist, and may it only be read or also changed? |
An endpoint is usable over HTTP only when it is both exposed and its access is not none. For example, loggers with read-only access can show log levels but cannot change them.
Q4. My application works, so why does the health endpoint say DOWN?
One of the health checks is failing, often for a feature you do not actually use, for example a mail server or Redis that was added as a dependency but is not running.
- Turn on details (
show-details=when-authorized) and call/actuator/healthas an admin. - Find the component with
"status": "DOWN"and read itsdetails. - Fix the connection, remove the unused dependency, or switch off that check:
management.health.redis.enabled=false
Q5. Which HTTP status codes does the health endpoint return?
UP and UNKNOWN return 200 OK. DOWN and OUT_OF_SERVICE return 503 Service Unavailable. Load balancers usually look only at the status code, which is why this mapping matters.
You can change the mapping if a tool expects something different:
management.endpoint.health.status.http-mapping.out-of-service=200
Q6. What is the difference between liveness and readiness?
- Liveness asks, “Is the application broken beyond repair?” If the answer is no, Kubernetes restarts it.
- Readiness asks, “Can the application handle requests right now?” If not, Kubernetes stops sending traffic to it but leaves it running.
An application that is still loading a large cache is alive but not ready. An application whose threads are all stuck is not alive. Keep liveness simple and based on the application’s own state.
Q7. Can I use Actuator without Spring Security?
Yes. Actuator works on its own. Without Spring Security, however, every exposed endpoint is open to anyone who can reach the port.
If you do not want Spring Security, protect Actuator in other ways:
- Expose only
health, which is the default. - Run Actuator on a separate port (
management.server.port) that only internal systems can reach. - Bind that port to an internal address with
management.server.address.
Q8. How do I show the application version in /actuator/info?
The simplest way is the build-info goal of the Spring Boot Maven plugin. It writes a build-info.properties file into the JAR at build time, and Actuator reads it automatically.
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
For Gradle, add springBoot { buildInfo() } to build.gradle.
Q9. What is Micrometer, and how does it relate to Actuator?
Micrometer is a metrics library that works like a universal adapter. Your code records numbers through one API, and Micrometer sends them to whichever monitoring system you choose, such as Prometheus, Datadog or CloudWatch.
Actuator sets Micrometer up for you, records the built-in metrics such as JVM, HTTP and connection-pool numbers, and shows them at /actuator/metrics. To switch monitoring systems you change a dependency, not your code.
Q10. How do I see response times for just one URL?
Add tag filters to the http.server.requests metric. Use the URL pattern, not the actual URL with IDs:
curl "http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/orders/{id}&tag=method:GET"
Divide TOTAL_TIME by COUNT to get the average. For percentiles such as the 95th, use a monitoring system like Prometheus with histograms turned on.
Q11. Should I use a counter, a gauge or a timer?
| Ask yourself | Use |
|---|---|
| Am I counting events that only add up? | Counter |
| Am I reporting a current amount that rises and falls? | Gauge |
| Do I care how long something takes? | Timer |
A timer also counts how many times the action happened, so you do not need a separate counter for the same action.
Q12. What is metric cardinality, and why is it a problem?
Cardinality is the number of different combinations of tag values a metric has. Each combination is stored as a separate time series.
methodwith valuescard,walletandcash: 3 series. Fine.customerIdwith a million customers: a million series. Memory runs out and dashboards become unusable.
Use tags only for values from a small, fixed list. Put IDs in logs or traces instead.
Q13. Does Actuator work with Spring WebFlux?
Yes. The same endpoints are available in reactive applications. Two things are different:
- Health checks can implement
ReactiveHealthIndicatorand return aMono<Health>to avoid blocking. - Security uses
SecurityWebFilterChainand the reactive version ofEndpointRequest(in the...security.reactivepackage).
Q14. How can I find out why an auto-configuration did not apply?
Call the conditions endpoint. It lists every auto-configuration in two groups, positiveMatches (applied) and negativeMatches (skipped), with the exact reason, for example a missing class or a bean you already defined.
curl http://localhost:8080/actuator/conditions
Starting the application with --debug prints a similar report to the console, which is useful when the application fails before Actuator is available.
Q15. Is it safe to expose the heapdump endpoint?
Not on any network you do not fully trust. A heap dump is a copy of everything in memory, which can include:
- Database passwords and API keys.
- Session tokens of logged-in users.
- Customer names, addresses and other personal data.
Creating it also pauses the application and can produce a file of several gigabytes. Expose it only temporarily, to admins, over an internal port.
Q16. Should I use the shutdown endpoint to stop my application?
Usually not. Platforms such as Kubernetes and systemd stop applications by sending a termination signal, and Spring Boot already handles that cleanly. Make sure graceful shutdown is configured so that requests already in progress can finish:
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
Graceful shutdown is the default from Spring Boot 3.4. The shutdown endpoint is off by default; if you turn it on, protect it carefully, because one POST stops the application.
Q17. Should my health check fail when a service I call is down?
It depends on what you want to happen.
- If the application is useless without that service and other instances may still reach it, failing readiness can make sense.
- If the application can still do useful work, for example show products while payments are down, keep it
UPand show the problem as a detail or metric instead.
Never fail liveness because of another service. That causes restarts that cannot help and can spread one outage across many services.
Q18. How do I see which URLs my application handles?
Use the mappings endpoint. It lists every request path with its HTTP methods, the controller method that handles it, and what it consumes and produces.
curl http://localhost:8080/actuator/mappings
It is a quick way to check why a request returns 404: maybe the path is spelled differently, or the controller was never picked up by component scanning.
Q19. Can Actuator help me trace a request across several microservices?
Yes, together with Micrometer Tracing. Add a tracing bridge (for OpenTelemetry or Brave) and an exporter to a tracing system such as Zipkin, Jaeger or Grafana Tempo. Spring Boot then:
- Gives each request a trace ID and passes it to the next service in HTTP headers.
- Adds the trace ID to your log lines, so you can find every log entry for one request.
- Sends timing data for each step to the tracing system.
Control how many requests are traced with management.tracing.sampling.probability, for example 0.1 for 10%.
Q20. How do I check a health endpoint from a shell script or Docker health check?
Call it and use the HTTP status code: 200 means healthy, 503 means not healthy. With curl, the -f option turns error codes into a non-zero exit code:
curl -fsS http://localhost:8080/actuator/health || exit 1
The same command works in a Dockerfile HEALTHCHECK instruction, as long as curl is installed in the image. If you use a separate management port, call that port instead.