📑 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 References

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.

PieceJob
Alert ruleA 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 pointA notification destination: Slack, PagerDuty, email, webhook, Telegram, Discord…
Notification policyA label-matching routing tree that decides which contact point (and on-call rotation) receives an alert
Silence / mute timingSuppresses 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":

  • 1
    Define & evaluate. A rule holds a query (e.g. a PromQL expression), a reduce expression, and a threshold. The ruler evaluates it every evaluation interval (default 1m) and records the current state.
  • 2
    State machine. When the condition is true, the rule enters Pending; after the for duration elapses with the condition still true, it becomes Alerting (firing) and an alert is handed to Alertmanager.
  • 3
    Route & notify. Alertmanager deduplicates identical alerts, groups them by labels, and walks the notification policy tree to pick contact point(s). It fires the configured integration (Slack, PagerDuty, webhook…).
  • There are two places a rule can live, and the choice matters for reliability:

    Grafana-managed rules

    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.

    Data source–managed rules

    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 expressionWhat it doesPrometheus equivalent
    A — QueryThe data query (PromQL, LogQL, SQL…)the expr in an alerting rule
    B — ReduceCollapses the query to a single number (Last / Min / Max / Mean / Sum / Count)a function like sum(), max(), quantile()
    C — ThresholdIS ABOVE/BELOW X for for: 5mthe > 0.01 comparison + for: 5m
    Prometheus alerting rule (data source–managed) — service error rate
    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=page vs severity=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:

    StateMeaningWhen it happens
    NormalCondition is falseHealthy — nothing to do
    PendingCondition true, but for not yet elapsedA blip; will fire if it persists
    AlertingCondition true for the full for durationAlert fired to Alertmanager
    NoDataQuery returned no time seriesTarget stopped emitting metrics, label mismatch, scrape down
    ErrorQuery failed to executeSyntax 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:

    RED (request-driven)

    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.

    USE (resource-driven)

    Utilization, Saturation, Errors of internal resources — CPU, memory, DB pool, queue depth. Good for infrastructure you own like the Unleash server's Postgres.

    SeverityTriggerWhoChannel
    PAGEUser-facing outage or imminent SLO breachOn-call, immediatelyPagerDuty / phone / push
    TICKETDegradation, trend, or cause that needs attention soonTeam during work hoursSlack / Jira
    LOGInformational — capacity, slow driftNobody (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%):

    SeverityShort windowBurn rateLong windowBurn rate
    Page5 min14.4× (error ratio > 1.44%)1 hour6× (error ratio > 0.6%)
    Ticket30 min6 hours1× (error ratio > 0.1%)
    Multi-window burn-rate alert (99.9% availability SLO)
    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 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:

    Unleash server / Edge target down
    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.)

    Edge upstream (Unleash server) errors
      - 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:

    Edge cache hit rate too low
      - 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:

    Edge flag-evaluation p99 latency
      - 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:

    App-side SDK flag-evaluation errors (RED-style)
      - 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

    LayerAlertSeverityCatches
    Control planeUnleashTargetDownpageServer/Edge unreachable
    ConnectivityUnleashEdgeUpstreamErrorspageEdge→server link broken
    CacheUnleashEdgeLowCacheHitRateticketStale cache, churn, upstream load
    PerformanceUnleashEdgeHighLatencyticketSlow flag lookups
    ApplicationFeatureFlagEvaluationFailingpageYour app can't get flags
    BudgetSLOBurnRateHighpageOverall 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:

    Contact points

    Named notification destinations: Slack channel, PagerDuty service, email address, webhook, Telegram, Discord, Opsgenie, and more. Each holds its own credentials and message settings.

    Notification policies

    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.
    Notification template snippet — human-readable Slack message
    {{ define "slack.message" }}
    *[{{ .Status | toUpper }}]* {{ .CommonLabels.alertname }}
    {{ range .Alerts }}
    • {{ .Annotations.summary }} ({{ .Labels.service }})
      {{ .Annotations.description }}
    {{ end }}
    {{ end }}

    9. Grafana Alerting vs Alertmanager vs SaaS

    📊 Grafana Alerting
    • 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
    🔥 Prometheus + Alertmanager
    • 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
    ☁️ SaaS (Datadog / CloudWatch / New Relic)
    • 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.

    ✅ Do

    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.

    ⚠️ Avoid

    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).

    🔔 For Unleash specifically

    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".

    🧪 Verify

    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