Storage and Index Latency Metrics Reference

This reference catalogues the storage and index latency and throughput metrics worth alerting on in JanusGraph, pairing each one with the MBean it comes from, the range that counts as healthy, and the PromQL that turns a raw timer into a p99 or a rate. It exists to answer the question that stalls most incident reviews: given a slow traversal, which of the dozen janusgraph_* series actually localizes the fault to storage, to the index, or to the transaction layer. The reference sits under the Prometheus JMX Metrics reference, which covers how these beans are exposed; this page is the downstream catalogue you consult once the series are flowing. Adopt the metric set below as a unit — expose every metric, name it consistently, and attach an alert to each — because a partial set leaves exactly the blind spot an incident finds first.

The single most useful mental model is the write-path latency budget: a mutation’s total observed latency is the sum of the storage commit, the index dispatch, and the index refresh, and each metric below measures one slice of it.

The JanusGraph write-path latency budget: commit, dispatch, refresh A horizontal budget bar divides total write latency into three consecutive segments. The first segment, storage commit, is measured by the CQLStoreManager mutate timer and typically consumes the largest fixed share as the coordinator acknowledges the write at the chosen consistency level. The second segment, index dispatch, is measured by the IndexProvider bulk-flush timer and covers batching mutations and sending them to Elasticsearch or OpenSearch. The third segment, index refresh, is the eventually-consistent tail before a document becomes searchable, governed by the index refresh interval rather than a JanusGraph timer, and it is the part a client waiting on a has predicate actually feels. Below the bar, each segment is annotated with the metric that measures it and a healthy target. t = commit start searchable storage commit coordinator ack @ consistency level index dispatch batch + bulk send index refresh eventual — refresh interval janusgraph_store_operation_latency_seconds operation="mutate" · target p99 < 25 ms janusgraph_index_bulk_flush_seconds target p99 < 200 ms refresh_interval (index config) 1s default · felt by has() reads
Total write latency splits into storage commit, index dispatch, and index refresh; each JanusGraph metric measures exactly one segment, so a widened budget points straight at the responsible layer.

The metric set

Every metric below is a series the Prometheus JMX Metrics exporter emits after the rename ruleset runs. Healthy ranges assume a three-node CQL backend at LOCAL_QUORUM; treat them as starting baselines to tighten against your own SLO, not universal constants.

Metric MBean source What it means Healthy range p99 / rate PromQL
janusgraph_store_operation_latency_seconds{operation="getSlice"} CQLStoreManager getSlice timer Storage read latency per store p99 < 15 ms max by (store) (janusgraph_store_operation_latency_seconds{operation="getSlice",quantile="0.99"})
janusgraph_store_operation_latency_seconds{operation="mutate"} CQLStoreManager mutate timer Storage write/commit latency p99 < 25 ms max by (store) (janusgraph_store_operation_latency_seconds{operation="mutate",quantile="0.99"})
janusgraph_cql_pool_in_flight CQLStoreManager pool gauge Sockets checked out vs the ceiling < 80% of max janusgraph_cql_pool_in_flight / janusgraph_cql_pool_available
janusgraph_index_queue_size IndexProvider queue gauge Documents waiting to flush flat / near zero janusgraph_index_queue_size{backend="search"}
janusgraph_index_bulk_flush_seconds IndexProvider bulk-flush timer Time to send one bulk batch p99 < 200 ms janusgraph_index_bulk_flush_seconds{quantile="0.99"}
janusgraph_index_bulk_flush_seconds_count IndexProvider bulk-flush timer Flush throughput steady under load rate(janusgraph_index_bulk_flush_seconds_count[5m])
janusgraph_db_cache_retrievals_total / _misses_total StandardJanusGraph cache counters Database cache hit ratio ratio > 0.85 1 - rate(janusgraph_db_cache_misses_total[5m]) / rate(janusgraph_db_cache_retrievals_total[5m])
janusgraph_tx_rollback_total StandardJanusGraph tx meter Transaction abort rate < 1% of commits rate(janusgraph_tx_rollback_total[5m]) / rate(janusgraph_tx_commit_total[5m])

Two formulas underpin the table. The cache hit ratio over a window is one minus the miss rate divided by the retrieval rate:

H=1ΔmissesΔretrievalsH = 1 - \frac{\Delta_{\text{misses}}}{\Delta_{\text{retrievals}}}

The abort ratio, the fraction of transactions that rolled back rather than committed, is:

A=ΔrollbackΔcommit+ΔrollbackA = \frac{\Delta_{\text{rollback}}}{\Delta_{\text{commit}} + \Delta_{\text{rollback}}}

