skip to content

Counter & Gauge

Counters only go up and are read as rates; gauges sample a live value through a reference you must keep alive. Interviewers ask why a gauge stopped reporting, and a garbage-collected reference is the classic cause.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

4

What is a Micrometer Counter and when should you use one instead of a Gauge?

level: juniorimportance: must knowfreq 70%

answer

  1. Counter = only goes up
  2. increment() / increment(n>=0)
  3. backends compute rate() from the total
  4. Gauge = up-and-down instantaneous
  5. resets to 0 on restart

basics

~20 s

A Counter records a value that only ever goes up (a total, like requests served). Use a Counter for cumulative counts you increment; use a Gauge for a value that can go up and down, like current queue size.

solid answer

~40 s

A Micrometer Counter is a meter for a single monotonically increasing value — you call increment() as events happen, and it reports the running total. It's ideal for cumulative event counts: HTTP requests served, orders placed, errors thrown. You never set a Counter directly; you only add to it (increment() adds 1, increment(n) adds n where n must be non-negative). Downstream systems (Prometheus, etc.) typically compute a rate from the ever-growing total. Use a Gauge instead when the value can both rise and fall and represents an instantaneous measurement — active connections, queue depth, cache size, memory used. A quick test: 'total number of things that happened' -> Counter; 'current value right now' -> Gauge.

code

java · 21 lines
java
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final Counter ordersPlaced;

    public OrderService(MeterRegistry registry) {
        this.ordersPlaced = Counter.builder("orders.placed")
            .description("Total orders placed since startup")
            .tag("channel", "web")
            .register(registry);
    }

    public void placeOrder(Order order) {
        // ... business logic ...
        ordersPlaced.increment(); // monotonic: only ever +1
    }
}

go deeper

for a junior

Must know Counter = only goes up, use increment(); Gauge = current value that can go down.

for a middle

Should also know the rate() consumption model, non-negative increment contract, and tag cardinality.

for a senior

Explains FunctionCounter, restart-reset handling by backends, and naming/tagging conventions across registries.

for a principal

Frames Counter vs Gauge as a data-model choice affecting aggregation across instances and lossless-between-scrapes semantics.

**Micrometer** is the metrics facade Spring Boot uses under the hood (via `spring-boot-starter-actuator` plus a registry like `micrometer-registry-prometheus`). It defines a small set of *meter* types; the two most fundamental are **Counter** and **Gauge**. **Counter** represents a single, **monotonically increasing** number — it only ever goes up (or resets to zero when the application restarts). You create one from a `MeterRegistry` and record events by calling `increment()`: - `counter.increment()` adds 1. - `counter.increment(amount)` adds `amount`. The amount must be **non-negative** — Micrometer's contract is that a Counter never decreases; passing a negative value is a misuse. Because the stored value is a cumulative total, monitoring systems rarely graph the raw number. Instead they compute a **rate** — e.g. Prometheus `rate(http_requests_total[5m])` gives requests per second. The Counter's job is just to keep an accurate running total; the *derivative* is what humans look at. **When to use a Counter:** anything phrased as 'how many X have happened since the app started' — requests handled, messages consumed, retries attempted, exceptions caught, bytes written. The event is discrete and additive. **Gauge**, by contrast, holds an **instantaneous** value that can go **up or down**: current thread-pool active count, current queue depth, current cache entry count, free disk space. A Gauge is *sampled* — Micrometer reads the current value when the registry scrapes, rather than you pushing increments. **Why not just use a Gauge for everything?** A Gauge you `set()` manually loses information between samples (spikes that happen between scrapes vanish), and it can't be safely summed across instances. A Counter captures every increment, so no events are lost between scrapes and rates are exact. **Creating a Counter — two common styles:** 1. Fluent builder: `Counter.builder("orders.placed").tag("type", "digital").register(registry)`. 2. Convenience: `registry.counter("orders.placed", "type", "digital")`. **Naming & tags:** use dot-separated lowercase names (`orders.placed`) — Micrometer translates to each backend's convention (Prometheus turns it into `orders_placed_total`). Attach **tags** (dimensions) for slicing, but keep tag *values* **bounded** (low cardinality) — never user IDs or raw URLs, or you create an unbounded number of time series. **Edge cases / gotchas:** - A Counter resets to 0 on restart; backends handle this via rate functions that detect counter resets — don't try to persist it yourself. - Don't call `increment(-1)` to 'undo' — that violates the monotonic contract. If you need up-and-down, you want a Gauge. - `FunctionCounter` is a variant that reads a monotonic total from an existing object (e.g. a library's internal counter) instead of you calling increment.

  • Why do monitoring systems graph a rate rather than the raw Counter value?
    The raw value is an ever-growing cumulative total, which isn't meaningful on its own. The rate (derivative over a window) shows current throughput — e.g. requests/second — and rate functions also handle the Counter resetting to zero on restart.
  • What happens if you call increment(-5) on a Counter?
    It violates the Counter's monotonic contract. Micrometer's Counter is meant only to increase; negative amounts are a misuse. If you need a value that decreases, use a Gauge instead.

saying these in an interview costs you the question

  • Thinking a Counter can be set to an arbitrary value
  • Using increment(-1) to decrement a Counter
  • Graphing the raw cumulative total instead of its rate

context

open as a page

How do you register a Gauge that tracks a live object or function, and how is its value read?

level: middleimportance: must knowfreq 60%

basics

~20 s

You bind the Gauge to an object plus a function that reads its current value, e.g. Gauge.builder("queue.size", queue, Queue::size).register(registry). Micrometer calls that function each time the registry is scraped, so the reported value is always current.

open as a page

What are the pitfalls of gauging a collection's size, especially around references and re-registration?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Micrometer keeps only a weak reference to the collection, so if you don't hold a strong reference it gets collected and the Gauge shows NaN. Also, if you replace the collection with a new instance, the Gauge still points at the old one, and re-registering a same-named Gauge is ignored.

open as a page

When would you back a metric with an AtomicInteger/DoubleAdder and gauge it, versus using a Counter or a plain bound Gauge? How do you reason about it at design scale?

level: principalimportance: should knowfreq 25%

basics

~20 s

Use a Counter for cumulative, only-up event totals. Use a bound Gauge when you already have a live object to read a current value from. Back a metric with an AtomicInteger/LongAdder + Gauge when the current value goes up and down but no natural object exposes it, so you maintain the number yourself.

open as a page