Metrics
The :runtime module exports metrics in Prometheus text format on GET /metrics. Internally it's a Micrometer PrometheusMeterRegistry — the engine reports events to a Micrometer instrumentation callback, the runtime binds JVM and Kafka-client metrics to the same registry, and the handler serves the registry's scrape output. Point your existing Prometheus stack at the endpoint and the StoatFlow meters appear alongside everything else.
Enable and scrape
Metrics are enabled by default and live under runtime.metrics:
runtime:
http:
enabled: true
port: 8080 # default
metrics:
enabled: true # default
prefix: stoatflow # default — prefix on all metric names
recording-level: info # info | debug | trace (default: info)
bind-jvm-metrics: true # default — JVM memory/GC/thread binders
common-tags: # extra tags applied to every meter
environment: production
service: word-count
Scrape it from the shell:
curl -s localhost:8080/metrics | head -40
A minimal Prometheus scrape config:
scrape_configs:
- job_name: stoatflow
metrics_path: /metrics
static_configs:
- targets: ["my-app:8080"]
The endpoint returns the standard exposition format, so any Prometheus-compatible collector (Prometheus, Grafana Agent, OpenTelemetry Collector with the Prometheus receiver, the VictoriaMetrics agent) scrapes it without extra translation. On Kubernetes, add prometheus.io/scrape / prometheus.io/path pod annotations or a ServiceMonitor pointing at the HTTP port — see Observability.
runtime.metrics.enabled: false) un-registers the /metrics endpoint — it returns 404 — and switches the engine to no-op instrumentation, so there's no measurement overhead when metrics are off.Recording levels
recording-level controls how much detail the engine reports, mirroring Kafka Streams' levels. A higher level is a superset of the ones below it.
| Level | What it adds | Use |
|---|---|---|
info | Essential operational meters — throughput, barrier/commit latency, watermark progress, restoration, consumer/producer, errors. | Production default. |
debug | Per-store, per-partition, per-processor detail and dispatch internals. Higher cardinality. | Targeted troubleshooting. |
trace | Deepest internals; highest overhead. | Development only. |
Tags like lane_id, store_name, topic, and partition multiply series count. On large topologies, keep production at info and switch to debug only while investigating.
Naming and tags
stoatflow.* names are not KS names. Set runtime.metrics.naming: both to also emit Kafka Streams-named, KS-shaped series derived from these meters so your existing KS dashboards work — see Reusing your Kafka Streams dashboards. (One side effect while it's on: the mapped native timers' _seconds_max tracks a ~30 s window instead of the ~2 min default, and stoatflow.e2e.latency gains a {quantile="0.0"} series.)All StoatFlow meters use a dotted name with the configured prefix (stoatflow by default). The Prometheus registry rewrites dots to underscores and appends the conventional suffix (_total for counters; _seconds_count/_seconds_sum/_seconds_max for timers). So stoatflow.barrier.completed.total is scraped as stoatflow_barrier_completed_total, and the timer stoatflow.barrier.commit.latency produces stoatflow_barrier_commit_latency_seconds_* series. The latency timers publish client-side percentiles as {quantile="0.5|0.95|0.99"} labels on the base _seconds name rather than _bucket histograms, so use rate(_sum)/rate(_count) (or the {quantile=...} series) — histogram_quantile() over _bucket won't match.
Two application tags are present on StoatFlow meters:
| Tag | Source | Notes |
|---|---|---|
application | Registry-level common tag | Carries application-id; applied to every meter, including JVM and Kafka-client metrics. |
application_id | Per-meter tag | Applied by the StoatFlow instrumentation callback. |
Anything you add under common-tags (for example environment, region, service) is also applied to every meter — handy for selecting one deployment in a shared Prometheus. Per-meter tags vary by group: lane_id, store_name, topic, partition, error_type, trigger, type, result, and others are listed with each meter below.
{subtopology}_{lane} — e.g. 0_0, 0_15, 1_7. Aggregate across a sub-topology in PromQL with a regex matcher: sum(stoatflow_lane_records_processed_total{lane_id=~"1_.*"}).sub_topology / subtopology_id value or a lane_id=~"N_.*" regex. Re-check those selectors against a live scrape after upgrading. Setting stoatflow.topology.sub-topology-split: eager restores the previous numbering. Processor-API topologies renumber a second time in 1.0.0: process() / processValues() no longer open a boundary after a re-key (stoatflow.topology.processor-api-key-affinity: off, matching Kafka Streams); presumed restores those.Key meters
The dotted names below are the canonical metric IDs the engine emits. Remember the Prometheus rewrite (. → _, plus the type suffix) when writing queries. This is a working set, not the exhaustive list — the full catalogue is on the metrics reference, and your live scrape stays authoritative for exact tag sets.
Two families are deliberately left to the reference rather than repeated here: stoatflow.rocksdb.* (~43 series per store, opt-in via stoatflow.rocks-db.metrics.enabled — reach for it when tuning RocksDB, not on a general dashboard) and stoatflow.suppress.* (a suppression-tuning signal).
Throughput and end-to-end latency
| Metric | Type | Level | Tags | Meaning |
|---|---|---|---|---|
stoatflow.lane.records.processed.total | Counter | info | lane_id | Records processed by a lane. |
stoatflow.lane.bytes.processed.total | Counter | info | lane_id | Bytes processed by a lane. |
stoatflow.lane.process.latency | Timer | info | lane_id | Per-record processing latency. |
stoatflow.lane.queue.size | Gauge | info | lane_id | Current lane queue depth (backpressure signal). |
stoatflow.lane.queue.capacity | Gauge | info | lane_id | Configured lane queue capacity. |
stoatflow.lane.queue.full.total | Counter | info | lane_id | Times a lane queue was full (backpressure events). |
stoatflow.e2e.latency | Timer | info | — | Per-record end-to-end latency, source timestamp to processing completion (analogous to KS record-e2e-latency). |
# Total processing throughput (records/sec)
sum(rate(stoatflow_lane_records_processed_total[1m]))
# Average per-record processing latency (this timer emits _sum/_count/_max, not _bucket)
sum(rate(stoatflow_lane_process_latency_seconds_sum[5m])) / sum(rate(stoatflow_lane_process_latency_seconds_count[5m]))
Barrier commits
Commit barriers are StoatFlow's exactly-once mechanism (see Exactly-once). These meters track barrier lifecycle and commit-phase latency. The latency timers publish client-side P50/P95/P99 percentiles.
| Metric | Type | Level | Tags | Meaning |
|---|---|---|---|---|
stoatflow.barrier.initiated.total | Counter | info | — | Barriers initiated. |
stoatflow.barrier.completed.total | Counter | info | — | Barriers successfully completed. |
stoatflow.barrier.failed.total | Counter | info | error_type | Barrier failures, by error type (e.g. timeout, producer_fenced, fatal_error for an Error thrown by user code inside the commit; full list in the metrics reference). |
stoatflow.barrier.abandoned.total | Counter | info | reason | Barriers deliberately not committed (pre_assignment, poison_abort, abort_teardown); the epoch replays on restart. Not a failure. |
stoatflow.epoch.invariant.violations.total | Counter | info | kind | Epoch-isolation, offset-order and input-buffer-bound tripwires. Unreachable by design: any non-zero value is a bug to report (alert on it). One exception: partition_offset_regression also counts each broker-side seek-back (log truncation, an offset reset), after which the re-fetched records are processed again. |
stoatflow.barrier.in.flight | Gauge | info | — | Currently pending barriers. |
stoatflow.barrier.latency | Timer | info | — | Initiation to completion. |
stoatflow.barrier.alignment.latency | Timer | info | — | Time for all lanes to receive the barrier. |
stoatflow.lane.holdback.records | DistributionSummary | info | sub_topology | Records and timer fires held back and replayed per barrier at a sub-topology boundary (receiver-side epoch hold-back — keeps the commit a consistent cut across the cascade). |
stoatflow.lane.holdback.drains.total | Counter | info | sub_topology | Non-empty hold-back drains (a receiving lane replayed stashed ahead-of-epoch records or timer fires after crossing its barrier). |
stoatflow.barrier.commit.latency | Timer | info | — | Commit latency (producer drain + Kafka TX + state flush). Comparable to KS commit-latency. |
stoatflow.barrier.commit.changelog.latency | Timer | info | — | Aggregate changelog serialization+send latency per commit epoch. |
stoatflow.barrier.commit.state.flush.latency | Timer | info | — | Aggregate state-store flush latency per commit epoch. |
stoatflow.barrier.commit.producer.flush.latency | Timer | info | — | Pre-commit producer flush latency. |
stoatflow.barrier.commit.send.offsets.latency | Timer | info | — | sendOffsetsToTransaction RPC latency. |
stoatflow.barrier.records.committed.total | Counter | info | — | Records committed across barriers. |
stoatflow.barrier.bytes.committed.total | Counter | info | — | Bytes committed across barriers. |
stoatflow.barrier.trigger.total | Counter | info | trigger | Barrier creation count by trigger — TIME, RECORD_COUNT, MEMORY_PRESSURE, CACHE_PRESSURE (state-cache cap forced an early commit). |
stoatflow.transaction.commit.total | Counter | info | — | Kafka transactions committed. |
stoatflow.transaction.abort.total | Counter | info | — | Kafka transactions aborted. |
stoatflow.transaction.commit.latency | Timer | info | — | Transaction commit latency. |
# Commit rate vs. failure rate
rate(stoatflow_barrier_completed_total[1m])
rate(stoatflow_barrier_failed_total[5m])
stoatflow.barrier.interval.target.ms, stoatflow.barrier.epoch.max.records, and related) report what the engine is currently doing so you can see the cadence on a dashboard. The bound knobs themselves are documented under Tuning and the configuration reference.Watermarks
Event-time progress for windowing and late-event detection (see Event time and watermarks).
| Metric | Type | Level | Tags | Meaning |
|---|---|---|---|---|
stoatflow.watermark.current | Gauge | info | — | Current global watermark (epoch ms). |
stoatflow.watermark.lag.ms | Gauge | info | — | Wall-clock time minus the global watermark (event-time lag). Sampled ~1 Hz regardless of whether the watermark advanced, so it climbs during a stall; -1 until the first watermark is emitted. |
stoatflow.watermark.advance.total | Counter | info | — | Watermark advancement events. |
stoatflow.watermark.preview.partitions | Gauge | info | — | Partitions whose next record waits behind older records of other partitions and counts toward the global watermark as if it had been processed, so event time keeps moving (see records stamped in the future). Typical causes: a record stamped in the future, or a partition far ahead of the others during a skewed catch-up or replay. Sampled ~1 Hz; idle partitions are not counted, nor one whose own watermark is already as high. Non-zero for long with no catch-up under way usually means a producer clock or a timestamp unit needs checking — /watermarks names the partition. It can also read non-zero while the application is paused or fully backpressured. |
stoatflow.watermark.late.records.total | Counter | debug | topic, partition | Records arriving after the watermark. |
stoatflow.watermark.alignment.held.time.total | Counter | info | group | Seconds during which a watermark-alignment hold in the group stopped all dispatch, waiting for a lagging source with nothing buffered. rate() is the fraction of time the application waited on the group; a sustained climb means the drift is sized too small for the source's gaps between records. |
# Alert when event time falls more than 5 minutes behind wall clock while the app is running (RUNNING == 4).
# The lag meter climbs during a stall — a lagging-but-live partition pins the global watermark and every
# window/session store stops expiring, so segments accumulate (see stoatflow_store_segments below).
stoatflow_watermark_lag_ms > 300000 and stoatflow_client_state == 4
# Fraction of time watermark alignment kept the application waiting, per group (0..1).
rate(stoatflow_watermark_alignment_held_time_total[5m])
Changelog writes and state restoration
Changelog write counters track state durability traffic; restoration meters track recovery progress on startup (see State stores and State and thread safety).
| Metric | Type | Level | Tags | Meaning |
|---|---|---|---|---|
stoatflow.store.changelog.records.written.total | Counter | info | store_name | Changelog records written. |
stoatflow.store.changelog.bytes.written.total | Counter | info | store_name | Changelog bytes written. |
stoatflow.store.expired.records | Counter | info | store_name, store_type | Records event-time-expired past retention from an in-memory window or session store. A sudden climb usually means watermarks jumped — worth checking during an incident. |
stoatflow.store.segments | Gauge | info | store_name, store_type | Live segment count of a segmented RocksDB window/session store (one RocksDB instance, ~7 file descriptors per segment). In steady state the count is phase-dependent and bounded by ceil(retention / segmentInterval) + 1 (wider pre-existing segments can raise it through a retention change); it climbs without bound while a lagging-but-live partition pins the watermark — alert when it stays above that bound (RS-03). |
stoatflow.store.versioned.sweep.lag.ms | Gauge | info | store_name | Milliseconds since a versioned store's last completed history-reclamation pass. Its retention is a minimum bound — expired versions stop being readable at once, but their bytes are reclaimed by a rolling sweep. Alert when this exceeds stoatflow.state.versioned.sweep-period-ms for a sustained interval: reclamation is not keeping up and the store will keep growing. Reclamation runs normally on a quiet application too — an idle app still commits on a slow cadence, so the sweep keeps running until it has nothing left to reclaim, then stops. |
stoatflow.store.versioned.sweep.budget.exhausted.total | Counter | info | store_name | Sweep ticks that hit a per-barrier cap before finishing their slice. Isolated increments under a burst are normal; 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.failures.total | Counter | info | — | Reclamation ticks that failed. The tick catches everything by design (a sweep failure has no business in the commit protocol), so without this a persistently-broken sweep is invisible outside the log while the process looks healthy. Any sustained increase means reclamation is not running at all. |
stoatflow.store.versioned.sweep.reclaimed.total | Counter | info | store_name | Versions the retention sweep removed from the local store. |
stoatflow.store.versioned.sweep.tombstones.total | Counter | info | store_name | Reclamation tombstones handed to the changelog writer. Alert when this falls persistently behind sweep.reclaimed.total: the local store is shedding but the topic is not. |
stoatflow.fk.enumeration.behind.scans | Counter | info | store_name | Foreign-key join evaluations that had to scan the subscription store as-of an earlier committed epoch. Sustained non-zero means FK joins are paying the slower enumeration path. |
stoatflow.restoration.in.progress | Gauge | info | — | 1 while restoration is running, 0 otherwise. |
stoatflow.restoration.stores.total | Gauge | info | — | Total stores to restore. |
stoatflow.restoration.stores.completed | Gauge | info | — | Stores that have finished restoring. |
stoatflow.restoration.records.restored.total | Counter | info | store_name | Records restored, per store. |
stoatflow.restoration.bytes.restored.total | Counter | info | store_name | Bytes restored, per store. |
stoatflow.restoration.remaining.records | Gauge | info | store_name | Changelog records still to restore, per store (0 once restored). |
stoatflow.restoration.duration.seconds | Timer | info | store_name | Per-store restoration duration. |
# Restoration progress as a fraction of stores completed
stoatflow_restoration_stores_completed / stoatflow_restoration_stores_total
Consumer, producer, and dispatch
| Metric | Type | Level | Tags | Meaning |
|---|---|---|---|---|
stoatflow.consumer.records.consumed.total | Counter | info | topic, partition | Records consumed. |
stoatflow.consumer.bytes.consumed.total | Counter | info | topic, partition | Bytes consumed. |
stoatflow.consumer.lag.records | Gauge | info | topic, partition | Consumer lag, in records. |
stoatflow.consumer.poll.latency | Timer | info | — | Consumer poll latency. |
stoatflow.consumer.assigned.partitions | Gauge | info | — | Number of assigned partitions. |
stoatflow.consumer.partition.forgotten.total | Counter | info | reason | Source partitions dropped from offset tracking because their topic is gone. reason=topic_deleted — a pattern-matched topic was deleted on the default subscribe() path and the engine restarted in place to converge on the rest; reason=ha_source_shrink — the HA assign()-mode active's detector confirmed a shrink and dropped the offsets in place. |
stoatflow.producer.records.produced.total | Counter | info | topic | Records produced. |
stoatflow.producer.bytes.produced.total | Counter | info | topic | Bytes produced. |
stoatflow.dispatcher.dispatch.latency | Timer | info | — | Per-batch dispatch latency. |
# Worst-case consumer lag across partitions
max(stoatflow_consumer_lag_records)
Errors
Error classification for alerting (see Error handling).
| Metric | Type | Level | Tags | Meaning |
|---|---|---|---|---|
stoatflow.error.total | Counter | info | error_type, topic | Errors by type (and topic where applicable). |
stoatflow.error.typed.total | Counter | info | category, error_type | Categorised errors (e.g. deserialization, processing, production). |
stoatflow.queue.offer.failure.total | Counter | info | queue_type | Internal queue offer rejections. |
stoatflow.dlq.poison.replays.total | Counter | info | — | Poison-epoch replays — in-place engine restarts that reprocess an aborted epoch with the poison skipped. The one to alert on: the replay budget is in-memory, so it bounds continue per process, not end to end, and a non-converging poison will crash-loop the pod. |
stoatflow.dlq.poison.quarantined.total | Counter | info | — | Source records quarantined as poison ahead of a replay. Each one loses its own outputs across all its sinks — bounded loss, not zero. |
stoatflow.dlq.poison.replay.budget.exhausted.total | Counter | info | — | The replay budget ran out and the instance shut down with the offsets held. Terminal — alert on it too: …replays.total counts replays, so it goes quiet precisely when a non-converging poison starts crash-looping the pod. |
stoatflow.dlq.poison.quarantine.size | Gauge | info | — | Source offsets a replay is currently skipping. 0 in steady state; non-zero only between a poison abort and the replay's first clean commit. |
stoatflow.timer.lane.mismatch.total | Counter | info | processor | Record keys whose re-serialization with the processor's timer key serde picks another lane than the record's, so the key's timers would run on another lane (see Timer key serde). Pre-registered at 0 per timer-declaring Processor API processor. A split for a timer on the record's own key stops the app by default (TimerLaneAffinityException, timer.lane-affinity.guard); a sampled split is WARN-only by default (timer.lane-affinity.sampled-check). Alert on any increase and on application state — a fatal detection can stop the app before the next scrape. The fix is a declared key serde. |
Client state
| Metric | Type | Level | Tags | Meaning |
|---|---|---|---|---|
stoatflow.client.state | Gauge | info | — | 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.lanes | Gauge | info | — | Configured lane count. |
stoatflow.client.lane.alive | Gauge | info | — | Currently running lanes. |
stoatflow.client.uptime.seconds | Gauge | info | — | Seconds since the client was created. |
stoatflow.engine.restart.total | Counter | info | disposition, target, trigger | Successful in-place engine restarts (fault recovery via REPLACE_THREAD, HA demote-in-place). |
High availability
Bound only when hot standby is enabled (ha.mode != off).
| Metric | Type | Level | Tags | Meaning |
|---|---|---|---|---|
stoatflow.ha.role | Gauge | info | — | 1 when this pod is the active, 0 when standby. |
stoatflow.ha.standby.replication.lag.records | Gauge | info | — | Standby replication lag in records (0 when active or caught up). |
stoatflow.ha.standby.replication.applied.records | Counter | info | — | Changelog records the standby applier has applied. rate() gives standby replication throughput — a flat line while lag is non-zero means the applier is stuck. |
stoatflow.ha.standby.replication.lag.ms | Gauge | info | — | Standby replication lag in milliseconds. |
stoatflow.ha.peer.freshness.ms | Gauge | info | — | Time since the freshest observed peer's last heartbeat. |
stoatflow.ha.peers | Gauge | info | — | Number of observed HA peers. |
stoatflow.ha.redundancy.ready.standbys | Gauge | info | — | Caught-up (READY_STANDBY) standby count, including self. |
stoatflow.ha.redundancy.below.desired | Gauge | info | — | 1 when the active has fewer ready standby spares than desired-standbys. |
stoatflow.ha.token.epoch | Gauge | info | — | Current promotion-token epoch (increments once per election). |
stoatflow.ha.election.outcome | Counter | info | outcome | Cumulative election outcomes (won / reclaimed / stood_down_fresh / stood_down_lost). |
stoatflow.ha.election.latency.ms | Gauge | info | — | Last claim→ACTIVE election latency in ms (-1 until the first promotion). |
stoatflow.ha.restart.required | Gauge | info | — | 1 when this pod requires a restart (e.g. source-topic partition count changed). |
stoatflow.ha.restarting | Gauge | info | — | 1 while this pod is restarting its engine in place to absorb a processing fault (it stays ACTIVE, so stoatflow.ha.role does not move). |
License metrics
When a license is configured, the runtime binds license meters to the same registry (see License configuration). These use the <prefix>.license prefix (default stoatflow.license, tracking runtime.metrics.prefix) and carry the registry common tags (application, plus your common-tags). They're exported even when recording-level is info.
| Metric | Type | Tags | Meaning |
|---|---|---|---|
stoatflow.license.valid | Gauge | tier | 1 when operating under a VALID or GRACE_PERIOD license, else 0 — including the deferred-validation PENDING window (the first ~5 minutes after every start). |
stoatflow.license.expiry_timestamp_seconds | Gauge | — | Unix epoch seconds of the entitlement deadline. |
stoatflow.license.days_remaining | Gauge | — | Whole days until expiry; negative within the post-expiry grace window. |
stoatflow.license.grace_remaining_seconds | Gauge | — | Seconds left in the offline-grace budget; 0 when exhausted. |
stoatflow.license.heartbeat_consecutive_failures | Gauge | — | Heartbeat ticks failed since the last success; resets to 0 on success. |
stoatflow.license.heartbeat_last_success_timestamp_seconds | Gauge | — | Unix epoch seconds of the most recent successful heartbeat. |
stoatflow.license.validations_total | Counter | result | Validation outcomes, by result label — success, authoritative reject codes, plus the deferred-validation outcomes network_failure, environment_required, and unexpected_error. |
stoatflow.license.heartbeats_total | Counter | result | Heartbeat tick outcomes, by result label. |
# Alert when the license is not valid, or when expiry is within 14 days.
# Use a `for:` clause of at least 10m on the valid==0 rule: every deploy
# reads 0 for the ~5-minute deferred-validation PENDING window.
stoatflow_license_valid == 0
stoatflow_license_days_remaining < 14
/metrics only from your in-cluster monitoring stack, not through internet-facing ingress. See Kubernetes.JVM and Kafka-client metrics
With bind-jvm-metrics: true (the default), the runtime binds Micrometer's standard JVM binders to the registry, so the usual jvm_* and system_* series appear on the same endpoint:
jvm_memory_used_bytes/jvm_memory_max_bytes(heap and non-heap)jvm_gc_pause_secondsand related GC metricsjvm_threads_live_threads,jvm_threads_states_threadsjvm_classes_loaded_classessystem_cpu_usage,process_cpu_usage,system_load_average_1m
These carry the registry common tags (application + your common-tags) but not application_id. Native Kafka client metrics are also surfaced through Micrometer, so producer/consumer client internals appear alongside the StoatFlow meters.
Dashboards and alerts
A starting overview combines throughput, latency, backpressure, and commit health:
# Throughput
sum(rate(stoatflow_lane_records_processed_total[1m]))
# Backpressure — total queued vs. capacity
sum(stoatflow_lane_queue_size) / sum(stoatflow_lane_queue_capacity)
# Commit health
rate(stoatflow_barrier_completed_total[1m])
rate(stoatflow_barrier_failed_total[5m])
# Event-time lag
stoatflow_watermark_lag_ms
Useful alert conditions:
| Alert | Condition | Severity |
|---|---|---|
| Barrier failures | rate(stoatflow_barrier_failed_total[5m]) > 0 | Critical |
| License invalid | stoatflow_license_valid == 0 (for: 10m — every deploy reads 0 during the ~5-min PENDING window) | Critical |
| High consumer lag | max(stoatflow_consumer_lag_records) > 100000 | Warning |
| Watermark stall | delta(stoatflow_watermark_current[5m]) == 0 and stoatflow_client_state == 4 | Warning |
| Backpressure | sum(stoatflow_lane_queue_size) > 0.8 * sum(stoatflow_lane_queue_capacity) | Warning |
| Timer lane mismatch | increase(stoatflow_timer_lane_mismatch_total[5m]) > 0 | Critical |
Next steps
- Observability — wiring
/metrics, logs, and probes into a monitoring stack. - Health checks — the readiness and liveness signals that complement metrics.
- Tuning — which bound knobs the self-tuning cadence respects.
- The REST API — the other operational endpoints on the same HTTP server.
Health checks
Liveness and readiness probes on the StoatFlow runtime — the built-in indicators, what flips each, and how they behave during state restoration.
Pause and resume
Pause and resume record processing at runtime with POST /pause and /unpause — for maintenance windows and downstream backpressure, without restarting the instance.