Micrometer is the dashboard wiring behind Spring Boot production apps: it turns raw app behavior into numbers you can alert on.
Question: What is Micrometer monitoring in Spring Boot?
Answer: Micrometer is a vendor-neutral metrics library. In Spring Boot, it sits behind Actuator and lets you record things like request counts, error rates, and latency, then export them to systems such as Prometheus, Datadog, or CloudWatch. It works best for the question, “How is the system behaving right now?”
Interview-Ready Answer: I use Micrometer as Spring Boot’s metrics layer. It gives me counters, gauges, and timers, and Spring Boot auto-configures a MeterRegistry so I can publish to backends like Prometheus without hard-coding one vendor. In production, I rely on it for fast signals like latency, error rate, and saturation, and I keep tags low-cardinality so the metrics stay cheap and usable.
Micrometer is a facade (a simple front door over different implementations) for application metrics. Spring Boot wires it into Actuator, so your code records numbers once and Micrometer adapts them to the backend you choose. That means the same business code can publish to Prometheus today and another system later with very little change.
MeterRegistry, which is the object that stores and exports metrics.Counter (only goes up), a Gauge (current value), or a Timer (count + duration).status=200 or method=POST)./actuator/metrics. If you add a backend registry like Prometheus, Boot also exposes /actuator/prometheus.| Meter | What it means | Best use |
|---|---|---|
| Counter | Monotonic total | Requests, errors |
| Gauge | Current value | Queue size, in-flight work |
| Timer | Count + duration | Latency, slow calls |
| DistributionSummary | Value distribution | Payload size, bytes |
One practical rule: use Timer for anything that takes time, because it already gives you both a count and duration percentiles in many backends. Use Counter for events that only increase, and use Gauge when you care about the current value, not the history.
| Tool | Best for | Trade-off |
|---|---|---|
| Metrics | Trends and alerts | Low detail |
| Logs | Exact events | More noise |
| Traces | Request flow | Often sampled |
status, method, region. Bad tags: userId, orderId, requestId.health, metrics, and maybe prometheus.Memory angle: think of Micrometer as a scoreboard, not a movie. It tells you the current score and trend, while logs are the play-by-play and traces are the route map.
Real-World Story: Imagine a checkout service in an e-commerce app. The team uses Micrometer to track request latency, payment failures, and the number of requests currently in flight. A dashboard shows the 95th percentile latency rising from 120 ms to 900 ms after a deployment, and an alert fires before customers fully complain.
The incident that often happens is a bad tag choice: someone adds orderId to a metric “for debugging.” At first the dashboard looks fine, but Prometheus suddenly has thousands or millions of series, scrapes slow down, memory usage rises, and alerts start timing out. Users see slow checkout pages, logs show scrape warnings, and the ops team realizes the monitoring layer became part of the outage.
The lesson is simple: metrics should summarize the system, not uniquely identify every request.
package com.example.micrometer;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
// Add spring-boot-starter-actuator, and add micrometer-registry-prometheus if you want /actuator/prometheus.
@SpringBootApplication
public class MicrometerMonitoringApplication {
public static void main(String[] args) {
SpringApplication.run(MicrometerMonitoringApplication.class, args);
}
@Bean
CheckoutService checkoutService(MeterRegistry registry) {
return new CheckoutService(registry);
}
@Bean
CheckoutController checkoutController(CheckoutService service) {
return new CheckoutController(service);
}
static final class CheckoutService {
private final MeterRegistry registry;
private final Counter successCounter;
private final Counter rejectedCounter;
private final Counter failedCounter;
private final Timer successTimer;
private final Timer rejectedTimer;
private final Timer failedTimer;
private final AtomicInteger inFlight = new AtomicInteger();
CheckoutService(MeterRegistry registry) {
this.registry = registry;
// Counters tell us how many events happened over time.
this.successCounter = Counter.builder("demo.checkout.requests")
.tag("outcome", "success")
.description("Successful checkout requests")
.register(registry);
this.rejectedCounter = Counter.builder("demo.checkout.requests")
.tag("outcome", "rejected")
.description("Rejected checkout requests")
.register(registry);
this.failedCounter = Counter.builder("demo.checkout.requests")
.tag("outcome", "failed")
.description("Failed checkout requests")
.register(registry);
// Timers record latency; the backend can later compute percentiles and trends.
this.successTimer = Timer.builder("demo.checkout.latency")
.tag("outcome", "success")
.description("Checkout latency")
.register(registry);
this.rejectedTimer = Timer.builder("demo.checkout.latency")
.tag("outcome", "rejected")
.description("Checkout latency")
.register(registry);
this.failedTimer = Timer.builder("demo.checkout.latency")
.tag("outcome", "failed")
.description("Checkout latency")
.register(registry);
// A gauge is a current value, so it is perfect for in-flight work.
Gauge.builder("demo.checkout.inflight", inFlight, AtomicInteger::get)
.description("Requests currently being processed")
.register(registry);
}
Receipt placeOrder(long amount) {
inFlight.incrementAndGet();
Timer.Sample sample = Timer.start(registry);
Timer chosenTimer = successTimer;
try {
if (amount <= 0) {
// A bad request should still be counted; otherwise you hide the problem.
rejectedCounter.increment();
chosenTimer = rejectedTimer;
throw new IllegalArgumentException("amount must be positive");
}
if (amount > 5000) {
// Simulate a downstream failure path that should show up in monitoring.
failedCounter.increment();
chosenTimer = failedTimer;
throw new IllegalStateException("payment gateway rejected the charge");
}
// Simulate real work so the timer records visible latency.
Thread.sleep(ThreadLocalRandom.current().nextInt(40, 111));
successCounter.increment();
return new Receipt("ORD-" + System.currentTimeMillis(), amount, Instant.now());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
failedCounter.increment();
chosenTimer = failedTimer;
throw new IllegalStateException("checkout was interrupted", ex);
} finally {
// Stop the timer even when the request fails; that keeps latency visible.
sample.stop(chosenTimer);
inFlight.decrementAndGet();
}
}
int currentInFlight() {
return inFlight.get();
}
}
@RestController
@RequestMapping("/orders")
static final class CheckoutController {
private final CheckoutService service;
CheckoutController(CheckoutService service) {
this.service = service;
}
@PostMapping
ResponseEntity<Receipt> placeOrder(@RequestParam long amount) {
return ResponseEntity.ok(service.placeOrder(amount));
}
@GetMapping("/inflight")
Map<String, Integer> inflight() {
return Map.of("inFlight", service.currentInFlight());
}
@ExceptionHandler(IllegalArgumentException.class)
ResponseEntity<Map<String, String>> badRequest(IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", ex.getMessage()));
}
@ExceptionHandler(IllegalStateException.class)
ResponseEntity<Map<String, String>> badGateway(IllegalStateException ex) {
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
.body(Map.of("error", ex.getMessage()));
}
}
record Receipt(String orderId, long amount, Instant createdAt) {}
}Follow-up & Tricky Questions:
spring-boot-starter-actuator, expose the endpoints you need, and add a backend registry such as Prometheus if you want scrape-based export. Then use /actuator/metrics for inspection and /actuator/prometheus for scraping.MeterRegistry in a test, call the business method, and assert the counter or timer values. That checks the instrumentation without needing a real monitoring server.Common Mistakes:
Memory Hook: Think of Micrometer as the dashboard for your app: counters are the odometer, gauges are the fuel meter, and timers are the stopwatch.
Cheat Sheet:
MeterRegistry.status, method, and region.Practice Tasks:
DistributionSummary for payload sizes.amount=0.orderId, then observe how quickly the number of series grows.