📑 Contents
Overview / TL;DR How Alerting Works Anatomy of an Alert Rule States, Evaluation & Timing Symptom vs Cause SLO-Based Burn Rate Alerts Unleash-Specific Rules Notification & Routing Grafana vs Alertmanager vs SaaS Best Practices & Checklist ReferencesGrafana Alerting — Deep Study
Unified alerting for services backed by Unleash (open-source feature management): alert-rule design, evaluation & states, SLO burn-rate alerts, and the specific rules that keep a feature-flag service from going down.
1. Overview / TL;DR
Grafana Alerting (a.k.a. "unified alerting", introduced in Grafana 8) is the alerting engine built into Grafana. It lets you define alert rules against any data source (Prometheus, Mimir, Loki, Postgres, etc.), evaluate them on a schedule, and route notifications through contact points — Slack, PagerDuty, email, webhooks — with one consistent rule model and one routing tree. Under the hood it embeds Prometheus Alertmanager, so the routing/silencing/dedup semantics you know from the Prometheus ecosystem apply unchanged.
Core idea: alert on symptoms your users care about (error rate, latency, availability), not on causes (CPU %, disk space) — and for a service that depends on Unleash, treat "can my app still evaluate feature flags?" as a first-class availability signal. The goal is to page a human before an outage, not after it.
| Piece | Job |
|---|---|
| Alert rule | A query + threshold + "for" duration; defines what to detect (e.g. error rate > 1% for 5 min) |
| Evaluation engine (ruler) | Runs rules on a schedule, holds state (Normal → Pending → Alerting) |
| Alertmanager (embedded) | Deduplicates, groups, and routes firing alerts to the right contact points |
| Contact point | A notification destination: Slack, PagerDuty, email, webhook, Telegram, Discord… |
| Notification policy | A label-matching routing tree that decides which contact point (and on-call rotation) receives an alert |
| Silence / mute timing | Suppresses alerts during a window (maintenance, known incident, "quiet hours") |
Alerting only works if you also handle the NoData and Error states of a rule. A monitoring system that silently stops scraping (or an Unleash Edge that stops emitting metrics) produces no "high error rate" alert — so you must explicitly alert on "no data" and "target down", or you'll be blind exactly when things break.
2. How Grafana Alerting Works
Grafana Alerting is not a single process — it's a pipeline with three stages, each with a clear responsibility. Understanding the stages is what lets you debug "why didn't I get paged":
for duration elapses with the condition still true,
it becomes Alerting (firing) and an alert is handed to Alertmanager.There are two places a rule can live, and the choice matters for reliability:
Stored in Grafana's database, evaluated by Grafana's own ruler. Easiest to author (UI or Terraform), works against any data source, and keeps everything in one place — but if Grafana itself is down, these rules don't evaluate.
Stored and evaluated inside Prometheus/Mimir (via their ruler API). They keep firing even if the Grafana UI is down, and scale with Prometheus. Recommended for production PromQL alerts — Grafana just displays and manages them.
Rule of thumb: author in Grafana for convenience, but for the alerts that keep your service up, prefer data source–managed rules in Prometheus so evaluation is independent of the Grafana control plane. An alerting system whose own front-end can take down the pager defeats the purpose.
3. Anatomy of an Alert Rule
In Grafana's rule builder, a rule is three stacked expressions. In Prometheus (data source–managed) the same thing is written as a single PromQL alerting rule. Here is the mapping:
| Grafana expression | What it does | Prometheus equivalent |
|---|---|---|
| A — Query | The data query (PromQL, LogQL, SQL…) | the expr in an alerting rule |
| B — Reduce | Collapses the query to a single number (Last / Min / Max / Mean / Sum / Count) | a function like sum(), max(), quantile() |
| C — Threshold | IS ABOVE/BELOW X for for: 5m | the > 0.01 comparison + for: 5m |
groups:
- name: service-availability
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
> 0.01
for: 5m
labels:
severity: page
annotations:
summary: "High 5xx error rate on {{ $labels.service }}"
description: "{{ $value | humanizePercentage }} errors over the last 5m."
Every field matters:
expr— a PromQL expression that yields a vector; it's "firing" when it returns at least one element.for— the "debounce" duration. Prevents flapping: the condition must hold continuously for this long before paging. 5m is a common default; too short = alert storms, too long = you notice too late.labels— attached to the alert, drive routing (e.g.severity=pagevsseverity=ticket).annotations— human-readable text with Go-template variables ({{ $labels.* }},{{ $value }}) shown in the notification and dashboard.
Put a runbook URL in the annotation (runbook_url) and have your
notification template link to it. The single biggest factor in whether a 3am page gets resolved
fast is whether the on-call engineer has a doc telling them exactly what to do.
4. Alert States, Evaluation & Timing
A rule is always in one of a few states, and — crucially — not only "on/off". The two extra states are where most monitoring setups silently fail:
| State | Meaning | When it happens |
|---|---|---|
| Normal | Condition is false | Healthy — nothing to do |
| Pending | Condition true, but for not yet elapsed | A blip; will fire if it persists |
| Alerting | Condition true for the full for duration | Alert fired to Alertmanager |
| NoData | Query returned no time series | Target stopped emitting metrics, label mismatch, scrape down |
| Error | Query failed to execute | Syntax error, data source down, timeout |
By default, NoData and Error do not fire an alert — they just leave the rule stuck. For availability-critical rules you must explicitly set their behavior (usually "Alerting") in the rule's "No Data / Error handling" section, so a silent scrape failure or a broken query pages you instead of quietly going blind.
Evaluation interval, group & jitter
- Evaluation interval — how often the rule runs (default 1m). Lower = faster detection, higher = more load on the data source.
- Evaluation group — rules in the same group are evaluated sequentially with the same interval; a slow rule in a group delays its siblings. Keep heavy/slow rules in their own group.
- Jitter — a small random offset Grafana adds so every rule doesn't hit the data source at the exact same instant.
- Pending ≠ silent — the "for" window is where flapping gets absorbed. A 5m "for" on a 1m evaluation interval means ~5 evaluations must all be true before you page.
5. Alerting Philosophy: Symptom vs Cause
The most common reason alerting "doesn't help" is that it fires on the wrong things and trains everyone to ignore it. Google's SRE book distills this into one rule:
Alert on symptoms, not causes. A symptom is something the user experiences (slow responses, 5xx errors, feature flag lookups failing). A cause is an internal implementation detail (high CPU, full disk, a specific DB query). Causes are for dashboards and debugging, not for waking people up.
Two methods give you a vocabulary of "symptoms" for any service:
Rate (requests/sec), Errors (5xx rate), Duration (latency p99). The standard for HTTP/gRPC services — matches your Unleash SDK traffic and every API you run.
Utilization, Saturation, Errors of internal resources — CPU, memory, DB pool, queue depth. Good for infrastructure you own like the Unleash server's Postgres.
| Severity | Trigger | Who | Channel |
|---|---|---|---|
| PAGE | User-facing outage or imminent SLO breach | On-call, immediately | PagerDuty / phone / push |
| TICKET | Degradation, trend, or cause that needs attention soon | Team during work hours | Slack / Jira |
| LOG | Informational — capacity, slow drift | Nobody (dashboards) | No notification |
Alert fatigue is the real enemy. Every alert that fires and doesn't require action teaches the team to ignore the next one. If an alert can be answered by "oh, that again" — either fix the root cause, auto-remediate, or downgrade it. A short, high-signal pager is worth more than a wall of red.
6. SLO-Based Burn Rate Alerts
The most reliable way to "prevent downtime" is to alert on how fast you're burning your error budget, not on a single hard threshold. A burn rate is the ratio of your current error rate to your allowed error budget. If you have a 99.9% availability SLO, your error budget is 0.1%, and a burn rate of 1 means you're consuming budget at exactly the rate you can afford over 30 days.
Why burn rates beat plain thresholds: a plain "error rate > 1%" alert can't tell the difference between "1% errors for 5 minutes" (a rounding error) and "1% errors all day" (a real problem). Burn-rate alerts fire only when the error rate is high and sustained long enough to actually threaten the SLO — so they page early on real incidents and stay quiet on noise.
The standard recipe is the multi-window, multi-burn-rate alert (Google SRE): fire only when BOTH a short window shows a fast burn AND a long window confirms it's not a transient blip. For a 99.9% SLO (error budget 0.1%):
| Severity | Short window | Burn rate | Long window | Burn rate |
|---|---|---|---|---|
| Page | 5 min | 14.4× (error ratio > 1.44%) | 1 hour | 6× (error ratio > 0.6%) |
| Ticket | 30 min | 6× | 6 hours | 1× (error ratio > 0.1%) |
groups:
- name: slo-burn-rate
rules:
# Error ratio over the last 5 minutes
- record: job:error_ratio_5m
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
# Error ratio over the last 1 hour
- record: job:error_ratio_1h
expr: |
sum(rate(http_requests_total{status=~"5.."}[1h]))
/ sum(rate(http_requests_total[1h]))
# PAGE: fast burn (5m) AND sustained burn (1h)
- alert: SLOBurnRateHigh
expr: |
job:error_ratio_5m > 14.4 * 0.001
and
job:error_ratio_1h > 6 * 0.001
for: 2m
labels:
severity: page
annotations:
summary: "Error budget burning too fast ({{ $labels.job }})"
description: "5m error ratio {{ $value | humanizePercentage }}."
The magic numbers: 14.4× burns the entire 30-day budget in ~1 hour, and 6×
over 1h means the incident has already consumed a meaningful slice of budget. The and
of two windows is what makes the alert both fast to fire and resistant to flapping
— the short window catches the spike, the long window vetoes it if it was a 30-second blip.
Do the same for latency, not just errors: a service that gets 100× slower without erroring is just as "down" from a user's perspective. Track a latency SLO (e.g. p99 < 300ms over 30 days) and burn-rate-alert on the fraction of requests over the threshold.
7. Unleash-Specific Alert Rules
The specific risk with Unleash is silent staleness. Unleash SDKs cache the last-known flag state locally, so a brief outage usually doesn't crash your services — they keep serving with stale flags. That's a feature (availability), but it means the failure mode is invisible: your app is up, but flag changes stop propagating, and a brand-new instance with an empty cache can't evaluate flags at all. Your alerts must catch that degradation before it becomes a real incident.
Unleash's SDKs are designed to degrade gracefully, not fail closed. Assume your service stays "up" when Unleash is down — and that the danger is the drift between what your apps think the flags are and what they actually are. Alert on the health of the Unleash control plane and on your apps' ability to reach it.
7.1 Target down — the foundation
The single most important alert. Prometheus's up metric is 1 when a scrape succeeded,
0 when it didn't. If your Unleash server or Edge stops answering scrapes, everything else is moot:
groups:
- name: unleash-health
rules:
- alert: UnleashTargetDown
expr: up{job=~"unleash-server|unleash-edge"} == 0
for: 2m
labels:
severity: page
annotations:
summary: "Unleash target {{ $labels.job }} is not scraping"
description: "Prometheus cannot reach {{ $labels.instance }}."
7.2 Edge can't reach the Unleash server
Your Unleash Edge proxies flag evaluations to the server. If Edge's upstream connection breaks, Edge falls back to cache — but that cache is now going stale. This is the earliest warning of the "flags stopped updating" failure mode. (Metrics below match the Edge metrics already covered in the Unleash Edge notes.)
- alert: UnleashEdgeUpstreamErrors
expr: rate(edge_upstream_errors_total[5m]) > 0
for: 5m
labels:
severity: page
annotations:
summary: "Unleash Edge cannot reach the upstream server"
description: "{{ $value | humanize }} upstream errors/sec over 5m."
7.3 Cache hit rate dropping
A healthy Edge serves most flag evaluations from its cache (high hit rate). A falling hit rate means Edge is hitting the server more — either the cache is being evicted, keys are churning, or upstream is flapping. Sustained low hit rate is a leading indicator of upstream overload:
- alert: UnleashEdgeLowCacheHitRate
expr: |
sum(rate(edge_cache_hit_total[5m]))
/ (sum(rate(edge_cache_hit_total[5m])) + sum(rate(edge_cache_miss_total[5m])))
< 0.95
for: 10m
labels:
severity: ticket
annotations:
summary: "Unleash Edge cache hit rate below 95%"
description: "Cache hit rate is {{ $value | humanizePercentage }}."
7.4 Flag evaluation latency
If flag lookups get slow, every request that depends on a feature flag gets slow. Edge should answer flag evaluations in single-digit milliseconds; a p99 over ~10ms is a real regression:
- alert: UnleashEdgeHighLatency
expr: |
histogram_quantile(0.99,
sum(rate(edge_request_duration_seconds_bucket[5m])) by (le))
> 0.010
for: 5m
labels:
severity: ticket
annotations:
summary: "Unleash Edge p99 latency above 10ms"
description: "p99 is {{ $value | humanizeDuration }}."
7.5 Your apps can't evaluate flags (the one that actually pages)
The real "is my service down?" signal is measured from your app, not from Unleash. Expose (or scrape) the SDK's own error/evaluation counters and alert when flag evaluation starts failing:
- alert: FeatureFlagEvaluationFailing
expr: |
sum(rate(unleash_sdk_evaluation_errors_total[5m])) by (service)
/ sum(rate(unleash_sdk_evaluation_total[5m])) by (service)
> 0.01
for: 5m
labels:
severity: page
annotations:
summary: "{{ $labels.service }} cannot evaluate feature flags"
description: "{{ $value | humanizePercentage }} of flag evaluations failing."
Exact metric names vary by SDK language and Unleash version. The safe workflow: point Prometheus
at the Unleash server / Edge /internal-backstage/prometheus endpoint, open Grafana's
Explore, and confirm the actual metric names for your version before locking in a rule. The
shape of the alerts above (down / upstream / cache / latency / SDK errors) is what matters.
7.6 The full Unleash alert ladder
| Layer | Alert | Severity | Catches |
|---|---|---|---|
| Control plane | UnleashTargetDown | page | Server/Edge unreachable |
| Connectivity | UnleashEdgeUpstreamErrors | page | Edge→server link broken |
| Cache | UnleashEdgeLowCacheHitRate | ticket | Stale cache, churn, upstream load |
| Performance | UnleashEdgeHighLatency | ticket | Slow flag lookups |
| Application | FeatureFlagEvaluationFailing | page | Your app can't get flags |
| Budget | SLOBurnRateHigh | page | Overall availability at risk |
8. Notification, Routing & Silencing
Once an alert fires, Grafana's embedded Alertmanager decides who hears about it and how. Two concepts do the heavy lifting:
Named notification destinations: Slack channel, PagerDuty service, email address, webhook, Telegram, Discord, Opsgenie, and more. Each holds its own credentials and message settings.
A label-matching routing tree. The root policy catches everything, then nested policies route
by severity, service, team, etc. — "severity=page → PagerDuty",
"team=backend → #backend-alerts".
Grouping & dedup: Alertmanager collapses N firing alerts sharing labels into
one notification. Without this, a single Unleash server outage that takes down 20 services
produces 20 pages. Configure group_by (e.g. by alertname or
service) so related alerts arrive as one digest.
Silences & mute timings
- Silence — manually suppress alerts matching labels for a fixed window (during a
planned maintenance or a known incident). Created in the UI, API, or via
amtool. - Mute timing — a recurring schedule (e.g. "weekends", "after midnight") that routes non-critical alerts to a low-priority channel or drops them entirely.
- Inhibition — one alert suppresses another (e.g. "host down" inhibits all the "service down" alerts on that host), cutting noise at the source.
{{ define "slack.message" }}
*[{{ .Status | toUpper }}]* {{ .CommonLabels.alertname }}
{{ range .Alerts }}
• {{ .Annotations.summary }} ({{ .Labels.service }})
{{ .Annotations.description }}
{{ end }}
{{ end }}
9. Grafana Alerting vs Alertmanager vs SaaS
- Unified rules across all data sources
- UI authoring + Terraform provisioning
- Embedded Alertmanager for routing
- Grafana-managed or data source–managed rules
- Best when Grafana is already your pane of glass
- The original, most mature stack
- Rules in PromQL YAML, GitOps-friendly
- Evaluates independently of any UI
- No built-in rule editor (historically)
- Best for pure-Prometheus, code-first teams
- Zero-ops, hosted evaluation & storage
- Rich integrations & on-call tooling built in
- Per-host / per-GB pricing at scale
- Vendor lock-in, data egress costs
- Best if you don't want to run any of it
In practice these aren't mutually exclusive: the common pattern is Prometheus evaluates the rules (data source–managed), Grafana manages & visualizes them, and PagerDuty/Opsgenie handles on-call escalation as the last-hop contact point. That gets you GitOps-able rules, a nice UI, and real on-call scheduling without betting on one vendor.
10. Best Practices & Pre-Launch Checklist
Before you call your alerting "done", walk this checklist. Every unchecked item is a way to be paged at 3am by something you can't act on — or worse, not paged at all.
Alert on symptoms (RED/USE) and SLO burn rate. Set NoData and Error handling to "Alerting" on critical rules. Attach a runbook URL to every page. Tier severity and route page → PagerDuty, ticket → Slack. Test contact points with the "Test" button.
Alerting on raw CPU/memory without a symptom. Thresholds with no "for" duration (flapping). One alert per affected service with no grouping (alert storms). Rules that fire on known-expected conditions (unactionable noise).
Alert on: target down, Edge upstream errors, cache hit rate, flag-eval latency, and app-side SDK errors. Remember the SDK fails open (stale flags), so your alerts watch the control plane and the app's ability to reach it, not just "is the process up".
Fire a test alert end-to-end (not just "Test" — actually breach a threshold on a staging service). Confirm PagerDuty/Slack receives it, the runbook link works, and silencing a fired alert stops notifications. Do this before you rely on it in production.
Done when: you have (1) target-down + NoData coverage, (2) RED/SLO alerts per service, (3) the Unleash ladder from §7, (4) severity-tiered routing with grouping, and (5) a tested end-to-end page with a runbook. That's the state where alerting actually prevents downtime instead of just documenting it.
References
-
1Grafana Alerting — official documentationThe authoritative reference for alert rules, contact points, notification policies, and the state machine.
-
2Alert rule fundamentals — Grafana docsThe query/reduce/threshold expression model, "for" duration, and evaluation semantics.
-
3Prometheus alerting rules — official docsThe PromQL alerting rule format (expr / for / labels / annotations) used by data source–managed rules.
-
4Alerting on SLOs — Google SRE BookThe canonical treatment of burn rates and the multi-window multi-burn-rate alerting approach.
-
5Alerting — Prometheus best practicesSymptom-vs-cause guidance, alert fatigue, and what to alert on.
-
6Unleash technical overview — official docsUnleash architecture (server, SDKs, Edge), health/metrics endpoints, and the fail-open cache behavior.
-
7Configuring Unleash — metrics & health endpointsPrometheus metrics endpoint and health-check configuration for the Unleash server.
-
8Unleash Edge — official docsEdge metrics (
edge_upstream_errors_total,edge_cache_hit_total,edge_request_duration_seconds) used in the Unleash alerts above.