Building a Cache Hit-Ratio SLO Dashboard

A hit-ratio number on a wall dashboard tells you almost nothing on its own: 0.94 might be excellent for one workload and a fire for another, and staring at the instantaneous value gives no sense of whether you are spending your reliability budget faster than you can afford. This page turns the raw keyspace_hits/keyspace_misses counters into a proper service-level objective: define the hit ratio as an SLI, pick an SLO target and a rolling window, precompute the ratio with Prometheus recording rules, express reliability as an error budget, and drive Grafana panels plus multi-window burn-rate alerts that page only when the budget is genuinely at risk. Everything is derived from the same two counters you already scrape for eviction and miss alerting, so the dashboard reuses infrastructure you have running.

Prerequisites

  • Redis 6.2+ or 7.x with keyspace_hits and keyspace_misses exposed through a Prometheus exporter as counters.
  • Prometheus 2.40+ with a recording-rules file and Grafana 9+ for the panels.
  • redis-py 5.x on Python 3.10+ (pip install "redis>=5,<6") if you sample the ratio from application code as a cross-check.
  • Agreement from stakeholders on what a hit-ratio miss costs — database load, tail latency — so the SLO target reflects a real budget, not a guess.
  • A steady-state baseline: at least a week of hit-ratio history so the target is grounded in measured behavior.

Step-by-Step Implementation

1. Define the SLI as a ratio of good events to total events

State the indicator precisely as good-over-total so it is unambiguous: a "good" read is a hit, the total is hits plus misses, and the SLI is the fraction of reads served from cache over a window.

# SLI: fraction of reads served from cache. clamp_min avoids 0/0 when idle.
sum(rate(redis_keyspace_hits_total[5m]))
  /
clamp_min(
  sum(rate(redis_keyspace_hits_total[5m])) + sum(rate(redis_keyspace_misses_total[5m])),
  1)

2. Pick an SLO target and a rolling window

Choose a target that sits just below your measured steady state and a compliance window long enough to smooth normal variance — a 99.0% cache-hit SLO over a rolling 30 days is a common starting point for a read-heavy service.

# slo.yaml — the objective the dashboard and alerts are measured against.
slo:
  name: redis-cache-hit-ratio
  objective: 0.990          # 99.0% of reads should be cache hits
  window: 30d               # rolling compliance window
  error_budget: 0.010       # 1 - objective; the miss fraction you can spend

3. Precompute the SLI with recording rules

Evaluating the ratio ad hoc on every dashboard refresh is expensive and inconsistent, so materialize the good, total, and ratio series as recording rules that Prometheus computes once per evaluation interval.

groups:
  - name: redis-slo-recording
    interval: 30s
    rules:
      - record: redis:reads_good:rate5m
        expr: sum(rate(redis_keyspace_hits_total[5m]))
      - record: redis:reads_total:rate5m
        expr: |
          sum(rate(redis_keyspace_hits_total[5m]))
            + sum(rate(redis_keyspace_misses_total[5m]))
      - record: redis:hit_ratio:ratio5m
        expr: |
          redis:reads_good:rate5m
            / clamp_min(redis:reads_total:rate5m, 1)

4. Express reliability as an error budget

Reframe misses as budget spend: the error budget is 1 - objective, and the fraction of it consumed over the window is what tells you whether you have room to absorb a bad day or are already overdrawn.

# Miss ratio actually observed over the 30d window.
1 - (
  sum(rate(redis_keyspace_hits_total[30d]))
  / clamp_min(
      sum(rate(redis_keyspace_hits_total[30d]))
        + sum(rate(redis_keyspace_misses_total[30d])), 1))

# Budget consumed = observed miss ratio / allowed miss ratio (0.010).
# A value >= 1 means the 30d error budget is fully spent.

5. Compute the burn rate

Burn rate is how many times faster than sustainable you are spending the budget: a burn rate of 1 exhausts the whole 30-day budget exactly at day 30, while a burn rate of 14 would exhaust it in roughly two days.

# Burn rate over a 1h window: observed miss ratio / error budget (0.010).
(
  1 - (
    sum(rate(redis_keyspace_hits_total[1h]))
    / clamp_min(
        sum(rate(redis_keyspace_hits_total[1h]))
          + sum(rate(redis_keyspace_misses_total[1h])), 1))
) / 0.010

6. Build the Grafana panels

Assemble the dashboard from three complementary panels so a viewer reads current health, trend, and budget in one glance: a stat panel for the live ratio, a time series for the SLI against the target line, and a bar gauge for remaining budget.

{
  "panels": [
    {"type": "stat", "title": "Hit ratio (5m)",
     "targets": [{"expr": "redis:hit_ratio:ratio5m"}],
     "fieldConfig": {"defaults": {"min": 0.9, "max": 1,
       "thresholds": {"steps": [
         {"value": null, "color": "red"},
         {"value": 0.99, "color": "green"}]}}}},
    {"type": "timeseries", "title": "SLI vs 99.0% target",
     "targets": [{"expr": "redis:hit_ratio:ratio5m"}]},
    {"type": "bargauge", "title": "30d error budget remaining",
     "targets": [{"expr": "1 - ((1 - (sum(rate(redis_keyspace_hits_total[30d])) / clamp_min(sum(rate(redis_keyspace_hits_total[30d])) + sum(rate(redis_keyspace_misses_total[30d])), 1))) / 0.010)"}]}
  ]
}

7. Add multi-window burn-rate alerts

