“ShopLite was completely down for 25 minutes. Not one alert fired. Every rule looked correct.”
case name: The alert that couldn't fire
- Service
- shoplite-api · alerting
- Impact
- Total outage for 25 minutes, detected by customers
- Detected by
- Customers
- Time to resolve
- 25 min outage; alert rules fixed the same day
Skills you'll use on this case
up and absent()NaN in ratiosunit-testing alert rules with promtool00 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
- 22:10
REPORT
Customers: the site returns 502 from the load balancer
The error-ratio and burn-rate alerts are perfectly written. They're just not firing.
- 22:14
QUERY
What does the SLI return right now?
Nothing: an empty result, not 0, not 1. With the process down, Prometheus has no fresh samples for
http_request_duration_seconds_count; after a few minutes those series go STALE and disappear from queries. A comparison on an empty vector is empty, and an empty alert expression means 'not firing'.PromQL· Prometheusslo:checkout_errors:ratio_rate5mresultEmpty query result - 22:16
QUERY
The one metric that DOES know
Prometheus records every scrape attempt.
upis 0 when the scrape fails.PromQL· Prometheusup{job="shoplite"}result{instance="shoplite:8080", job="shoplite"} 0 - 22:35
RESOLVED
Process restarted; ShopLiteDown and absence alerts added and unit-tested
Root cause
Every alert measured the SERVICE'S OWN metrics. When the service stopped, those metrics stopped existing, so every alert expression evaluated to 'no data', which Prometheus treats as not firing. There was no alert on the scrape itself (up) and none on metrics going absent.
02 The concepts behind it
No data is not zero
PromQL works on sets of series. If the series don't exist (process down, target removed, label renamed in a deploy, exporter crashed), expressions return an EMPTY result, and alert rules only fire for series that exist and match. Ratios have a second trap: 0 errors / 0 requests is NaN, and NaN > 0.05 is false. A service with no traffic looks perfectly healthy.
up, absent, absent_over_time
up{job="x"} == 0 catches targets Prometheus knows about but can't scrape. absent(up{job="x"}) returns a series when NO matching series exists at all, which catches a target removed from service discovery or a typo in a job name. absent_over_time(metric[10m]) catches a metric that silently stopped being reported while the process stays up (renamed metric, broken instrumentation).
Test alerts like code
promtool test rules feeds synthetic series into your rules and asserts which alerts fire at which times, with which labels and annotations. It's fast, runs in CI, and is the only practical way to know that an alert handles gaps, NaN, and edge cases BEFORE an outage tests it for you. Pair it with regular fire drills (trigger a scenario, confirm the page arrives), as you've done throughout this course.
03 The fix
01Alert on the scrape and on absence
ShopLiteDownpages; the Alertmanager inhibit rule from Case 4.1 then suppresses the noise from everything else about shoplite.ShopLiteMetricsAbsentcatches the rarer case where the target itself vanished.obs-lab/prometheus/alerts.ymladd to fileyaml - alert: ShopLiteDown expr: up{job="shoplite"} == 0 for: 1m labels: { severity: critical, service: shoplite } annotations: summary: "shoplite is down (scrape failing)" - alert: ShopLiteMetricsAbsent expr: absent(up{job="shoplite"}) for: 5m labels: { severity: critical, service: shoplite } annotations: summary: "No shoplite scrape target exists — removed from discovery or job renamed"terminal$ curl -s -X POST localhost:9090/-/reloadsleep 90 && curl -s localhost:9090/api/v1/alerts | jq -r '.data.alerts[] | .labels.alertname + " " + .state'── expected output ──ShopLiteDown firing02Bring it back
terminal$ docker compose start shoplite── expected output ──✔ Container shoplite Started
04 Make sure it never surprises you again
01Unit-test the rule with promtool
The test feeds
upvalues minute by minute (1, 1, then 0 from minute 2) and asserts that at minute 5 the alert is firing with exactly these labels and annotations. Add a test like this for every paging alert and runpromtool test rulesin CI.obs-lab/prometheus/alerts.test.ymlwhole fileyaml rule_files: [alerts.yml] evaluation_interval: 1m tests: - interval: 1m input_series: - series: 'up{job="shoplite", instance="shoplite:8080"}' values: '1 1 0 0 0 0 0' alert_rule_test: - eval_time: 1m alertname: ShopLiteDown exp_alerts: [] - eval_time: 5m alertname: ShopLiteDown exp_alerts: - exp_labels: severity: critical service: shoplite job: shoplite instance: "shoplite:8080" exp_annotations: summary: "shoplite is down (scrape failing)"terminal$ docker compose exec prometheus promtool test rules /etc/prometheus/alerts.test.yml── expected output ──Unit Testing: /etc/prometheus/alerts.test.ymlSUCCESS
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.
Rewrite the checkout error ratio so it returns 0 instead of an empty result when there are no errors but there IS traffic.
Write an alert for 'checkout traffic dropped to nothing', which is often the first sign of an upstream outage (DNS, load balancer, app store release).
06 Interview questions from this case
Why might an alert fail to fire during a total outage, and how do you prevent it?
How do you test Prometheus alerting rules?