Prometheus Alerting Rules for Tile Availability

This procedure builds the multi-window, multi-burn-rate Prometheus rules that turn the raw 200-response rate of an OGC API Tiles or MVT endpoint into a paging-grade availability SLO, with recording rules that keep the alert expressions cheap and a promtool gate that proves they fire before they ship. It is the concrete rule set behind the availability objective defined in SLA Monitoring for Spatial Data Products, under the broader Spatial Pipeline Orchestration & Observability practice, and it pairs naturally with Monitoring Vector Query p95 Latency so a domain covers both the availability and the latency halves of its tile contract. Get the burn-rate windows right and a genuine outage pages within minutes while a deploy blip stays silent.

Prerequisites

Requirement Value / Assumption Notes
Prometheus >= 2.45 with rule evaluation enabled Recording + alerting rule groups
Validation promtool (ships with Prometheus) check rules and test rules
Tile metric tile_requests_total{code,domain,layer} counter Emitted by the tile server exporter
CRS labels EPSG:4326 and EPSG:3857 layers labelled distinctly Isolates reprojection-path faults
SLO target 0.995 availability over 30d Error budget 0.005
Alert routing Alertmanager receiver keyed on severity, domain page vs ticket split
Access role prometheus-rules-editor (RBAC) Required to apply rule files
Scrape security mTLS to exporter targets Zero-trust scrape path

Figure — Two burn-rate windows must both breach before a page fires; a slow single-window breach opens a ticket instead.

Multi-window multi-burn-rate alert decision The availability error rate is evaluated over a fast 5-minute window and a confirming 1-hour window. When both exceed 14.4 times the budget the AND gate fires a page. A separate 6-hour window exceeding 1 times the budget opens a ticket without paging. 5m window error > 14.4 × budget 1h window error > 14.4 × budget AND both breaching PAGE on-call severity: page 6h window error > 1 × budget TICKET severity: ticket

Step-by-Step Implementation

Each step below produces a fragment of the rule file and a command that proves the fragment is correct before you build on it.

1. Record the availability ratio at two windows

The alert expressions stay cheap and readable only if the SLI ratio is pre-computed. Record the 200-rate over both the fast and the confirming window, grouped by domain and layer so a single hot layer can be isolated.

yaml
# tile-availability-rules.yaml
groups:
  - name: tile_availability_recording
    interval: 30s
    rules:
      - record: tile:availability:ratio_rate5m
        expr: |
          sum(rate(tile_requests_total{code="200"}[5m])) by (domain, layer)
          /
          sum(rate(tile_requests_total[5m])) by (domain, layer)
      - record: tile:availability:ratio_rate1h
        expr: |
          sum(rate(tile_requests_total{code="200"}[1h])) by (domain, layer)
          /
          sum(rate(tile_requests_total[1h])) by (domain, layer)

Verify: confirm the rule file parses and the recording rules are well-formed.

bash
promtool check rules tile-availability-rules.yaml

2. Add the fast-burn page rule

The page fires only when the 5-minute and 1-hour windows both breach 14.4 times the 0.005 budget, which suppresses single-scrape spikes while still catching a fast burn within minutes.

How long each burn rate takes to exhaust a 30-day 99.95 percent budgetFive burn rates and the time each takes to consume the full 21 minutes and 36 seconds of a 30-day 99.95 percent availability budget. A burn rate of 1 takes the whole 30 days, which is the definition of the budget. A rate of 6 exhausts it in five days. The fast-burn factor of 14.4 exhausts it in about 50 hours, which is why it pages. A rate of 30 exhausts it in a day and a rate of 100 in around seven hours. The two-window rule pages on the high rates and opens a ticket on the low ones, because a single threshold cannot separate a sustained slow leak from an active outage.burn 1× (the budget)720 hburn 6× (ticket)120 hburn 14.4× (page)50 hburn 30×24 hburn 100×7 h