Fire on the combination of a fast and a slow window so a brief dip does not page but a sustained overspend does: require a high burn rate over both a long and a short window before alerting, which catches real budget threats while ignoring transient noise.

      - alert: RedisHitRatioFastBurn
        expr: |
          (redis:burn_rate:1h > 14) and (redis:burn_rate:5m > 14)
        for: 2m
        labels: { severity: page }
        annotations:
          summary: "Cache hit-ratio budget burning 14x — 2% of 30d budget/hour"
      - alert: RedisHitRatioSlowBurn
        expr: |
          (redis:burn_rate:6h > 6) and (redis:burn_rate:30m > 6)
        for: 15m
        labels: { severity: ticket }
        annotations:
          summary: "Cache hit-ratio budget burning 6x over 6h — investigate"
SLO dashboard panels and multi-window burn-rate alerting Top: three dashboard panels — a live hit-ratio stat, an SLI time series against a 99 percent target line, and a 30-day error-budget bar gauge. Bottom: two burn-rate alert lanes. The fast lane pages when a 1-hour window and a 5-minute window both exceed 14x burn. The slow lane files a ticket when a 6-hour window and a 30-minute window both exceed 6x burn. DASHBOARD Hit ratio (5m) 0.994 target 0.990 SLI vs target 99% 30d budget left 65% remaining MULTI-WINDOW BURN 1h > 14x long window AND 5m > 14x short window PAGE fast burn 6h > 6x long window AND 30m > 6x short window TICKET slow burn WHY TWO WINDOWS Short window stops the alert fast once the incident clears. Long window rejects a one-off single-scrape blip. Both must exceed the burn to fire.

Failure Modes

Cold-start skew poisons the ratio. Right after a deploy or restart, the cache is empty, so the first minutes are almost all misses and the SLI dives even though nothing is wrong. Diagnose by correlating the dip with process uptime:

redis-cli INFO server | grep uptime_in_seconds   # low uptime -> warm-up, not a regression
redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"

Fix by excluding a warm-up window from the SLO — gate the recording rule on redis_uptime_seconds > 600 — or pre-warm hot keys during deployment so the ratio never dips below target on a healthy start.

A recording-rule window that fights the alert window. The recording rule computes the ratio over 5m but a burn alert reasons over 1h, and mixing incompatible windows produces a burn rate that lags or overshoots reality. Diagnose by querying the recorded series directly and comparing it to the ad-hoc expression:

curl -s 'http://prometheus:9090/api/v1/query?query=redis:hit_ratio:ratio5m'

Fix by recording a distinct series per burn window (ratio1h, ratio6h) rather than reusing a single 5m rule everywhere, so each alert reads a window that matches its intent.

Miss surge from stampede misread as an SLO breach. A cache stampede driven by synchronized expiry spikes misses briefly and burns budget fast, but the fix is on the invalidation side, not the SLO target. Diagnose by checking whether the miss spike tracks expiry:

redis-cli INFO stats | grep -E "expired_keys|keyspace_misses"

If misses track expired_keys, jitter the TTLs per TTL vs Explicit Invalidation instead of loosening the objective.

Verification

Confirm the recording rules are materialized and returning sane values:

curl -s 'http://prometheus:9090/api/v1/query?query=redis:hit_ratio:ratio5m' | grep -o '"value".*'
promtool check rules redis-slo-recording.yml

Cross-check the Prometheus SLI against a direct sample from the instance so the exporter is not silently drifting:

redis-cli INFO stats | awk -F: '/keyspace_hits/{h=$2} /keyspace_misses/{m=$2} END{printf "hit ratio: %.4f\n", h/(h+m)}'

Exercise the burn-rate alert with a unit test so a threshold typo never ships:

promtool test rules redis-slo-burn.test.yml   # asserts page/ticket fire at 14x and 6x

FAQ

What hit-ratio target should the SLO use?

Set it just under your measured steady state, never a round number pulled from memory. Collect at least a week of the SLI, look at where it normally sits and how far it dips during ordinary traffic swings, then place the objective below that natural floor so routine variance keeps you in compliance while a genuine regression breaches it. A read-heavy catalog might justify 99.5%, whereas a write-heavy store that legitimately misses more often might land at 96% — the right target is the one that makes a breach mean something.

How long should the rolling window be?

The compliance window trades responsiveness against stability. A 30-day rolling window is the common default: long enough that a single bad hour does not blow the whole budget, short enough that a persistent regression still surfaces within the period. Shorter windows (7 days) react faster but make the budget volatile and can turn one incident into a breach; longer windows (90 days) are steadier but forgive sustained degradation for too long. Pair the long compliance window with the short burn-rate windows in the alerts so you get both stability and fast detection.

Why does the ratio crater right after a deploy?

Because the cache starts cold. A freshly restarted instance has an empty keyspace, so nearly every read misses until the working set is repopulated, and the SLI dips sharply for the first few minutes regardless of health. Exclude a warm-up window from the SLO by gating the recording rule on uptime, or pre-warm the hottest keys during rollout, so a normal deploy is not scored as an outage.

Why alert on burn rate instead of the SLI crossing the target?

A raw threshold alert on the SLI fires the instant the ratio dips below target, which is both too noisy for a brief dip and too slow to convey urgency. Burn rate measures how fast you are consuming the error budget, so a multi-window alert can distinguish a 14x fast burn that will exhaust a month of budget in two days — page now — from a mild 2x drift that only warrants a ticket. Requiring a long and a short window to agree suppresses single-scrape noise while still catching a real, sustained overspend quickly.


Up: Monitoring and Alerting for Redis Caches