“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
rate vs increase vs iraterange selection rulesresets() and process_start_time_seconds00 It starts
Reproduce this incident in your lab, then work the case alongside the timeline below. Try each query yourself before reading its result.
01 The investigation
- 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· Prometheussum(http_request_duration_seconds_count{job="shoplite", route="/checkout", status="201"})result{} 312checkouts 'today' (raw counter)raw counter - 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?
- 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· Prometheuschanges(process_start_time_seconds{job="shoplite"}[1h])result{instance="shoplite:8080", job="shoplite"} 1 - 15:20
QUERY
increase()over the day — resets handledincrease(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· Prometheussum(increase(http_request_duration_seconds_count{job="shoplite", route="/checkout", status="201"}[24h]))result{} 41519.6 - 15:43
RESOLVED
Panel fixed; escalation stood down
Note the
.6:increaseextrapolates 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
01Rewrite the panel with increase()
For a 'today' panel, set the range to the dashboard's time range (
$__rangein Grafana) so the number always matches the selected period.PromQL· Prometheussum(increase(http_request_duration_seconds_count{job="shoplite", route="/checkout", status="201"}[$__range]))
04 Make sure it never surprises you again
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· Prometheuschanges(process_start_time_seconds{job="shoplite"}[1m]) > 002Alert on crash-looping, which is resets you didn't plan
A deploy restarts once. A crash loop restarts constantly.
restart: unless-stoppedin 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.
How many requests did payments serve in the last hour, in total?
Restart shoplite twice more. Write a query that returns how many times its counters reset in the last 30 minutes.
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
Why shouldn't you graph a Prometheus counter directly?
When would you use irate instead of rate?