The gap between "it compiles" and "it runs in production"
A Spring Boot app that passes its tests still tells you nothing once it's live. Is it healthy? What version is actually deployed? Why is one instance slow at 3 a.m.? Answering those questions is the job of Spring Boot Actuator — a module that bolts production-ready endpoints onto your application so you can monitor and manage it without writing the plumbing yourself. Health checks, metrics, configuration inspection, runtime log control: it's all there, behind conventions Boot already wired up.
The best part is how little it costs to adopt. One dependency and you have an operable service.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Auto-configuration registers every endpoint and its infrastructure the moment the starter hits the classpath. You don't annotate anything; you only touch properties when you want to change what's exposed, secure it, or move it around.
The endpoints worth knowing
Actuator publishes a family of endpoints under the /actuator base path. A handful do the heavy lifting
in day-to-day operations, and the rest are diagnostics you reach for when something's wrong.
/health overall status + component health (DB, disk, Redis)
/info build, git, and custom app metadata
/metrics Micrometer metrics — JVM, HTTP, datasource, custom
/loggers view and change log levels at runtime
/env Environment properties and their sources
/beans every bean in the context
/mappings all request mappings
/threaddump a JVM thread dump
/heapdump downloads a heap dump file
/httpexchanges the last N HTTP exchanges
/shutdown graceful shutdown (disabled by default)
/health and /metrics are the two you'll wire into monitoring. /metrics is backed by Micrometer, the
vendor-neutral metrics facade that feeds Prometheus, Datadog, CloudWatch, and friends — but that's a topic of
its own. For now, just know /actuator/metrics is your window into JVM memory, request latency, and
connection-pool gauges.
Enabling versus exposing — two different switches
This trips up almost everyone the first time. Actuator has two independent gates. Enabling decides whether an endpoint exists in the application; exposing decides whether an enabled endpoint is reachable over a transport — HTTP or JMX. An endpoint must be both before you can curl it.
Most endpoints are enabled by default but not web-exposed. In fact, out of the box only /health is
reachable over HTTP. That's deliberate: endpoints like /env and /heapdump leak sensitive internals, so Boot
ships secure by default and makes you opt in to anything more.
management:
endpoint:
shutdown:
enabled: true # ENABLE: /shutdown is off by default, switch it on
endpoints:
web:
exposure:
include: health,info,metrics,loggers # EXPOSE these over HTTP
exclude: env # exclude always wins over include
You can use include: "*" to expose everything, and it's tempting in development. In production it's a
liability unless those endpoints sit behind authentication or on a locked-down port. Prefer an explicit
whitelist of the endpoints you actually monitor.
If the default /actuator prefix collides with your app's routes or clashes with a platform convention, rename
it:
management:
endpoints:
web:
base-path: /manage # now /manage/health, /manage/metrics, ...
The /health endpoint and health groups
/health aggregates health indicators — datasource, disk space, message broker — into one overall status:
UP, DOWN, OUT_OF_SERVICE, or UNKNOWN. That status maps to an HTTP code (200 for UP, 503 for DOWN),
which is exactly what a load balancer or orchestrator probe wants. By default the endpoint shows only the
top-level status; the component breakdown is gated by show-details.
management:
endpoint:
health:
show-details: when-authorized # never (default) | when-authorized | always
Keep it at never or when-authorized in production — a public probe has no business seeing which internal
dependency is degraded.
Where health gets genuinely powerful is groups, which bundle a subset of indicators under their own sub-endpoint. The classic use is Kubernetes, where liveness and readiness mean different things. Liveness answers "is the process broken, restart me?"; readiness answers "can I take traffic right now?".
management:
endpoint:
health:
group:
liveness:
include: livenessState # /actuator/health/liveness
readiness:
include: readinessState,db,redis # /actuator/health/readiness
A flaky database should fail readiness — stop routing traffic until it recovers — but it should never fail liveness, because killing and restarting the pod won't fix the database. Getting this distinction right is the difference between a graceful degradation and a restart storm.
Securing what you expose
Here's the sharp edge: web-exposed actuator endpoints are not secured automatically. If Spring Security
isn't on the classpath, an exposed /env, /heapdump, or /shutdown is wide open to anyone who can reach the
port. Think about what that means — /env leaks datasource URLs and secrets, /heapdump hands over a snapshot
of everything in memory, and /shutdown lets a stranger stop your application. Boot sanitizes known secret
keys with ******, but that masking is pattern-based and not something to rely on.
The fix is to treat actuator like an admin console and guard it with Spring Security.
@Bean
SecurityFilterChain actuator(HttpSecurity http) throws Exception {
http.securityMatcher(EndpointRequest.toAnyEndpoint()) // matches /actuator/**
.authorizeHttpRequests(reg -> reg
.requestMatchers(EndpointRequest.to("health")).permitAll() // health stays public
.anyRequest().hasRole("ADMIN")) // everything else: ADMIN
.httpBasic(withDefaults());
return http;
}
Practice defense in depth: expose the minimum set, require authentication and roles, and — the simplest big win — put management traffic on its own port.
A separate management port
Set management.server.port and the actuator endpoints move to a different port from your application
traffic. Now your network layer can block that port from the public internet entirely while the app's main
listener keeps serving users.
server:
port: 8080 # application traffic
management:
server:
port: 8081 # actuator endpoints only
address: 127.0.0.1 # optionally bind to a private interface
Firewalling :8081 is a coarse but extremely effective control — sensitive endpoints simply aren't routable
from outside, no matter what's exposed.
Changing log levels without a redeploy
One endpoint earns its keep during incidents more than any other: /loggers. It lists every logger's level
and, via a POST, lets you change a level on a running instance. When a bug only reproduces in production, you
can raise one package to DEBUG, capture the evidence, and set it back — no redeploy, no restart.
# Raise one package to DEBUG on the live app
curl -X POST localhost:8081/actuator/loggers/com.example.orders \
-H 'Content-Type: application/json' \
-d '{"configuredLevel":"DEBUG"}'
Because it mutates the running application, /loggers is a write endpoint — lock it down like any admin action.
When you need your own endpoint
Sometimes the built-ins don't cover an operational need specific to your app — toggling a feature flag,
evicting a cache, kicking off a reconciliation job. Actuator lets you add a custom endpoint with
@Endpoint and operation annotations, and it slots into the same exposure and security model as the built-ins.
@Component
@Endpoint(id = "features") // GET/POST /actuator/features
public class FeatureFlagsEndpoint {
private final Map<String, Boolean> flags = new ConcurrentHashMap<>();
@ReadOperation // HTTP GET
public Map<String, Boolean> all() { return flags; }
@WriteOperation // HTTP POST { name, enabled }
public void set(String name, boolean enabled) { flags.put(name, enabled); }
}
It still has to be exposed via exposure.include to be reachable, and you'll want to secure any write
operation. Use @WebEndpoint or @JmxEndpoint if you want to limit it to a single transport.
Recap
Actuator is what makes a Spring Boot service operable: add the starter and you get health, metrics, info,
and a toolbox of diagnostics under /actuator. Internalize the two-gate model — enabled means the endpoint
exists, exposed means you can reach it — and expose only the minimum you monitor. Lean on /health with
groups for liveness and readiness, wire /metrics into Micrometer, and use /loggers to debug live without a
redeploy. Above all, respect the security posture: nothing is locked down for you, so authenticate every
endpoint but /health, keep /env, /heapdump, and /shutdown off the public internet, and put management
on its own internal port. Do that, and your service is ready to run — and be run — in production.