yaml
  - name: tile_availability_alerts
    rules:
      - alert: TileAvailabilityFastBurn
        expr: |
          (1 - tile:availability:ratio_rate5m) > (14.4 * 0.005)
          and
          (1 - tile:availability:ratio_rate1h) > (14.4 * 0.005)
        for: 2m
        labels:
          severity: page
          slo: tile_availability
        annotations:
          summary: "Fast burn on {{ $labels.domain }}/{{ $labels.layer }} tiles"
          description: "Tile 200-rate breaching 99.5% budget at >14.4x."

Verify: re-check the combined file after adding the alert group.

bash
promtool check rules tile-availability-rules.yaml

3. Add the slow-burn ticket rule

A slow leak that would still exhaust the budget over the window should open a ticket, not wake anyone. Use a wider window at a burn rate of 1.

The two-window rule: a long window decides severity, a short one decides whether it is still happeningAvailability is recorded at two windows in parallel. The long window, one hour, establishes that the burn rate is genuinely high rather than a scrape artefact. The short window, five minutes, establishes that the condition is still current rather than a spike that has already recovered. Only when both agree does the rule page. Either window alone produces a known failure: the long window alone keeps paging for an outage that ended, and the short window alone pages on every deploy blip.Long window1h burn rateShort window5m burn rateBoth above thresholdANDPage on-callseverity: pageandevaluatefirelong only → pages after recoveryshort only → pages on every deployAlert nobody trustssilenced within a week

yaml
      - alert: TileAvailabilitySlowBurn
        expr: |
          (1 - avg_over_time(tile:availability:ratio_rate1h[6h])) > (1 * 0.005)
        for: 15m
        labels:
          severity: ticket
          slo: tile_availability
        annotations:
          summary: "Slow burn on {{ $labels.domain }}/{{ $labels.layer }} tiles"

Verify: confirm both alerts are registered in the group.

bash
promtool check rules tile-availability-rules.yaml | grep -E 'SUCCESS|FAILED'

4. Prove the rules fire with a unit test

A rule that parses but never evaluates true is worse than no rule. Drive a synthetic series through promtool test rules to assert the page fires on a crafted outage and the slow burn tickets on a steady leak.

yaml
# tile-availability-tests.yaml
rule_files:
  - tile-availability-rules.yaml
evaluation_interval: 30s
tests:
  - interval: 30s
    input_series:
      - series: 'tile_requests_total{code="200",domain="basemap",layer="roads"}'
        values: '0+5x120'          # steady successes then...
      - series: 'tile_requests_total{code="503",domain="basemap",layer="roads"}'
        values: '0+95x120'         # ...a heavy 503 flood
    alert_rule_test:
      - eval_time: 10m
        alertname: TileAvailabilityFastBurn
        exp_alerts:
          - exp_labels:
              severity: page
              slo: tile_availability
              domain: basemap
              layer: roads

Verify: run the behavioural test; a non-zero exit fails CI.

bash
promtool test rules tile-availability-tests.yaml

5. Apply and confirm the rules are live

Load the file into Prometheus and confirm the alert is registered and evaluating, not just present on disk.

bash
# after reload (SIGHUP or /-/reload), confirm the alert is loaded
curl -s http://localhost:9090/api/v1/rules \
  | jq '.data.groups[].rules[] | select(.name=="TileAvailabilityFastBurn") | {name, state}'

Configuration Reference

Field Scope Required value Effect
interval recording group 30s Evaluation cadence for the SLI ratios
[5m] fast window fast-burn expr 5m Detection window for a rapid outage
[1h] long window fast-burn expr 1h Confirmation leg suppressing deploy spikes
14.4 burn factor fast-burn expr 14.4 Pages when budget drains ~30x too fast
[6h] slow window slow-burn expr 6h Detection window for a slow leak
1 burn factor slow-burn expr 1 Tickets when budget is on pace to exhaust
0.005 budget both exprs 1 - objective Derived from the 0.995 SLO
for both alerts 2m / 15m Debounce before firing
severity alert labels page / ticket Alertmanager routing key

