Command Palette

Search for a command to run...

Hectal
Case 1.1·Metrics in AngerSEV3

“Right after the 15:00 deploy, the 'checkouts today' panel dropped from 41,207 to 312.”

case name: The deploy that 'lost' 40,000 orders

Service
grafana dashboard · shoplite-api
Impact
No customer impact; a false 'we lost orders' escalation that pulled in four engineers for 40 minutes
Detected by
The head of sales, watching the dashboard on the office TV
Time to resolve
40 min

Skills you'll use on this case

counters and resetsrate vs increase vs iraterange selection rulesresets() and process_start_time_seconds

00 It starts

Reproduce this incident in your lab, then work the case alongside the timeline below. Try each query yourself before reading its result.

terminal
$ docker compose restart shoplite
── lab output ──
✔ Container shoplite Started
A restart is all a deploy does to a counter: the new process starts counting from zero.

01 The investigation

  1. 15:03

    REPORT

    'Did we just lose forty thousand orders??'

    The panel's query is the raw counter value, the same thing Case 0.1 warned about.

    PromQL· Prometheus
    sum(http_request_duration_seconds_count{job="shoplite", route="/checkout", status="201"})
    result
    {}  312
    checkouts 'today' (raw counter)raw counter
    0.00233454669014:0015:30
  2. 15:06

    HYPOTHESIS

    Orders table in Postgres: 41,519 rows, still growing

    The business is fine. The NUMBER on the dashboard is wrong, so the question becomes: what does this metric actually measure?

  3. 15:14

    FINDING

    The counter lives in the process — and the process was replaced

    Prometheus client libraries keep counters in memory. A new process starts at 0. process_start_time_seconds (a default metric) changed at 15:00, and so did the counter.

    PromQL· Prometheus
    changes(process_start_time_seconds{job="shoplite"}[1h])
    result
    {instance="shoplite:8080", job="shoplite"}  1
  4. 15:20

    QUERY

    increase() over the day — resets handled

    increase(counter[24h]) computes how much the counter went up over the window. When it sees a value drop, it assumes a reset and adds the pre-reset value. The dashboard shows the right number again.

    PromQL· Prometheus
    sum(increase(http_request_duration_seconds_count{job="shoplite", route="/checkout", status="201"}[24h]))
    result
    {}  41519.6
  5. 15:43

    RESOLVED

    Panel fixed; escalation stood down

    Note the .6: increase extrapolates to the window edges, so it's an estimate, not an exact count. The concepts section explains why that's fine for dashboards but not for billing.

Root cause

The panel graphed a raw COUNTER value. Counters reset to zero whenever the process restarts (deploys, crashes, scaling), so any query that treats the raw value as a running total breaks on every deploy. rate() and increase() exist specifically to handle resets.

02 The concepts behind it

Counter, gauge, histogram — and what you may do with each

A COUNTER only goes up (requests served, bytes sent, errors), except when it resets to 0 on restart. Never graph it raw; always use rate() or increase(). A GAUGE goes up and down (memory in use, queue length, temperature); graph it raw, or use avg_over_time, max_over_time, and deriv. A HISTOGRAM is a bundle of counters (Case 0.2) and needs rate() first too.

Naming conventions make this visible: counters end in _total (or _count/_sum/_bucket for histograms). If you see rate() on something without those suffixes, it's probably a gauge being misused, and vice versa.

rate, irate, and increase

rate(x[5m]): average per-second increase over 5 minutes, using all samples in the window. Smooth, good for graphs and alerts. irate(x[5m]): per-second rate between just the LAST TWO samples. Very spiky, good only for fast-moving graphs of volatile data, and bad for alerts. increase(x[1h]): total increase over the window, which is rate × window seconds. Good for 'how many in the last hour'.

All three handle counter resets and extrapolate to the edges of the window, which is why increase can return fractional numbers for an integer counter. For exact counts (billing, audits), use a database, not Prometheus.

Choosing the range

A range must contain at least two samples for rate to work, and should comfortably contain several. The rule of thumb is at least 4× the scrape interval: with 15 s scrapes, [1m] is the minimum, and [5m] is common for alerts. Too short and a single missed scrape leaves gaps; too long and spikes are averaged away. Grafana's $__rate_interval variable picks a safe value automatically.

03 The fix

  1. 01Rewrite the panel with increase()

    For a 'today' panel, set the range to the dashboard's time range ($__range in Grafana) so the number always matches the selected period.

    PromQL· Prometheus
    sum(increase(http_request_duration_seconds_count{job="shoplite", route="/checkout", status="201"}[$__range]))

04 Make sure it never surprises you again

  1. 01Make restarts visible on every dashboard

    Add this as a Grafana annotation query. A vertical line at every process start makes 'that change happened at the deploy' obvious, and explains any step change in a graph at a glance.

    PromQL· Prometheus
    changes(process_start_time_seconds{job="shoplite"}[1m]) > 0
  2. 02Alert on crash-looping, which is resets you didn't plan

    A deploy restarts once. A crash loop restarts constantly. restart: unless-stopped in the lab (and ECS or Kubernetes in production) hides crashes by restarting, so alert on the restart RATE.

    obs-lab/prometheus/alerts.ymladd to fileyaml
          - alert: ShopLiteRestartingFrequently
            expr: changes(process_start_time_seconds{job="shoplite"}[15m]) > 3
            labels: { severity: warning, service: shoplite }
            annotations:
              summary: "shoplite restarted {{ $value }} times in 15 minutes"

05 Your turn: write the query

Run each one against your lab before revealing the answer. Share your own version in the comments; there's usually more than one correct query.

01

How many requests did payments serve in the last hour, in total?

02

Restart shoplite twice more. Write a query that returns how many times its counters reset in the last 30 minutes.

03

Graph irate and rate over [1m] for /products requests side by side. Describe the difference and say which you'd put in an alert.

06 Interview questions from this case

01

Why shouldn't you graph a Prometheus counter directly?

02

When would you use irate instead of rate?

0/4 · 0%