Metrics reference

The full catalogue of Prometheus meters StoatFlow exposes on /metrics — name, type, tags, and what each one measures, grouped by area.

The :runtime module exports metrics on GET /metrics in Prometheus text format, backed by a Micrometer PrometheusMeterRegistry. This page is the meter catalogue: every StoatFlow meter the current release registers, grouped by area, with its type, tags, and a one-line description. For how to enable, scrape, and configure metrics — recording levels, naming rewrites, PromQL examples — see Metrics. For what to alert on, see Observability.

Contract. This catalogue lists the stable meter set. The live /metrics scrape is authoritative for the exact tag sets and any point-release additions — when in doubt, trust the scrape.

How to read this page

  • Meter — the canonical dotted metric ID. Prometheus rewrites . to _ and appends the type suffix (_total for counters; _seconds_count / _seconds_sum / _seconds_max for timers), so stoatflow.barrier.commit.latency is scraped as stoatflow_barrier_commit_latency_seconds_*.
  • Tags — per-meter tags beyond the common ones. Every StoatFlow meter carries application_id; the registry-level application tag plus anything under runtime.metrics.common-tags is applied to every meter on the endpoint, including JVM and Kafka-client meters.
  • Recording level — meters marked (debug) in the description only appear with runtime.metrics.recording-level: debug; everything else is exported at the default info level.
  • The commit-path latency timers publish client-side P50/P95/P99 quantiles as {quantile="…"} series on the base _seconds name — there are no _bucket histograms.

Application and engine

MeterTypeTagsWhat it measures
stoatflow.client.stateGauge—Application state ordinal: CREATED=0, STARTING=1, VALIDATING_STATE=2, RESTORING=3, RUNNING=4, DRAINING=5, PAUSED=6, STOPPING=7, STOPPED=8, ERROR=9.
stoatflow.client.lane.aliveGauge—Currently alive/healthy lanes.
stoatflow.client.uptime.secondsGauge—Seconds since the application started.
stoatflow.client.start.time.epoch.secondsGauge—Application start time (epoch seconds).
stoatflow.lanesGaugesub_topology, chainConfigured key-affinity lanes per sub-topology; sum(stoatflow_lanes) gives the cross-topology total.
stoatflow.engine.restart.totalCounterdisposition, target, triggerSuccessful in-place engine restarts (fault recovery, programmatic, HA demote, poison replay, source-topic discovery under HA, and trigger=source_topic_deleted when a pattern-matched source topic is deleted without HA). Topic-set triggers do not consume the restart budget.
stoatflow.topology.statelessGauge—1 if the topology has no state stores, 0 otherwise.
stoatflow.topology.event.time.trackingGauge—1 if watermark tracking is active, 0 if bypassed.
stoatflow.topology.dispatch.modeGauge—The dispatch mode resolved at start-up: 0 for LANE, 1 for DIRECT (AUTO resolves to one of the two).

Lanes and throughput

MeterTypeTagsWhat it measures
stoatflow.lane.queue.sizeGaugelane_id, chainCurrent lane queue depth (backpressure signal).
stoatflow.lane.queue.capacityGaugesub_topology, chainConfigured lane queue capacity per sub-topology.
stoatflow.lane.queue.full.totalCounterlane_idTimes a lane queue was full (backpressure events).
stoatflow.lane.records.processed.totalCounterlane_idRecords processed by a lane.
stoatflow.lane.bytes.processed.totalCounterlane_idBytes processed by a lane.
stoatflow.lane.process.latencyTimerlane_idPer-record processing latency.
stoatflow.e2e.latencyTimer—Per-record end-to-end latency, record timestamp to processing completion (KS record-e2e-latency analogue).

Lane IDs follow the {subtopology}_{lane} convention — e.g. 0_0, 1_7. Aggregate with a regex matcher: sum(stoatflow_lane_queue_size{lane_id=~"1_.*"}).

Commit barriers and transactions

