Skip to main content

Kubernetes SLO Alerting with Prometheus Burn Rates

Define a real SLO on Kubernetes, then build multi-window burn rate alerts in Prometheus and Alertmanager that page you before the budget is gone.

Blueprint Walkthrough

Architecture Overview & Problem Statement

This project defines a 99% availability SLO for a Kubernetes service and enforces it with multi-window burn rate alerts in Prometheus, routed through Alertmanager to separate Slack channels for pages and tickets. You deploy a sample API that exposes real request metrics, write recording rules for the error ratio over eight time windows, create four burn-rate alerts, build an SLO dashboard in Grafana, and then trigger the alerts with generated errors. You finish with an SLO document, an error budget policy, and a dashboard you can show in a portfolio.

This is the reliability engineering project. It assumes you can already run Prometheus, Grafana, and Alertmanager on Kubernetes, which is exactly what Kubernetes Monitoring with Prometheus and Loki teaches. That project is about collecting metrics and logs. This one is about deciding how reliable a service must be and alerting on how fast you are losing that reliability.

What you build:

  • A sample API scraped through a ServiceMonitor
  • An SLO: 99% of requests succeed over 28 days, which gives a 1% error budget
  • Recording rules for the error ratio over 5m, 30m, 1h, 2h, 6h, 1d, 3d, and 28d
  • Four burn-rate alerts, two that page and two that open tickets
  • An SLO dashboard and a one-page error budget policy
flowchart LR APP[orders-api
request metrics] --> PROM[Prometheus] PROM --> REC[Recording rules
error ratios] REC --> ALERT[Burn-rate alerts] REC --> GRAF[Grafana
SLO dashboard] ALERT --> AM[Alertmanager] AM -->|critical| PAGE[Slack: page] AM -->|warning| TICKET[Slack: ticket] classDef src fill:#475569,color:#fff classDef core fill:#F97316,color:#fff classDef rule fill:#8B5CF6,color:#fff classDef out fill:#10B981,color:#fff class APP src class PROM core class REC,ALERT rule class GRAF,AM,PAGE,TICKET out

The problem this solves

A fixed threshold such as "alert when the error rate passes 1 percent" is a poor alert. It pages for brief spikes that cost almost nothing, and it stays silent about a steady 0.9 percent error rate that quietly burns through the budget. Burn rate alerting asks a better question: how fast are we spending the error budget?

An SLO of 99 percent over 28 days allows 1 percent of requests to fail, which is about 403 minutes of full downtime. A burn rate of 1 means you spend the budget exactly over the 28 days. A burn rate of 14.4 means you spend about 2 percent of the whole budget in a single hour and would exhaust it in roughly two days. That deserves a page. A burn rate of 1 sustained for three days deserves a ticket, not a 3am wake-up.

Tip

Which SLO you pick is a product decision, not a technical one. Start at 99 percent or 99.5 percent and tighten it only when you can show the service is reliably better.

Milestone 1: Prepare the Cluster and Monitoring Stack

This milestone gives you a cluster with kube-prometheus-stack running. If you finished the monitoring project, reuse that cluster and skip to Milestone 2. Otherwise, run the condensed setup below.

Bash
kind create cluster --name slo-project
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm search repo prometheus-community/kube-prometheus-stack
export PROM_VERSION=<version from the search above>
kubectl create namespace monitoring

Create prometheus-values.yaml. Retention is 30 days because your SLO window is 28 days, and Prometheus cannot compute a 28-day ratio from 15 days of data.

YAML
grafana:
adminPassword: "change-me"
prometheus:
prometheusSpec:
retention: 30d
serviceMonitorSelectorNilUsesHelmValues: false
ruleSelectorNilUsesHelmValues: false
kubeEtcd:
enabled: false
kubeControllerManager:
enabled: false
kubeScheduler:
enabled: false
Bash
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--values prometheus-values.yaml \
--version $PROM_VERSION \
--wait
kubectl get pods -n monitoring
Common Mistake

Keeping the default 15-day retention with a 28-day SLO. The long-window queries silently return a ratio computed from only the data that exists, so your budget numbers look better than reality.

Milestone 2: Deploy an App That Exposes Request Metrics

This milestone deploys the service you will measure. An SLO needs a request counter with a status code, so the app must expose one. A plain nginx container does not, so this uses a small example app that publishes http_requests_total{code, method}.

