skip to content

When and how do you use Timer.Sample instead of Timer.record(Runnable)?

level: middleimportance: should knowfreq 55%

answer

  1. start here, stop there (decoupled)
  2. Timer.start(registry) -> Sample -> sample.stop(timer)
  3. tags decided at stop time (status/outcome)
  4. same registry Clock start & stop
  5. how http.server.requests is timed

basics

~10 s

Use Timer.Sample when the start and stop of an operation are in different places (e.g. async callbacks) so you can't wrap them in one lambda. Call Timer.start(registry), carry the Sample, then sample.stop(timer) later.

solid answer

~40 s

Timer.record(Runnable/Supplier) works when the timed code is a single synchronous block. Timer.Sample handles the case where the start and end are decoupled — async pipelines, event listeners, filters, callbacks — or where you don't yet know which tags/Timer to attribute the duration to until the operation finishes. You call Timer.start(registry) to capture a start timestamp (it reads the registry's clock), pass that Sample object along, and later call sample.stop(timer) with the fully-resolved Timer (including outcome/status tags decided at the end). stop() computes the elapsed nanos against the same clock and records it. This is exactly how Spring's WebMvc/WebFlux metrics work: they start a sample at the beginning of the request and stop it with tags like status and uri once the response outcome is known.

code

java · 21 lines
java
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import java.util.concurrent.CompletableFuture;

class AsyncOrderClient {
    private final MeterRegistry registry;
    AsyncOrderClient(MeterRegistry registry) { this.registry = registry; }

    CompletableFuture<Order> fetch(long id) {
        Timer.Sample sample = Timer.start(registry);   // start now
        return callRemote(id).whenComplete((order, error) -> {
            // outcome tag is only known here, in the async callback
            String outcome = (error == null) ? "SUCCESS" : "ERROR";
            sample.stop(Timer.builder("orders.fetch")
                .tag("outcome", outcome)
                .register(registry));
        });
    }

    private CompletableFuture<Order> callRemote(long id) { /* ... */ return null; }
}

go deeper

for a junior

Aware that record(lambda) exists; may not know the decoupled start/stop case.

for a middle

Can explain start/stop decoupling, resolving tags at stop, and the registry Clock.

for a senior

Connects it to how Spring times http.server.requests and to async/reactive propagation concerns.

for a principal

Weighs Timer.Sample vs the Observation API (spans + metrics) and context propagation across threads.

## The problem Timer.Sample solves `timer.record(() -> ...)` and `timer.record(duration)` assume you can express the timed operation as one contiguous block, and that you already know **which** `Timer` (with which tags) to record into before you start. That breaks down when: - Start and stop happen in **different methods / threads / callbacks** — e.g. a servlet `Filter` starts timing, and a completion callback stops it. - The **tags depend on the outcome** — you can't decide `status=200` vs `status=500` until the operation finishes, so you can't pick the final Timer up front. - The flow is **asynchronous** (`CompletableFuture`, reactive, message listener) and a lambda can't straddle it. ## The API ```java Timer.Sample sample = Timer.start(registry); // captures start time from registry's Clock // ... work happens, possibly elsewhere, possibly async ... sample.stop( Timer.builder("orders.process") .tag("outcome", outcome) // decided only now .register(registry) ); ``` - **`Timer.start(MeterRegistry)`** returns a `Timer.Sample`. It records the start instant using the **registry's `Clock`** (important: it uses the same monotonic clock the Timer will use to compute elapsed time, so it's not affected by wall-clock adjustments). There's also `Timer.start(Clock)`. - The `Sample` is a lightweight object you carry through your flow (store it on the request, capture it in a lambda, put it in a `Context`). - **`sample.stop(Timer)`** computes `now - start` in nanoseconds and records it into the supplied Timer, returning the elapsed nanos. The Timer is resolved **at stop time**, letting you attach outcome-dependent tags. ## How Spring uses it Spring Boot's own web instrumentation is the canonical example: an interceptor/filter calls `Timer.start(registry)` when the request arrives, stashes the sample, and on completion calls `sample.stop(...)` building the `http.server.requests` Timer with `uri`, `method`, `status`, and `outcome` tags known only at the end. `@Timed` on a controller method is sugar over the same mechanism. ## Gotchas - **You must call stop()** exactly once; forgetting it leaks the timing (no record) — wrap in try/finally when start and stop are in the same method but you also want exception safety and outcome tags. - **Don't reuse a Sample** — one Sample maps to one measurement. - **Thread/async propagation**: the Sample itself is thread-safe to move between threads, but if you rely on Micrometer *observation* context or MDC, propagate that separately. - **Clock consistency**: always start with the same registry (clock) you'll stop against; mixing clocks yields nonsense durations. ## When to use which | Situation | Use | |---|---| | Simple synchronous block, tags known upfront | `timer.record(Runnable/Supplier)` | | Known Duration already computed | `timer.record(duration)` | | Start/stop decoupled, or tags known only at end, or async | `Timer.start` + `sample.stop` | Note: in newer stacks the **Observation API** (`ObservationRegistry`) is often preferred over hand-rolled samples because it also emits tracing spans; but `Timer.Sample` remains the low-level timing primitive.

  • Why does Timer.start take the MeterRegistry rather than just calling System.nanoTime()?
    It reads the registry's Clock so start and stop use the same monotonic time source the Timer records against, keeping durations consistent and testable (you can inject a MockClock).
  • How does this pattern let you set a status tag you don't know at the start?
    The Timer (with its tags) is resolved at sample.stop(timer) time, so you build the final Timer — including outcome/status tags decided only once the operation completes — at the end.

saying these in an interview costs you the question

  • Claiming Timer.Sample stores the duration and you can stop it multiple times
  • Using System.nanoTime() manually and losing testability/clock consistency
  • Thinking you must know all tags before Timer.start (you resolve the Timer at stop)
  • Forgetting to call stop(), so nothing is ever recorded

context