MeterTypeTagsWhat it measures
stoatflow.barrier.initiated.totalCounter—Barriers initiated.
stoatflow.barrier.completed.totalCounter—Barriers successfully completed.
stoatflow.barrier.failed.totalCountererror_typeBarrier failures, by error type: timeout, bounded_await_timeout, commit_stall, commit_failed, producer_fenced, tx_error, state_flush_failed (a previous epoch's state flush failed), poison_epoch, poison_halt, abandoned_epoch_refused (refused after an earlier epoch on the engine was aborted), abandonment_invariant, dispatch_failed (the dispatcher failed between registering a barrier and broadcasting it), fatal_error (an Error such as TODO() or AssertionError thrown inside the commit — typically user code: a serde, a custom store, a consumer interceptor; a JVM-fatal error such as OutOfMemoryError is not counted — it ends the engine), shutdown (queued barriers drained by a failure shutdown), unknown.
stoatflow.barrier.abandoned.totalCounterreasonBarriers deliberately not committed, by reason: pre_assignment (shutdown before the first partition assignment), poison_abort (the epoch was aborted around a poison record), abort_teardown (an ABORT engine teardown — a fault-triggered in-place restart — was under way, or a graceful shutdown ran out of time for a barrier whose output it could no longer send) or not_broadcast (the engine stopped before the barrier left the dispatcher). Not a failure — the epoch replays on restart.
stoatflow.epoch.invariant.violations.totalCounterkindEpoch-isolation tripwires that fired, by kind: rotation_over_pending (a state store started a new epoch while the previous one was still uncommitted), skipped_commit_with_pending (a commit was skipped while state stores still held its epoch's writes), lane_epoch_lag (a lane reported a barrier after a newer one was already dispatched), holdback_epoch_gap (a lane held records from two different epochs ahead of it at once), offsets_send_refused (a second barrier's offsets were about to share an open transaction with another barrier's — the send is refused, or the commit fails and replays its epoch), partition_offset_regression (a partition dispatched a record at a lower offset than one it had already dispatched — both processing guarantees. A broker-side seek-back — log truncation after an unclean leader election, an offset reset — fires it too, once per seek-back, and is not a StoatFlow bug: offset tracking follows the partition back, so the re-fetched records are processed again rather than skipped by the next commit. Logged at WARN, at most once a minute per partition), partition_buffer_over_bound (a poll delivered records to a partition whose input buffer was already full — the pause that bounds the buffer did not take effect. The records are not buffered: they are fetched again once the buffer has room, so memory stays bounded at buffered-records-per-partition − 1 + max.poll.records. Logged at WARN once until the buffer drains; an earlier WARN about a failed consumer pause usually names the cause), and five timer kinds — timer_claim_without_slot (a due timer reached its lane's onTimer without having been handed out by the timer store; it fires anyway), timer_claim_while_firing (a timer's fire arrived while the same timer was already firing; the second is skipped), timer_registry_not_empty_at_reload (a standby promoted to active found timers in flight, which a standby never has), timer_event_without_handler (a timer's fire reached a lane with no timer callback; with a persistent timer backend it fires again after a restart, a Heap timer is lost), timer_claim_behind_queue_epoch (a timer's fire was about to run in an older commit than writes made before the timer became due; it is skipped and the timer fires again once its lane has caught up). Every other kind is unreachable by design, so any non-zero value there is a bug — each is also logged at ERROR; please report it. Recorded at every recording level.
stoatflow.barrier.in.flightGauge—Currently pending barriers.
stoatflow.barrier.latencyTimer—Barrier initiation to completion.
stoatflow.barrier.alignment.latencyTimer—Time for all lanes to receive the barrier.
stoatflow.lane.holdback.recordsDistributionSummarysub_topologyRecords and timer fires held back and replayed per barrier at a sub-topology boundary — receiver-side epoch hold-back that keeps the commit a consistent cut across the cascade (ADR-135; timer fires since ADR-147's 2026-10-13 amendment).
stoatflow.lane.holdback.drains.totalCountersub_topologyNon-empty hold-back drains — a receiving lane replayed stashed ahead-of-epoch records or timer fires after crossing its barrier (ADR-135).
stoatflow.barrier.commit.latencyTimer—Commit latency: producer drain + Kafka TX + state flush (KS commit-latency analogue).
stoatflow.barrier.records.committed.totalCounter—Records committed across barriers.
stoatflow.barrier.bytes.committed.totalCounter—Bytes committed across barriers.
stoatflow.barrier.subtopology.received.totalCountersubtopology_idBarriers received per sub-topology. (debug)
stoatflow.barrier.subtopology.pending.lanesGaugesubtopology_idPending lane completions per sub-topology. (debug)
stoatflow.barrier.interval.target.msGauge—Current target barrier-to-barrier interval (ms).
stoatflow.barrier.commit.duration.estimate.msGauge—Smoothed estimate of commit duration (ms).
stoatflow.barrier.epoch.max.recordsGauge—Current dynamic epoch record limit.
stoatflow.barrier.epoch.actual.recordsGauge—Records dispatched in the last epoch.
stoatflow.barrier.epoch.memory.capGauge—Memory-derived epoch record cap; -1 = uncapped.
stoatflow.barrier.epoch.bytes.per.recordGauge—Smoothed uncommitted state bytes per record (feeds the memory cap).
stoatflow.s1.epoch.records.smoothedGauge—Smoothed epoch-records average used by the epoch-size controller.
stoatflow.s1.cap.grow.events.totalCounterreasonEpoch-cap grow events by binding signal (record_count, memory_pressure, stall, soft_binding, time_grow).
stoatflow.s1.cap.decay.events.totalCounter—Epoch-cap decay events.
stoatflow.barrier.trigger.totalCountertriggerBarrier creation count by trigger: TIME, RECORD_COUNT, MEMORY_PRESSURE, CACHE_PRESSURE.
stoatflow.dispatcher.stall.latencyTimer—Dispatcher stall time waiting for an active commit after hitting the epoch record limit.
stoatflow.barrier.commit.changelog.latencyTimer—Aggregate changelog serialization+send latency per commit epoch.
stoatflow.barrier.commit.state.flush.latencyTimer—Aggregate state-store flush latency per commit epoch.
stoatflow.barrier.commit.producer.flush.latencyTimer—Pre-commit producer flush latency.
stoatflow.barrier.commit.send.offsets.latencyTimer—sendOffsetsToTransaction RPC latency.
stoatflow.barrier.commit.queue.wait.latencyTimer—Barrier wait in the commit queue between alignment and commit-thread pickup.
stoatflow.barrier.commit.post.tx.latencyTimer—Post-transaction phase latency (guard wait + epoch prepare + send-buffer drain).
stoatflow.barrier.commit.window.durationTimer—Duration of the commit-in-progress blocking window. (debug)
stoatflow.commit.state.flush.guard.waitTimer—Time the next transaction waited on the previous epoch's async state flush.
stoatflow.commit.state.flush.async.latencyTimer—Async (off-commit-path) state flush latency.
stoatflow.commit.state.flush.overlapCounter—Commits that overlapped a still-running previous flush.
stoatflow.commit.stall.detected.totalCounter—Commit-pipeline stalls detected by the in-process watchdog.
stoatflow.commit.stall.duration.msGauge—Age of the most recently observed commit stall (ms).
stoatflow.transaction.begin.totalCounter—Kafka transactions begun.
stoatflow.transaction.commit.totalCounter—Kafka transactions committed.
stoatflow.transaction.abort.totalCounter—Kafka transactions aborted.
stoatflow.transaction.commit.latencyTimer—Kafka transaction commit latency.
stoatflow.alo.commit.delegation.waitTimer—Commit delegation wait (at-least-once mode only).
stoatflow.alo.commit.sync.latencyTimer—commitSync latency (at-least-once mode only).
stoatflow.commit.barrier.idle.throttledGauge—1 while the barrier scheduler is parked on the stretched warm-idle cadence, else 0 (ADR-134). Expected on an idle exactly-once application, and — since reclamation became idle-safe — on an idle at-least-once one that still has a versioned store left to sweep.

State stores and caching

Most store meters are per-store (store_name) and require recording-level: debug; the changelog counters, store.num.keys and store.segments are exported at info.

MeterTypeTagsWhat it measures
stoatflow.store.get.totalCounterstore_nameGet operations. (debug)
stoatflow.store.put.totalCounterstore_namePut operations. (debug)
stoatflow.store.delete.totalCounterstore_namedelete() calls from user code only — StoatFlow's DSL tombstones through put(key, null), counted under put. (debug)
stoatflow.store.get.latencyTimerstore_namePoint-read latency. (debug)
stoatflow.store.put.latencyTimerstore_nameWrite latency. (debug)
stoatflow.store.delete.latencyTimerstore_nameDelete latency, user-code delete() only. A delete() is a get plus a tombstone put, so it also lands in the get and put timers. (debug)
stoatflow.store.range.latencyTimerstore_nameRange query latency. (debug)
stoatflow.store.all.latencyTimerstore_nameFull-iteration latency. (debug)
stoatflow.store.flush.latencyTimerstore_nameStore flush latency. (debug)
stoatflow.store.cache.hit.totalCounterstore_nameStore cache hits. (debug)
stoatflow.store.cache.miss.totalCounterstore_nameStore cache misses. (debug)
stoatflow.store.changelog.records.written.totalCounterstore_nameChangelog records written.
stoatflow.store.changelog.bytes.written.totalCounterstore_nameChangelog bytes written.
stoatflow.store.commit.changelog.latencyTimerstore_namePer-store changelog serialization+send latency during the commit epoch. (debug)
stoatflow.store.commit.flush.latencyTimerstore_namePer-store flush latency during the commit epoch. (debug)
stoatflow.store.flush.entriesDistributionSummarystore_nameEntry count per epoch flush. (debug)
stoatflow.store.flush.sub.batchesDistributionSummarystore_nameSub-batch count per flush (1 = sequential, N = parallel). (debug)
stoatflow.store.num.keysGaugestore_name, store_typeCommitted key count of an in-memory store (KIP-1250 parity); silent for RocksDB-backed stores.
stoatflow.store.expired.recordsCounterstore_name, store_typeRecords event-time-expired (pruned past retention) from an in-memory window or session store; 0 for plain key-value stores (ADR-133).
stoatflow.store.segmentsGaugestore_name, store_typeLive segment count of a segmented RocksDB window or session store — one RocksDB instance per segment, ~7 file descriptors and 10 files each, no upper bound. Writes open segments and watermark-driven expiry drops them, so the count is bounded only while the watermark advances: in steady state it is phase-dependent and bounded by ceil(retention / segmentInterval) + 1 (wider pre-existing segments can raise it through a retention change); a stalled or lagging-but-live partition pins the watermark, nothing expires, and every write into a new time range adds a segment — the count climbs past that bound for as long as the lag lasts (RS-03). The value is the data segments the store holds (the offsets holder is one more RocksDB instance per store; a segment whose deletion waits behind an open iterator has already left the count). Sampled ~1 Hz from an O(1) counter. Pair it with stoatflow.watermark.lag.ms, which names the cause.
stoatflow.store.versioned.sweep.lag.msGaugestore_nameMilliseconds since the versioned store's last completed history-reclamation pass. Versioned-store retention is a minimum bound — an expired version stops being readable immediately, but its bytes are reclaimed by a rolling budgeted sweep whose target is one stoatflow.state.versioned.sweep-period-ms. This is that lag, made observable: alert when it exceeds the configured period for a sustained interval.
stoatflow.store.versioned.sweep.budget.exhausted.totalCounterstore_nameRetention-sweep ticks that hit a per-barrier byte, entry or deletion cap before finishing their slice. A steady rate means the configured sweep period is not achievable at this store's size — raise stoatflow.state.versioned.max-scan-bytes-per-barrier, or accept a longer period.
stoatflow.store.versioned.sweep.positionGaugestore_nameEntries the retention sweep has visited since the current pass began. Progress detail; the cursor is a byte array, so there is no cheap ordinal to report instead. (debug)
stoatflow.store.versioned.sweep.failures.totalCounter—Retention-sweep ticks that failed. The tick catches every error by design, so that a reclamation problem can never disturb the commit protocol — which also means a persistently-broken sweep is invisible outside the log while the app looks healthy. Untagged (a failure can happen before the store is even resolved; the accompanying warning names the sub-topology). Any sustained increase means reclamation is not running.
stoatflow.store.versioned.sweep.reclaimed.totalCounterstore_nameVersions the retention sweep removed from the local store. Pairs with sweep.tombstones.total below: in a healthy steady state the two track each other, and the local one running ahead means the store is shedding while its changelog is not.
stoatflow.store.versioned.sweep.tombstones.totalCounterstore_nameReclamation tombstones the sweep handed to the changelog — one Kafka null per reclaimed version, which is what lets log compaction bound a topic whose composite key it can never collapse on its own. Zero for a store with logging disabled (nothing to write to); otherwise a sustained gap below sweep.reclaimed.total means reclamation is not reaching the changelog and the topic will grow. Counts records handed to the changelog writer: under exactly-once the send rides the epoch transaction, so a broker-side failure surfaces as a failed commit rather than as a gap here.
stoatflow.store.format.downgrade.totalCounteroutcomeState stores whose on-disk format was newer than the topology asked for — record headers (KIP-1271) turned off over a store that carries them. Reported once per affected store at startup, before it opens. outcome is dropped-empty (the headers keyspace was empty and was dropped in place — free, no restore), wiped (acknowledged via stoatflow.state.format-downgrade: wipe-and-restore in YAML, or the state.format.downgrade Properties key; local state deleted and rebuilt from the changelog) or refused (startup failed). Do not alert on refused: the process exits moments later, so a scrape almost never lands in the window — treat a refusal as a log/crash-loop signal, and use this counter for the dropped-empty and wiped outcomes, which run on a process that goes on to serve. See state stores.
stoatflow.fk.enumeration.behind.scansCounterstore_nameForeign-key join evaluations that enumerated the subscription store as-of an earlier committed epoch because the reader was behind (ADR-136).
stoatflow.suppress.buffer.countGaugebuffer_nameRecords currently held in a suppression buffer.
stoatflow.suppress.buffer.bytesGaugebuffer_nameBytes currently held in a suppression buffer.
stoatflow.suppress.emit.totalCounterbuffer_nameRecords released from a suppression buffer.

Suppression buffers never drop records, so there is no drop counter here: a late record is dropped upstream by the windowed-aggregation guards and counted as stoatflow.dropped.records.total with reason=late_window or late_session. On a full buffer, EAGER early-emits the oldest entry (a forward, not a drop) while STRICT and maxBytes throw.

RocksDB

Opt-in, and off by default — set stoatflow.rocks-db.metrics.enabled: true for the property and cache gauges (info, cheap), and additionally statistics-enabled: true for the ticker, ratio and histogram meters (debug; enabling RocksDB statistics costs roughly 5–10% on the write path, and takes effect at store open, so it needs a restart). A recorder thread samples every interval-ms (default 60 s).

This section elides related meters into one row — compaction.time.{avg,min,max}.ms is three meters, not one. That is a deliberate exception to the rest of this page, which spells every name out: the family is ~43 series per store, and elision keeps it readable. All meters carry store_name except the three global shared-cache gauges.

Two caveats worth knowing before you build a dashboard on these. open.files is not published — it is derived from a RocksDB ticker (NO_FILE_CLOSES) that no longer exists in the bundled RocksJava. And *.min.ms is JNI-only: the default FFM backend reads statistics from a text dump that carries no MIN token, so those gauges stay silent unless you set stoatflow.rocks-db.backend: JNI. Averages, maxima and totals are unaffected.

MeterTypeTagsWhat it measures
stoatflow.rocksdb.{bytes.written,bytes.read}.totalCounterstore_nameBytes written / read by RocksDB. (debug)
stoatflow.rocksdb.memtable.bytes.flushed.totalCounterstore_nameBytes written to disk by memtable flushes. (debug)
stoatflow.rocksdb.compaction.bytes.{read,written}.totalCounterstore_nameBytes read / written by compaction — the write-amplification signal. (debug)
stoatflow.rocksdb.file.errors.totalCounterstore_nameRocksDB file errors. (debug)
stoatflow.rocksdb.memtable.hit.ratioGaugestore_nameMemtable hit ratio over the last sample interval. (debug)
stoatflow.rocksdb.block.cache.{,data.,index.,filter.}hit.ratioGaugestore_nameBlock-cache hit ratio over the last sample interval — overall, then per block role. A falling data-block ratio with a healthy index ratio usually means the working set outgrew the cache. (debug)
stoatflow.rocksdb.compaction.time.{avg,min,max}.msGaugestore_nameCompaction time. (debug)
stoatflow.rocksdb.compaction.time.total.msCounterstore_nameCumulative compaction time. (debug)
stoatflow.rocksdb.flush.time.{avg,min,max}.msGaugestore_nameMemtable-flush time. (debug)
stoatflow.rocksdb.flush.time.total.msCounterstore_nameCumulative memtable-flush time. (debug)
stoatflow.rocksdb.write.stall.duration.avg.msGaugestore_nameWrite-stall duration. Any sustained value is RocksDB backpressuring your commit path. (debug)
stoatflow.rocksdb.write.stall.duration.total.msCounterstore_nameCumulative time writes were stalled. (debug)
stoatflow.rocksdb.num.immutable.mem.tableGaugestore_nameImmutable memtables not yet flushed.
stoatflow.rocksdb.{cur.size.active.mem.table,cur.size.all.mem.tables,size.all.mem.tables}.bytesGaugestore_nameMemtable sizes: active, active plus unflushed, and including pinned.
stoatflow.rocksdb.num.entries.{active.mem.table,imm.mem.tables}Gaugestore_nameEntries in the active / unflushed immutable memtables.
stoatflow.rocksdb.num.deletes.{active.mem.table,imm.mem.tables}Gaugestore_nameDelete entries in the active / unflushed immutable memtables.
stoatflow.rocksdb.{mem.table.flush.pending,compaction.pending}Gaugestore_name1 while a flush / compaction is pending, else 0.
stoatflow.rocksdb.num.running.{flushes,compactions}Gaugestore_nameCurrently running flushes / compactions.
stoatflow.rocksdb.estimate.pending.compaction.bytesGaugestore_nameBytes compaction must rewrite to bring the LSM tree back to target size. A climbing value means compaction is losing ground.
stoatflow.rocksdb.{total.sst.files.size,live.sst.files.size}.bytesGaugestore_nameSST bytes on disk: all files, and those in the latest version.
stoatflow.rocksdb.num.live.versionsGaugestore_nameLive versions — more than one means an open iterator is pinning old files.
stoatflow.rocksdb.estimate.num.keysGaugestore_nameEstimated live keys. This is the RocksDB counterpart of store.num.keys, which covers in-memory stores only.
stoatflow.rocksdb.estimate.table.readers.mem.bytesGaugestore_nameMemory held by table readers (indexes and filters), outside the block cache.
stoatflow.rocksdb.background.errorsGaugestore_nameAccumulated background errors. Alert on any non-zero value.
stoatflow.rocksdb.block.cache.{capacity,usage,pinned.usage}.bytesGaugestore_nameThe shared block cache, repeated per store so Kafka Streams dashboards port unchanged.
stoatflow.rocksdb.shared.block.cache.{capacity,usage,pinned.usage}.bytesGauge—The shared block cache, reported once. StoatFlow runs one LRU cache across all stores, so these are the real numbers — prefer them over the per-store duplicates above.

State restoration

MeterTypeTagsWhat it measures
stoatflow.restoration.in.progressGauge—1 while restoration is running, 0 otherwise.
stoatflow.restoration.stores.totalGauge—Total stores to restore.
stoatflow.restoration.stores.completedGauge—Stores that have finished restoring.
stoatflow.restoration.records.restored.totalCounterstore_nameRecords restored, per store.
stoatflow.restoration.bytes.restored.totalCounterstore_nameChangelog bytes restored, per store — serialized key plus value as read off the changelog, excluding record and batch overhead. Compare against changelog volume to size a cold start.
stoatflow.restoration.remaining.recordsGaugestore_nameChangelog records the store still has to restore, across all its changelog partitions (queued ones included); counts down during restoration and reads 0 once the store is restored. An estimate — end offset minus start offset minus records restored, so a transactional changelog's commit markers keep it slightly above the true count until the store completes. The Kafka Streams–compatible metrics mode sums it into restore-remaining-records-total.
stoatflow.restoration.duration.secondsTimerstore_namePer-store restoration duration, recorded once when the store finishes. A store's changelog partitions restore in parallel, so this is the slowest partition, not their sum.

Consumer, producer, and buffers

MeterTypeTagsWhat it measures
stoatflow.consumer.poll.latencyTimer—Consumer poll latency.
stoatflow.consumer.poll.totalCounter—Poll invocations.
stoatflow.consumer.poll.recordsDistributionSummary—Records per poll.
stoatflow.consumer.records.consumed.totalCountertopic, partitionRecords consumed.
stoatflow.consumer.bytes.consumed.totalCountertopic, partitionBytes consumed.
stoatflow.consumer.lag.recordsGaugetopic, partitionConsumer lag, in records.
stoatflow.consumer.assigned.partitionsGauge—Number of assigned partitions.
stoatflow.source.pattern.matched.topicsGaugesourceTopics currently matched by a pattern-subscribed source node; updated on every (re)assignment. 0 means the pattern matches nothing yet.
stoatflow.consumer.partition.forgotten.totalCounterreasonSource partitions dropped from offset tracking because their topic no longer exists. reason=topic_deleted is a pattern-matched topic deleted on the default subscribe() path, answered by an in-place engine restart; reason=ha_source_shrink is the HA assign()-mode active's periodic detector confirming a shrink and forgetting the offsets in place. Counts partitions actually dropped, not drops requested — the HA detector re-confirms the same shrink every two ticks, and a partition that was assigned but never produced a record has nothing to drop — so a rate() over this meter reflects real shrink activity.
stoatflow.consumer.partitions.pausedGauge—Currently paused partitions.
stoatflow.consumer.partitions.delay.pendingGauge—Partitions with a pending resume delay.
stoatflow.consumer.partition.paused.totalCountertopic, partition, reasonA partition paused because its input buffer filled (reason=capacity). Counted even when the partition is already paused for another reason, so paused − resumed is the number of partitions currently paused for capacity. The application pause (POST /pause) and state restoration at start-up pause partitions too but are not counted here — the application state reports them. Watermark alignment never pauses a partition (it holds its records at dispatch instead — see the watermark.alignment meters).
stoatflow.consumer.partition.resumed.totalCountertopic, partition, reasonA partition released from its capacity pause, including when it is revoked — the counterpart of paused.total.
stoatflow.producer.records.produced.totalCountertopicRecords produced.
stoatflow.producer.bytes.produced.totalCountertopicBytes produced.
stoatflow.producer.send.latencyTimertopicProducer send latency. (debug)
stoatflow.buffer.partition.sizeGaugetopic, partitionCurrent partition buffer depth. (debug)
stoatflow.buffer.partition.capacityGaugetopic, partitionPartition buffer capacity. (debug)
stoatflow.buffer.offer.totalGaugetopic, partitionCumulative records offered to the partition buffer. (debug)
stoatflow.buffer.poll.totalGaugetopic, partitionCumulative records polled from the partition buffer. (debug)
stoatflow.buffer.oldest.age.msGaugetopic, partitionAge (by its timestamp) of the next record to leave the partition buffer — the lowest buffered offset; 0 when the buffer is empty or that record is stamped in the future. Sampled about twice a second; if sampling stops while records wait (a stalled dispatcher), it reports at least the time since the last sample rather than 0. (debug)
stoatflow.buffer.scheduled.source.sizeGauge—Scheduled-source dispatch buffer depth.
stoatflow.buffer.timer.event.sizeGauge—Event-time timer buffer depth.
stoatflow.buffer.timer.processing.sizeGauge—Processing-time timer buffer depth.

Dispatcher internals

MeterTypeTagsWhat it measures
stoatflow.dispatcher.batch.sizeDistributionSummary—Records per dispatch batch. (debug)
stoatflow.dispatcher.batch.held.totalCounter—Batches held because a target lane lacked capacity. (debug)
stoatflow.dispatcher.dispatch.latencyTimer—Per-batch dispatch latency.
stoatflow.parallel.deser.activated.totalCounter—Times parallel deserialization was activated.
stoatflow.queue.offer.failure.totalCounterqueue_typeInternal queue offer rejections (lane, partition_buffer, commit).
stoatflow.coordinator.select.latencyTimer—Time spent selecting the next timestamp-ordered batch. (debug)
stoatflow.coordinator.partition.switchesDistributionSummary—Partition switches per selected batch. (debug)

Watermarks and event time

MeterTypeTagsWhat it measures
stoatflow.watermark.currentGauge—Current global watermark (epoch ms).
stoatflow.watermark.lag.msGauge—Wall-clock time minus the global watermark (event-time lag). Sampled ~1 Hz while event-time tracking is on — independently of whether the watermark advanced, so during a stall it climbs rather than freezing at its last healthy value (RS-23). -1 until the first watermark has been emitted.
stoatflow.watermark.advance.totalCounter—Watermark advancement events.
stoatflow.watermark.preview.partitionsGauge—Partitions whose next record waits behind older records of other partitions — a record stamped in the future, or a partition far ahead during a skewed catch-up or replay — and whose watermark meanwhile counts as if that record had been processed, so event time follows the other partitions instead of standing still. Sampled ~1 Hz while event-time tracking is on; idle partitions are not counted, nor one whose own watermark is already as high. See its previewWatermarkMs / headHeldSinceMs on /watermarks. Can also read non-zero while the application is paused or fully backpressured.
stoatflow.watermark.partition.currentGaugetopic, partitionPer-partition watermark. (debug)
stoatflow.watermark.late.records.totalCountertopic, partitionRecords arriving after the watermark. (debug)
stoatflow.watermark.out.of.orderness.maxGauge—Maximum observed out-of-orderness (ms). (debug)
stoatflow.watermark.alignment.drift.msGaugetopic, partition, groupPer-partition drift above the alignment group's minimum watermark. (debug)
stoatflow.watermark.alignment.group.minGaugegroupMinimum watermark across the alignment group (epoch ms). (debug)
stoatflow.watermark.alignment.partitions.heldGaugegroupPartitions currently held for watermark alignment: their watermark is ahead of a member of their group by more than the drift, so their records are dispatched only in timestamp order. A held partition keeps being fetched.
stoatflow.watermark.alignment.held.time.totalCountergroupSeconds during which a hold in the group stopped all dispatch: the held partition's next record was the oldest buffered one, and a lagging member of its group had nothing buffered. Only one hold binds at a time, so rate() is the fraction of time the application waited on the group. Accrues while the hold lasts, not only when it ends. Counts no time spent waiting for any other reason (a readiness wait, backpressure, a pause).
stoatflow.watermark.alignment.yields.totalCountergroupTimes a held partition's records were dispatched in order although its group's laggards had not caught up — every laggard had data, so waiting could not release the hold. At most once per partition per alignment updateInterval. A flat held.time.total with a rising yields.total usually means the group's sources have different out-of-orderness bounds.

Timers, punctuators, and scheduled sources

MeterTypeTagsWhat it measures
stoatflow.timer.registered.totalCountertypeKeyed timers registered (event_time, processing_time).
stoatflow.timer.fired.totalCountertypeTimers fired.
stoatflow.timer.deleted.totalCountertypeTimers explicitly deleted.
stoatflow.timer.lane.mismatch.totalCounterprocessorRecord keys whose re-serialization with the processor's timer key serde picks another lane than the record's — the key's timers would run on another lane. Pre-registered at 0 for every timer-declaring Processor API processor. A split for a timer on the record's own key stops the app with TimerLaneAffinityException by default (timer.lane-affinity.guard); a sampled split is WARN-only by default (timer.lane-affinity.sampled-check). Recorded at every level. Alert on any increase and on application state — a fatal detection can stop the app before the next scrape.
stoatflow.timer.queue.sizeGaugetypeTimer dispatch-buffer depth, by timer type.
stoatflow.timer.fire.latencyTimertypeDelay between scheduled and actual fire time. (debug)
stoatflow.timer.fire.late.totalCountertypeTimers that fired late. (debug)
stoatflow.punctuator.invocation.totalCounterpunctuator_id, type, modePunctuator invocations.
stoatflow.punctuator.skip.totalCounterpunctuator_idPunctuator invocations skipped (previous run still active).
stoatflow.punctuator.activeGauge—Currently registered punctuators.
stoatflow.punctuator.execution.latencyTimerpunctuator_id, typePunctuator execution time. (debug)
stoatflow.scheduled.source.trigger.totalCountersource_name, typeScheduled source triggers.
stoatflow.scheduled.source.records.emitted.totalCountersource_nameRecords emitted by scheduled sources.

Error handling

MeterTypeTagsWhat it measures
stoatflow.error.totalCountererror_type, topicErrors by type (topic omitted when unknown).
stoatflow.error.typed.totalCountercategory, error_typeCategorised errors (e.g. production failures), by category and type.
stoatflow.dropped.records.totalCounterreasonRecords dropped before processing (KS dropped-records parity). The reason tag is the semantics — seven values: null_key (a record whose key is null, or serializes to null, reaching an aggregation), late_window / late_session (a record arriving after its window or session already closed), join_null_key / join_null_value (a join input the join cannot match on), table_source_null_key (a null-keyed record on a KTable source topic), null_window_value (a time or sliding window whose stored value your value serde reads back as null, so its final OnWindowClose result is not emitted — counted once per window, at close; also logged at WARN once per processor, and again after a restart; a closed session in that state is forwarded as null, as in Kafka Streams).
stoatflow.sink.records.dropped.totalCountersink_name, topicRecords dropped because a custom partitioner returned no partition (KIP-837).
stoatflow.sink.records.multicast.totalCountersink_name, topicOutput records produced via multicast — a custom partitioner returned multiple partitions (KIP-837).
stoatflow.dlq.poison.quarantined.totalCounter—Source records quarantined as poison after an asynchronous broker-side production failure returned continue. The epoch's source offsets are held and the epoch is replayed with exactly these records skipped, so the epoch's other records survive — but a quarantined record loses all of its outputs, across every sink it fans out to. See Error-handling model.
stoatflow.dlq.poison.replays.totalCounter—Poison-epoch replays: in-place engine restarts that reprocess an aborted epoch with the quarantined records skipped. Bounded by stoatflow.commit-barrier.max-poison-replays within poison-replay-window-ms. That budget is in-memory — exhausting it ends the process and the pod restarts with a clean budget — so continue is bounded per process, never end to end. Alert on this meter.
stoatflow.dlq.poison.replay.budget.exhausted.totalCounter—The replay budget ran out: max-poison-replays replays were already consumed within poison-replay-window-ms, so the instance shut down with the source offsets held rather than replaying again. Terminal — any non-zero value is an incident. Alert on it alongside …replays.total, which counts replays and therefore stops incrementing at the moment a non-converging poison starts crash-looping the pod.
stoatflow.dlq.poison.quarantine.sizeGauge—Source offsets currently quarantined and skipped by a replay. 0 in steady state; entries retire as soon as a commit moves permanently past them.

High availability

Registered only when hot standby is enabled (ha.mode != off).

MeterTypeTagsWhat it measures
stoatflow.ha.roleGauge—1 when this pod is the active, 0 when standby.
stoatflow.ha.standby.replication.lag.recordsGauge—Total committed-changelog lag in records across all changelog partitions; 0 when active or caught up.
stoatflow.ha.standby.replication.applied.recordsCounter—Cumulative changelog records the standby applier has applied; rate(...) = apply throughput.
stoatflow.ha.standby.replication.lag.msGauge—Standby replication lag in milliseconds.
stoatflow.ha.peer.freshness.msGauge—Time since the freshest observed peer's last heartbeat.
stoatflow.ha.peersGauge—Number of observed HA peers.
stoatflow.ha.restart.requiredGauge—1 when this pod needs a restart (e.g. after a source-topic partition-count change).
stoatflow.ha.restartingGauge—1 while this pod is restarting its engine in place to absorb a processing fault. The pod stays ACTIVE throughout, so stoatflow.ha.role does not move — this is what distinguishes "absorbing a fault" from "healthy".
stoatflow.ha.redundancy.ready.standbysGauge—Caught-up (READY_STANDBY) fresh standby count, including self.
stoatflow.ha.redundancy.below.desiredGauge—1 when the active has fewer ready standby spares than ha.desired-standbys.
stoatflow.ha.token.epochGauge—Current promotion-token epoch on the metadata topic.
stoatflow.ha.election.outcomeCounteroutcomeCumulative election outcomes: won, reclaimed, stood_down_fresh, stood_down_lost.
stoatflow.ha.election.latency.msGauge—Last claim-to-ACTIVE promotion latency (ms); -1 until the first promotion.

License

Registered once at startup; exported at every recording level. See License configuration.

MeterTypeTagsWhat it measures
stoatflow.license.validGaugetier1 under a VALID or GRACE_PERIOD license, else 0.
stoatflow.license.expiry_timestamp_secondsGauge—Unix epoch seconds of the license entitlement deadline.
stoatflow.license.days_remainingGauge—Whole days until expiry; negative within the post-expiry grace window.
stoatflow.license.grace_remaining_secondsGauge—Seconds left in the offline-grace budget; floored at 0.
stoatflow.license.heartbeat_consecutive_failuresGauge—Heartbeat ticks failed since the last success.
stoatflow.license.heartbeat_last_success_timestamp_secondsGauge—Unix epoch seconds of the last successful heartbeat.
stoatflow.license.validations_totalCounterresultCold-start validation outcomes, by result.
stoatflow.license.heartbeats_totalCounterresultHeartbeat tick outcomes, by result.

JVM, system, and Kafka-client meters

Beyond the stoatflow.* meters above, the same endpoint carries:

  • JVM and system meters (jvm_*, system_*, process_*) — bound by default (runtime.metrics.bind-jvm-metrics: true): heap/non-heap memory, GC pauses, thread counts, classloading, CPU usage.
  • Native Kafka client meters (kafka_consumer_*, kafka_producer_*) — the Kafka clients' own JMX metrics surfaced through Micrometer.

These carry the registry common tags (application plus your common-tags) but not application_id. See Metrics for details.

Next steps

  • Metrics — enabling, scraping, recording levels, naming and tag conventions, PromQL examples.
  • Observability — wiring /metrics into a monitoring stack and what to alert on.
  • Configuration reference — the runtime.metrics.* keys.