Skip to content
Spring Boot sb production 3 min read

Metrics & Micrometer

Micrometer is the metrics facade bundled with Spring Boot Actuator — think of it as SLF4J, but for metrics. You instrument your code against a vendor-neutral API, and a registry ships those measurements to whatever monitoring backend you choose (Prometheus, Datadog, CloudWatch, Graphite) by swapping one dependency. Out of the box you already get JVM memory, garbage collection, CPU, HTTP request timings, and connection-pool metrics for free.

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

The metrics endpoint

Expose metrics and Actuator lists every registered meter.

management:
  endpoints:
    web:
      exposure:
        include: health, metrics, prometheus
curl http://localhost:8080/actuator/metrics

Output (truncated):

{
  "names": [
    "jvm.memory.used", "jvm.gc.pause", "system.cpu.usage",
    "http.server.requests", "hikaricp.connections.active",
    "process.uptime", "logback.events"
  ]
}

Drill into one meter by name, optionally filtering by tag:

curl 'http://localhost:8080/actuator/metrics/http.server.requests?tag=status:200'

Output:

{
  "name": "http.server.requests",
  "measurements": [
    { "statistic": "COUNT", "value": 1842 },
    { "statistic": "TOTAL_TIME", "value": 37.21 },
    { "statistic": "MAX", "value": 0.418 }
  ],
  "availableTags": [
    { "tag": "uri", "values": ["/orders/{id}", "/orders"] },
    { "tag": "method", "values": ["GET", "POST"] }
  ]
}

Meter types

A meter is a named measurement. The three you use most:

MeterUse it forDirection
CounterThings that only increase: requests, errors, messages processedmonotonic
GaugeA value that goes up and down: queue depth, active sessions, cache sizesampled
TimerDuration and rate of events: method latency, external call timedistribution

There are also DistributionSummary (sizes, e.g. payload bytes) and LongTaskTimer (in-flight long-running tasks).

Custom metrics with MeterRegistry

Inject the MeterRegistry and register meters. Always attach tags (dimensions) instead of baking values into the metric name.

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final Counter ordersPlaced;
    private final Timer checkoutTimer;

    public OrderService(MeterRegistry registry) {
        this.ordersPlaced = Counter.builder("orders.placed")
                .description("Total orders successfully placed")
                .tag("channel", "web")
                .register(registry);
        this.checkoutTimer = registry.timer("orders.checkout.duration");
    }

    public Order checkout(Cart cart) {
        return checkoutTimer.record(() -> {
            Order order = process(cart);
            ordersPlaced.increment();
            return order;
        });
    }
}

A Gauge tracks live state by holding a reference to the object being measured — register it once, not on every change:

import io.micrometer.core.instrument.Gauge;

Gauge.builder("orders.queue.size", queue, BlockingQueue::size)
     .description("Pending orders waiting to ship")
     .register(registry);

Warning: Never put unbounded values (user IDs, order IDs, raw URLs) in tags. Each unique tag combination is a separate time series — high cardinality explodes memory in both your app and the monitoring backend.

The @Timed annotation

For declarative timing, add spring-boot-starter-aop and annotate a method. A TimedAspect bean activates @Timed.

import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MetricsConfig {
    @Bean
    TimedAspect timedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }
}
import io.micrometer.core.annotation.Timed;

@Timed(value = "report.generation", percentiles = {0.95, 0.99}, histogram = true)
public Report generateMonthlyReport(int month) {
    // ... expensive work timed automatically
}

Note: http.server.requests is already a Timer for every controller method, so you rarely need @Timed on web endpoints. Reach for it on services, schedulers, and batch jobs.

Exporting to Prometheus

Prometheus is the most common backend. Add the registry and a scrape endpoint appears.

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

Expose the prometheus endpoint (already included above). Now /actuator/prometheus serves the text exposition format Prometheus scrapes:

curl http://localhost:8080/actuator/prometheus

Output (excerpt):

# HELP orders_placed_total Total orders successfully placed
# TYPE orders_placed_total counter
orders_placed_total{channel="web"} 1842.0
# HELP orders_checkout_duration_seconds
# TYPE orders_checkout_duration_seconds summary
orders_checkout_duration_seconds_count 1842.0
orders_checkout_duration_seconds_sum 37.2104

Point a Prometheus scrape config at the endpoint:

scrape_configs:
  - job_name: order-service
    metrics_path: /actuator/prometheus
    static_configs:
      - targets: ['order-service:8080']

Add common tags so every metric carries the service identity, which keeps dashboards clean across many instances:

management:
  metrics:
    tags:
      application: order-service
      region: eu-west-1
  endpoint:
    metrics:
      access: read-only

Best Practices

  • Use Counter for totals, Gauge for live values, Timer for durations — don’t mix them up.
  • Keep tag cardinality low; never tag by user/order/request ID.
  • Add management.metrics.tags.application so multi-instance dashboards are filterable.
  • Let http.server.requests cover endpoint latency; reserve @Timed for non-web work.
  • Export with micrometer-registry-prometheus and scrape /actuator/prometheus.
Last updated June 13, 2026
Was this helpful?