Its / path returns 200 and its /err path returns an error, which gives you a controllable source of failures. A ServiceMonitor tells Prometheus to scrape it.

Bash
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: orders-api
template:
metadata:
labels:
app: orders-api
spec:
containers:
- name: orders-api
image: quay.io/brancz/prometheus-example-app:v0.5.0
ports:
- name: http
containerPort: 8080
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: orders-api
namespace: default
labels:
app: orders-api
spec:
selector:
app: orders-api
ports:
- name: http
port: 8080
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: orders-api
namespace: default
labels:
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app: orders-api
endpoints:
- port: http
path: /metrics
interval: 15s
EOF

Check which status code /err returns, because the SLI in the next milestone depends on it:

Bash
kubectl port-forward svc/orders-api 8080:8080 &
curl -s -o /dev/null -w "%{http_code}\n" localhost:8080/
curl -s -o /dev/null -w "%{http_code}\n" localhost:8080/err

In recent versions /err returns 404 rather than 500. That is why this project defines a failed request as any response outside the 2xx range, instead of only 5xx. Confirm the target shows as UP at http://localhost:9090/targets (port-forward svc/kube-prometheus-stack-prometheus 9090:9090 in the monitoring namespace).

Common Mistake

Adding prometheus.io/scrape annotations and expecting scraping. Those annotations only work with a Prometheus config that reads them. With the Prometheus Operator, you create a ServiceMonitor.

Milestone 3: Define the SLI and SLO

This milestone turns a vague goal into a measurable promise. A Service Level Indicator (SLI) is the measurement, and a Service Level Objective (SLO) is the target for it. Writing them down first stops you from alerting on whatever metric happens to be available.

Item Definition
SLI Fraction of requests to orders-api that return a 2xx status
SLO 99% of requests succeed over a rolling 28 days
Error budget 1% of requests, about 403 minutes of full outage per 28 days
Bad request Any response where code is not 2xx

Test the SLI in the Prometheus UI before you build rules on it:

TEXT
sum(rate(http_requests_total{job="orders-api",code!~"2.."}[5m]))
/
sum(rate(http_requests_total{job="orders-api"}[5m]))

The sum matters. Without it you get one ratio per pod, and a pod with no errors hides a pod with many. The query returns nothing when there is no traffic, which is correct: a service nobody calls has no measurable error ratio.

Common Mistake

Setting an SLO of 99.999 percent. That allows about 26 seconds of failure per month, and one routine deployment can use most of it. A budget that small gets ignored.

Milestone 4: Write Recording Rules for Every Window

This milestone pre-computes the error ratio over each time window. Burn-rate alerts compare a long window against a short one, so you need the same ratio at several lengths. Recording rules calculate each one on a schedule, so alert evaluation reads a stored value instead of scanning raw samples every time.

Create slo-recording-rules.yaml:

YAML
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: orders-api-slo-recording
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
groups:
- name: orders-api-slo-recording
interval: 30s
rules:
- record: orders_api:error_ratio:rate5m
expr: |
sum(rate(http_requests_total{job="orders-api",code!~"2.."}[5m]))
/ sum(rate(http_requests_total{job="orders-api"}[5m]))
- record: orders_api:error_ratio:rate30m
expr: |
sum(rate(http_requests_total{job="orders-api",code!~"2.."}[30m]))
/ sum(rate(http_requests_total{job="orders-api"}[30m]))
- record: orders_api:error_ratio:rate1h
expr: |
sum(rate(http_requests_total{job="orders-api",code!~"2.."}[1h]))
/ sum(rate(http_requests_total{job="orders-api"}[1h]))
- record: orders_api:error_ratio:rate2h
expr: |
sum(rate(http_requests_total{job="orders-api",code!~"2.."}[2h]))
/ sum(rate(http_requests_total{job="orders-api"}[2h]))
- record: orders_api:error_ratio:rate6h
expr: |
sum(rate(http_requests_total{job="orders-api",code!~"2.."}[6h]))
/ sum(rate(http_requests_total{job="orders-api"}[6h]))
- record: orders_api:error_ratio:rate1d
expr: |
sum(rate(http_requests_total{job="orders-api",code!~"2.."}[1d]))
/ sum(rate(http_requests_total{job="orders-api"}[1d]))
- record: orders_api:error_ratio:rate3d
expr: |
sum(rate(http_requests_total{job="orders-api",code!~"2.."}[3d]))
/ sum(rate(http_requests_total{job="orders-api"}[3d]))
- record: orders_api:error_ratio:rate28d
expr: |
sum(rate(http_requests_total{job="orders-api",code!~"2.."}[28d]))
/ sum(rate(http_requests_total{job="orders-api"}[28d]))
Bash
kubectl apply -f slo-recording-rules.yaml
Tip