Common Failure Modes & Fixes

The page never fires during a real outage. Root cause: only the fast window is breaching because the outage is shorter than the 1-hour confirmation leg, or the by (domain, layer) grouping in the alert does not match the recorded series labels. Fix: confirm the recording rule and alert share identical grouping, and verify with promtool test rules using a fixture that spans both windows.

The page fires on every deploy. Root cause: the alert is watching only the 5-minute window without the 1-hour and leg, so a brief restart blip trips it. Fix: restore the two-window and condition and keep for: 2m so a single scrape cannot page.

No series returned from the recording rule. Root cause: the exporter emits status rather than code, or omits the domain label the rule groups by. Fix: align the selector to the exporter’s actual label set — promtool check metrics against a scrape sample reveals the true names.

Alert fires but never routes to on-call. Root cause: the severity: page label is present but Alertmanager’s route tree matches on a different key. Fix: confirm the receiver’s match block keys on severity and domain, and reload Alertmanager after the change.

Cardinality explosion stalls rule evaluation. Root cause: a raw z/x/y tile-coordinate label multiplies series into the millions. Fix: drop the per-tile label in the recording rule’s by () clause and keep tile identity in a separate, sampled diagnostic metric.

FAQ

Why 14.4 as the fast-burn factor?

With a 0.995 objective over 30 days, the total error budget is 0.005 of requests. A burn rate of 14.4 means the budget is being consumed 14.4 times faster than the window can absorb, which would exhaust the entire 30-day budget in about two days. That pace is unambiguously an incident, so it warrants a page rather than a ticket. Pairing it with the 1-hour confirmation window is what stops a momentary spike from reaching the same conclusion.

Should I alert per layer or per domain?

Both, at different severities. Group the recording rules by domain and layer so a single hot EPSG:3857 basemap layer timing out is visible, but consider routing the page on domain so on-call is not fragmented across dozens of layer-scoped alerts. The layer label stays on the alert as context for the responder without splitting the rotation.

How do I keep freshness separate from availability here?

This rule set measures only the 200-rate, which a stale-but-serving endpoint passes. Add a distinct freshness indicator — for example an age gauge compared against freshness.max_age_seconds — as its own recording rule and alert, so a green availability SLI and a red freshness SLI point unambiguously at the production pipeline rather than the tile server.

Can I test the rules without a live Prometheus?

Yes, and you should gate CI on it. promtool test rules evaluates the rule file against an inline synthetic series with no server running, so a crafted outage fixture proves the page fires and a steady-leak fixture proves the ticket fires. A rule change that breaks either assertion fails the build before it can reach the estate.

How should the rules behave during a planned maintenance window?

Silence the page, keep the recording. Suppressing the recording rule during maintenance produces a gap in the availability series, and a gap is not zero — it silently shortens the window the SLO is computed over, which quietly inflates the reported availability for the whole month. The correct posture is to let the recording rules run continuously so the error budget genuinely absorbs the maintenance, and to silence only the notification in Alertmanager for the window’s duration. This has the useful side effect of making planned downtime cost budget, which is exactly the incentive you want: maintenance that consumes a quarter of the month’s budget should be visible as such rather than accounted as free.

What is the right way to alert on a partial outage that affects one tile pyramid?

Add a per-layer recording rule alongside the domain-level one, and page only on the domain rule. A single failing zoom range or a corrupt pyramid subtree can leave domain-wide availability at 99.9% while a specific layer is entirely unavailable — technically within the SLO, practically an outage for whoever depends on that layer. Recording availability per layer costs little because the label cardinality is bounded by the number of published layers, and it lets the domain page carry the layer breakdown as an annotation. Paging per layer as well is a mistake: it fragments on-call attention across alerts that almost always share a single root cause.