A cache hit ratio drifting below 0.85 or an abort ratio above 0.01 both widen the storage-commit segment of the budget bar, because a cache miss forces a storage read and an abort forces a full transaction replay.

Step 1 — Expose the metric set

Confirm every metric in the table is actually emitted before you build alerts on it — an alert on a series that never appears is a silent gap. The exporter must keep the storage, index, and cache beans, which the step-by-step exporter guide sets up in full.

bash
# Every metric name from the table must return at least one series
for m in janusgraph_store_operation_latency_seconds \
         janusgraph_index_queue_size \
         janusgraph_index_bulk_flush_seconds \
         janusgraph_db_cache_misses_total \
         janusgraph_tx_rollback_total; do
  n=$(curl -s http://localhost:9090/api/v1/query \
       --data-urlencode "query=$m" | jq '.data.result | length')
  printf "%-48s %s series\n" "$m" "$n"
done

Verify: every line reports a non-zero series count. A zero means the corresponding MBean is not being kept — recheck metrics.merge-stores=false and the includeObjectNames allow-list.

Step 2 — Name and normalize consistently

A metric set is only useful if the same concept has the same name and unit across every node. The two normalization rules that matter most are unit scaling and label consistency: all _seconds metrics must actually hold seconds, and the store and backend labels must be present so aggregation works.

text
# Latency in seconds, not milliseconds — a value near 1000 means a missing valueFactor
janusgraph_store_operation_latency_seconds{operation="mutate", quantile="0.99"}

# Confirm every store carries a label rather than collapsing to one aggregate
count by (store) (janusgraph_store_operation_latency_seconds)

Verify: the count by (store) query returns one row per real store (edgestore, graphindex, janusgraph_ids). A single unlabeled row means merge-stores is still on and the per-store view was lost upstream.

Step 3 — Attach an alert to each metric

Bind a threshold to every row so the set becomes actionable rather than decorative. Use the ratios from the formulas above for the cache and transaction alerts, and a duration guard so a single scrape spike does not page. These fire into the same on-call path described in Alert Routing for Violations.

yaml
# prometheus rules
groups:
  - name: janusgraph-latency
    rules:
      - alert: StorageWriteLatencyHigh
        expr: max by (store) (janusgraph_store_operation_latency_seconds{operation="mutate", quantile="0.99"}) > 0.025
        for: 10m
        labels: {severity: warning, tier: storage-backend}
        annotations:
          summary: "p99 storage write latency above 25ms on {{ $labels.store }}"

      - alert: IndexFlushBacklog
        expr: janusgraph_index_queue_size{backend="search"} > 5000
        for: 5m
        labels: {severity: warning}
        annotations:
          summary: "Mixed-index flush queue backing up — sync lag rising"

      - alert: CacheHitRatioLow
        expr: 1 - rate(janusgraph_db_cache_misses_total[5m]) / rate(janusgraph_db_cache_retrievals_total[5m]) < 0.85
        for: 15m
        labels: {severity: info}
        annotations:
          summary: "Database cache hit ratio below 0.85 — storage reads rising"

Verify: the rules load and evaluate without error.

bash
promtool check rules janusgraph-latency.rules.yml
curl -s http://localhost:9090/api/v1/rules | jq '.data.groups[] | select(.name=="janusgraph-latency") | .rules[].name'

Fallback and rollback procedures

Each step has a defined recovery path; validate between actions rather than stacking changes.

If Step 1 shows a zero series count. The metric is not being exported. Confirm the bean exists in jconsole, then check the includeObjectNames list actually names it and metrics.merge-stores=false is set. Do not build an alert on a metric that is not flowing — the alert will read as permanently healthy.

If Step 2 shows collapsed labels or wrong units. A count by (store) returning one row means merging is still on; fix merge-stores and restart the Gremlin Server. Latency values near 1000 mean the valueFactor is missing from the rename rule; add 0.001 for millisecond timers and re-scrape rather than compensating in every PromQL expression.

If Step 3 alerts flap. An alert that fires and clears repeatedly has too tight a threshold or too short a for. Lengthen the for window so a single slow scrape does not page, and widen the threshold toward your true SLO. For lag-based alerts, prefer a rate-of-change guard over a bare level so a slow steady queue does not alert while a genuinely growing one does.

To roll the metric set back. Remove the janusgraph-latency rule group and reload Prometheus; the series remain in the TSDB for historical queries while the alerts stop firing. Reverting the exporter ruleset is covered in the exporter guide’s rollback section.