A real team would generate these rules from an SLO definition with a tool such as Sloth or Pyrra instead of writing eight copies by hand. Write them by hand once so you understand what those tools produce.

Milestone 5: Create Multi-Window Burn Rate Alerts

This milestone creates the alerts. Each one fires only when a long window and a short window both exceed the same burn rate. The long window proves the problem is significant, and the short window proves it is still happening, so the alert also stops quickly once the problem is fixed.

The threshold for a burn rate is the burn rate multiplied by the budget (0.01):

Severity Burn rate Long window Short window Budget used Budget gone in
critical (page) 14.4 1h 5m about 2% in 1 hour about 2 days
critical (page) 6 6h 30m about 5% in 6 hours about 5 days
warning (ticket) 3 1d 2h about 10% in 1 day about 9 days
warning (ticket) 1 3d 6h about 10% in 3 days 28 days

Create slo-alert-rules.yaml:

YAML
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: orders-api-slo-alerts
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
groups:
- name: orders-api-slo-alerts
rules:
- alert: OrdersAPIBurnRateCritical1h
expr: |
orders_api:error_ratio:rate1h > (14.4 * 0.01)
and orders_api:error_ratio:rate5m > (14.4 * 0.01)
for: 2m
labels:
severity: critical
slo: orders-api-availability
annotations:
summary: "orders-api is burning error budget at 14.4x"
description: "Error ratio is {{ $value | humanizePercentage }} over 1h. About 2% of the 28-day budget is used per hour at this rate."
- alert: OrdersAPIBurnRateCritical6h
expr: |
orders_api:error_ratio:rate6h > (6 * 0.01)
and orders_api:error_ratio:rate30m > (6 * 0.01)
for: 5m
labels:
severity: critical
slo: orders-api-availability
annotations:
summary: "orders-api is burning error budget at 6x"
description: "Error ratio is {{ $value | humanizePercentage }} over 6h. The budget is gone in about 5 days at this rate."
- alert: OrdersAPIBurnRateWarning1d
expr: |
orders_api:error_ratio:rate1d > (3 * 0.01)
and orders_api:error_ratio:rate2h > (3 * 0.01)
for: 15m
labels:
severity: warning
slo: orders-api-availability
annotations:
summary: "orders-api is burning error budget at 3x"
description: "Error ratio is {{ $value | humanizePercentage }} over 1d. Open a ticket and investigate in working hours."
- alert: OrdersAPIBurnRateWarning3d
expr: |
orders_api:error_ratio:rate3d > (1 * 0.01)
and orders_api:error_ratio:rate6h > (1 * 0.01)
for: 30m
labels:
severity: warning
slo: orders-api-availability
annotations:
summary: "orders-api is burning error budget at 1x"
description: "Error ratio is {{ $value | humanizePercentage }} over 3d. Left alone, the 28-day budget runs out exactly at the end of the window."
Bash
kubectl apply -f slo-alert-rules.yaml

Cause-based alerts such as high memory or crash looping are not in this set on purpose. Page a person for symptoms users feel. Keep cause-based signals on dashboards, or as tickets, to help diagnose.

Common Mistake

Using one window per alert. A single short window pages for every blip. A single long window keeps paging for an hour after you fixed the problem. The pair is what makes the alert both sensitive and quick to clear.

Milestone 6: Route Pages and Tickets to Slack

This milestone sends critical alerts to one channel and warnings to another. If both land in one channel, people learn to ignore it, because a 2am outage and a slow leak look the same.

Create two Slack incoming webhooks (or one webhook per channel) and store them as a Secret so they never reach Git:

Bash
kubectl create secret generic slack-webhooks -n monitoring \
--from-literal=page='https://hooks.slack.com/services/REPLACE_PAGE' \
--from-literal=ticket='https://hooks.slack.com/services/REPLACE_TICKET'

Create alertmanager-values.yaml. It uses api_url_file, which reads the webhook from the mounted Secret:

YAML
alertmanager:
alertmanagerSpec:
secrets:
- slack-webhooks
config:
route:
receiver: slack-ticket
group_by: ["alertname", "slo"]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- receiver: "null"
matchers:
- alertname = "Watchdog"
- receiver: slack-page
matchers:
- severity = "critical"
receivers:
- name: "null"
- name: slack-page
slack_configs:
- api_url_file: /etc/alertmanager/secrets/slack-webhooks/page
channel: "#alerts-page"
send_resolved: true
- name: slack-ticket
slack_configs:
- api_url_file: /etc/alertmanager/secrets/slack-webhooks/ticket
channel: "#alerts-ticket"
send_resolved: true
Bash
helm upgrade kube-prometheus-stack prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--values prometheus-values.yaml \
--values alertmanager-values.yaml \
--version $PROM_VERSION

The default message already shows the alert name, labels, and annotations, so this config avoids custom Go templates. Custom templates inside Helm values need extra escaping, because Helm tries to render them first.

Security

Treat webhook URLs like passwords. Anyone holding one can post to your channel.

Milestone 7: Build the SLO Dashboard

This milestone makes the error budget visible to people who do not read PromQL. In Grafana, create a new dashboard with these panels, each backed by your recording rules:

Panel Query What it tells you
Error ratio now orders_api:error_ratio:rate5m * 100 Current failure percentage
Burn rate (1h) orders_api:error_ratio:rate1h / 0.01 1 is on budget, above 14.4 pages
Error budget remaining (1 - orders_api:error_ratio:rate28d / 0.01) * 100 Percent of the 28-day budget left
Requests per second sum(rate(http_requests_total{job="orders-api"}[5m])) Traffic, so you can tell quiet from broken

Save it as Orders API SLO Overview, then export the JSON from Dashboard Settings and commit it to Git. Grafana keeps dashboards in its own storage, so a recreated pod loses any dashboard that only lives in the UI.

Milestone 8: Write the Error Budget Policy

This milestone records what the team does as the budget runs down. An SLO that changes no decisions is decoration. Create a one-page SLO.md in your repository with the SLO from Milestone 3 and a policy such as this:

Budget remaining Action
More than 50% Ship features normally
25% to 50% Review recent incidents; require a rollback plan on risky changes
Under 25% Slow releases; prioritise reliability work
Exhausted Freeze feature releases until the budget recovers

Adjust the numbers for your own service, and state who decides when the policy is invoked. This document is the deliverable that interviewers ask about most.

Validation & Testing

First confirm the pieces exist:

Bash
kubectl get prometheusrule -n monitoring
# Expected: orders-api-slo-recording and orders-api-slo-alerts
# In Prometheus (http://localhost:9090), run:
# orders_api:error_ratio:rate5m
# Expected: a value once the app has traffic

Then break the service on purpose. This traffic generator sends one good and one bad request per loop, so about half the requests fail:

Bash
kubectl run loadgen --image=busybox --restart=Never -- /bin/sh -c \
'while true; do
wget -q -O /dev/null http://orders-api.default.svc:8080/
wget -q -O /dev/null http://orders-api.default.svc:8080/err
sleep 0.2
done'
  • In Prometheus, orders_api:error_ratio:rate5m should settle near 0.5.
  • In a fresh lab there is little history, so the 1h ratio also reads high quickly. Open Alerts in Prometheus and watch OrdersAPIBurnRateCritical1h move from Inactive to Pending, then Firing after its two-minute for period.
  • Confirm the message reaches your page channel, and that warning alerts go to the ticket channel.
  • On the dashboard, confirm the burn rate panel shows a value far above 14.4 and the budget remaining panel drops.

Now fix the problem and watch the alerts resolve:

Bash
kubectl delete pod loadgen

Record the time from the first bad request to the page, and the time from the fix to the resolved message. If the gap is longer than you want, shorten the for durations and note the trade-off.

Clean up when you finish:

Bash
kubectl delete pod loadgen --ignore-not-found
kubectl delete deployment,service,servicemonitor orders-api -n default
helm uninstall kube-prometheus-stack -n monitoring
kind delete cluster --name slo-project
Common Mistake

Testing with a service that has no traffic. A ratio of zero requests divided by zero returns nothing, so the alert never fires. Pair your SLO alerts with a separate check for a service that has stopped receiving traffic at all.

As an extension, add a latency SLO using the app's request duration histogram, for example "95% of requests complete in under 300 ms", and give it its own burn-rate alerts.

Found a mistake, an outdated step or a broken link?