SLOs, Alerts and Dashboards from Python Metrics

A Python service that exports request counters and latency histograms has the raw material for the most useful alerting there is: alerts that fire when users are affected, and stay silent when they are not. Getting there takes three steps — indicators that measure what users experience, objectives that say how good is good enough, and burn-rate alerts that fire when the objective is at risk. This guide covers all three, and the dashboards that sit around them. It is part of the Python metrics and instrumentation section, with detail in defining SLIs from Python request metrics, writing alert rules for Python services and burn-rate alerts and error budgets.

From counters to a page Four stages from left to right. First, the metrics a Python service already exports: http_requests_total labelled by status, and http_request_duration_seconds with a bucket at 300 milliseconds. Second, indicators computed from them: availability is non-5xx requests divided by all requests, and latency is requests under 300 milliseconds divided by all requests. Third, objectives: 99.9 percent availability and 99 percent under 300 milliseconds, each over thirty days, which gives error budgets of 0.1 and 1 percent of requests. Fourth, burn-rate alerts: a page when the budget is being consumed at 14.4 times the sustainable rate over both one hour and five minutes, and a ticket at 6 times over six hours and thirty minutes. The note says every stage is arithmetic on data the service already has; no new instrumentation is required beyond a bucket boundary at the latency threshold. metrics requests_total by status duration histogram le="0.3" bucket indicators availability =non-5xx / all latency =≤ 300 ms / all objectives 99.9 % over 30 dbudget 0.1 % 99 % over 30 dbudget 1 % alerts page14.4× · 1 h + 5 m ticket6× · 6 h + 30 m every stage is arithmetic on data the service already exports the only instrumentation requirement: a histogram bucket boundary at each latency threshold
Indicators, objectives, budgets and burn rates are all computed from a counter and a histogram. The work is in choosing, not in instrumenting.

Prerequisites

A Python service exporting, through either prometheus_client or the OpenTelemetry SDK:

  • a request counter labelled by route and status class, and
  • a latency histogram whose buckets include each latency threshold used in an objective.

Both are covered in instrumenting FastAPI with Prometheus metrics and the Prometheus client instrumentation topic. A Prometheus-compatible backend evaluates the rules; the examples use PromQL.

Concept and architecture

Symptoms, not causes. The central idea behind SLO-based alerting is that pages should fire on what users experience, not on what might be causing it. High CPU is a cause; a service can run at ninety percent CPU all day with every request fast. A full connection pool is a cause; the pool can be full and queued requests still complete in time. Conversely, a service can fail users while every resource metric looks healthy — a bad deploy returning errors, a dependency timing out, a misconfigured route. Alerting on causes produces both false pages and missed incidents. Alerting on symptoms produces neither, and the cause metrics become what they are best at: diagnosis, once a symptom page has fired.

An indicator is a ratio of good events to valid events. For availability, good events are requests that did not fail because of the service — typically, responses that are not 5xx — and valid events are all requests, perhaps excluding health checks. For latency, good events are requests that completed within a threshold. Both are ratios of counts, so both come from counters, and the latency one comes from the histogram's bucket counter at the threshold. Expressing latency as a ratio rather than a percentile is what makes it aggregatable, alertable and budgetable; a percentile is none of those.

An objective is a target for the ratio over a window. Ninety-nine point nine percent of requests succeed, measured over thirty days. The window matters as much as the target: thirty days is long enough to absorb a bad hour and short enough to reflect the service's current state.

The error budget is what the objective allows to fail. At 99.9 percent, one request in a thousand may fail. Over thirty days at a hundred requests a second, that is roughly 260 000 failed requests. The budget is a quantity to spend — on deploys, experiments, maintenance — and the rate at which it is spent is what alerts watch.

Burn rate is the spending speed relative to sustainable. A burn rate of one spends the budget exactly over the window. A burn rate of 14.4 spends two percent of a thirty-day budget in one hour and exhausts it in about two days. Alerts on burn rate fire according to how fast the objective is being consumed, which is the question a person being paged needs answered.

Why cause alerts fail Four situations compared. First, CPU at 90 percent while every request is fast: a CPU alert pages someone for nothing, and the SLO alert stays quiet, which is correct. Second, a bad deploy returning 500 errors from one route with CPU at 20 percent: the CPU alert stays quiet and misses the incident, while the SLO burn-rate alert fires within minutes. Third, a downstream API timing out, so requests wait and fail while the Python process is mostly idle: resource alerts miss it and the latency SLO fires. Fourth, a connection pool full at peak with requests queuing briefly but completing within threshold: a pool alert pages needlessly and the SLO stays quiet. The summary says cause alerts are right in none of the four; symptom alerts are right in all four, and the cause metrics then explain each page. situationcause alertSLO alert CPU 90 %, every request fastpages — needlesslyquiet — correct bad deploy, 500s from one route, CPU 20 %quiet — misses itfires in minutes downstream API timing out, process idlequiet — misses itlatency SLO fires pool full at peak, requests still in timepages — needlesslyquiet — correct cause alerts: wrong in all four · symptom alerts: right in all four the cause metrics still matter — they explain each page once it fires
Resource metrics are good diagnostics and bad alarms. The user-facing indicator is the only thing that reliably says whether to wake someone.

Step-by-step implementation

Step 1 — Write the indicators as recording rules. Recording rules compute the ratio once, at several window lengths, so alerts and dashboards read a cheap precomputed series.

groups:
  - name: orders-api-sli
    rules:
      - record: sli:availability:ratio_rate5m
        expr: |
          sum(rate(http_requests_total{service="orders-api",status!~"5.."}[5m]))
          /
          sum(rate(http_requests_total{service="orders-api"}[5m]))
      - record: sli:latency_300ms:ratio_rate5m
        expr: |
          sum(rate(http_request_duration_seconds_bucket{service="orders-api",le="0.3"}[5m]))
          /
          sum(rate(http_request_duration_seconds_count{service="orders-api"}[5m]))

The same rules repeat for 30m, 1h, 6h and 3d, the windows the alerts use.

Step 2 — Choose objectives. Start from what the service achieves today, measured over the last month, and set the objective slightly below it. An objective the service already misses pages constantly; one far below its performance never pages at all. Tighten it later, deliberately, when there is a reason to.

Step 3 — Alert on burn rate with two windows. A page when the budget burns at 14.4× over both the last hour and the last five minutes; a ticket at 6× over six hours and thirty minutes. The long window ensures the problem is significant; the short one ensures it is still happening, so the alert resolves promptly after a fix.

      - alert: OrdersApiAvailabilityBurnFast
        expr: |
          (1 - sli:availability:ratio_rate1h) > (14.4 * 0.001)
          and
          (1 - sli:availability:ratio_rate5m) > (14.4 * 0.001)
        labels: {severity: page}

Step 4 — Route cause alerts to tickets. Pool saturation, memory growth, GC pauses and event-loop lag remain valuable. They become tickets or dashboard panels — things to look at in working hours, or during an incident a symptom alert has already opened.

Step 5 — Build the dashboard around the indicators. Top row: each indicator against its objective, and the remaining budget. Below it: traffic, errors, latency distribution and saturation — the four signals that explain a change in the indicators.

Configuration reference

Element Typical value Why
Availability SLI non-5xx / all, excluding health checks client errors are not the service's failures
Latency SLI ≤ threshold / all a ratio, not a percentile
Latency threshold a histogram bucket boundary exact, not interpolated
Objective window 30 days rolling absorbs bad hours
Fast-burn page 14.4× over 1 h and 5 m 2 % of budget in an hour
Slow-burn ticket 6× over 6 h and 30 m 5 % of budget in six hours
Cause alerts tickets diagnosis, not paging

Async and concurrency considerations

The indicators are only as honest as the point where requests are measured. A Python service measuring latency inside the application handler excludes the time a request waited before a worker picked it up — in the Gunicorn backlog, in a saturated thread pool, or behind a blocked event loop. When the service is overloaded, that wait is the largest part of what users experience, and the in-process indicator does not see it.

Two mitigations exist. Measuring at the load balancer or ingress captures the queueing, at the cost of a second metrics source. Measuring in-process but alerting as well on a saturation signal that captures the wait — event-loop lag, thread-pool queue depth, as described in observing thread pool saturation — covers it indirectly. For services where overload is a realistic failure mode, the ingress measurement is the more reliable indicator.

Multi-process servers need aggregated metrics, or the indicators are computed from a random worker's share of traffic and fluctuate meaninglessly. Metrics in multi-process Python servers covers the setup; the ratio itself is robust to missing workers only if every worker's numerator and denominator are both missing, which unaggregated scraping does not guarantee.

Production code examples

A complete PrometheusRule for one service's availability objective, including all the windows the alerts need:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata: {name: orders-api-slo}
spec:
  groups:
    - name: orders-api-sli
      interval: 30s
      rules:
        - record: sli:availability:errors_ratio_rate5m
          expr: 1 - (sum(rate(http_requests_total{service="orders-api",status!~"5.."}[5m])) / sum(rate(http_requests_total{service="orders-api"}[5m])))
        - record: sli:availability:errors_ratio_rate1h
          expr: 1 - (sum(rate(http_requests_total{service="orders-api",status!~"5.."}[1h])) / sum(rate(http_requests_total{service="orders-api"}[1h])))
        - record: sli:availability:errors_ratio_rate30m
          expr: 1 - (sum(rate(http_requests_total{service="orders-api",status!~"5.."}[30m])) / sum(rate(http_requests_total{service="orders-api"}[30m])))
        - record: sli:availability:errors_ratio_rate6h
          expr: 1 - (sum(rate(http_requests_total{service="orders-api",status!~"5.."}[6h])) / sum(rate(http_requests_total{service="orders-api"}[6h])))
    - name: orders-api-slo-alerts
      rules:
        - alert: OrdersApiErrorBudgetBurnFast
          expr: sli:availability:errors_ratio_rate1h > 14.4 * 0.001 and sli:availability:errors_ratio_rate5m > 14.4 * 0.001
          labels: {severity: page}
          annotations:
            summary: "orders-api is spending its 30-day error budget at over 14x"
        - alert: OrdersApiErrorBudgetBurnSlow
          expr: sli:availability:errors_ratio_rate6h > 6 * 0.001 and sli:availability:errors_ratio_rate30m > 6 * 0.001
          labels: {severity: ticket}

Expected Output: during a deploy that fails two percent of requests, the fast-burn alert fires within about five minutes of the error rate crossing the threshold, and resolves within five minutes of the rollback.

Dashboards that answer the first question

The first question during any incident is whether users are affected, and how badly. A dashboard built around SLOs answers it in the top row, before anyone has to interpret a graph.

A dashboard in the order questions are asked A dashboard mock-up in three rows. The top row answers whether users are affected: availability at 99.94 percent against a 99.9 objective, latency at 98.7 percent under 300 milliseconds against a 99 percent objective, shown in the warning colour, and error budget remaining at 62 percent for availability and minus 30 percent for latency. The middle row explains the indicators: request rate by route, error rate by route and status, and latency percentiles by route, where one route shows a clear rise. The bottom row shows causes: worker and pool saturation, event-loop lag, and dependency latency by downstream service. A note says the rows are ordered by the questions an engineer asks — are users hurt, where, and why. row 1 · are users affected? availability99.94 % ≤ 300 ms98.7 % avail. budget left62 % latency budget left−30 % row 2 · where? requests by route errors by route + status p99 by route/reports ↑ row 3 · why? worker + pool saturation event-loop lag dependency latency
Indicators and budgets first, then where the damage is, then why. The layout follows the order of the questions.

A few layout rules keep such dashboards useful:

  • One dashboard per service, with the same layout for every service, so an engineer who knows one knows all of them.
  • Budgets as numbers, not only graphs. "Thirty percent over budget" is read in a second; a burn-down line takes longer.
  • Breakdowns by route, since most incidents affect a subset of routes, and the route label is what locates them.
  • Links from each panel to the traces and logs for the same service and window, using the correlation described in correlating logs, traces and metrics.

What the error budget is for

The error budget is often introduced as an alerting mechanism, and its more important role is as a decision rule. With budget to spare, a team can deploy freely, run experiments and accept some risk; the objective is being met and users are, by the team's own definition, adequately served. With the budget exhausted, the team slows down: fewer risky changes, more reliability work, until the window rolls forward and budget returns.

This turns a recurring argument — ship faster versus be more careful — into a measurement both sides agreed to in advance. It also makes the choice of objective consequential. An objective set too tight exhausts the budget on normal variation and stalls delivery for no user benefit; one set too loose never constrains anything. Revisiting objectives quarterly, against what users actually complained about, keeps them calibrated.

For a Python service specifically, the budget policy often decides practical questions: whether a dependency upgrade can go out on a Friday, whether a new feature flag rollout proceeds to the next stage, whether a performance regression found in profiling needs fixing this sprint. The budget does not answer those alone, and it gives the discussion a shared number.

Deciding what counts as a valid event

Most arguments about an indicator come down to the denominator. Which requests count at all?

Health checks and readiness probes should usually be excluded. They are frequent, cheap and almost always succeed, so including them inflates availability and dilutes the effect of real failures. A service handling ten requests a second, with a probe every second from three sources, has almost a quarter of its traffic from probes. Metrics scrapes should be excluded for the same reason.

Requests rejected deliberately need a decision. A 429 from rate limiting that protects the service from an abusive client is the service working; a 429 from a limit set too low for legitimate traffic is the service failing its users. Many teams count 429s as good by default and track them separately, revisiting if they rise.

Requests cancelled by the client — a user closing the page, an upstream timeout — are neither good nor bad from the service's point of view, but they often indicate latency. Recording them with a distinct status label, rather than letting them appear as errors or vanish, keeps the choice open.

Finally, some routes matter more than others. A checkout endpoint failing is not the same as a settings page failing, even at the same rate. Separate objectives for a handful of critical routes, alongside the service-wide one, keep a problem on an important route from disappearing into the average. The route label on the request counter makes this a matter of adding a selector.

SLOs for work that is not a request

Background jobs, queue consumers and scheduled tasks have users too, and the same approach applies with different events. For a Celery queue, the good event might be a task completing successfully within its freshness target — an email sent within five minutes of the order — and the valid event every task enqueued. For a nightly batch, the good event is the batch finishing by its deadline, and the indicator is a count of days rather than requests.

These indicators come from the metrics described in collecting metrics from Celery workers and logging from cron and batch jobs. The burn-rate arithmetic is identical; the windows are sometimes longer, since a daily job produces thirty events a month and short windows contain none.

Introducing SLOs to an existing service

A service with years of cause-based alerts cannot switch overnight, and trying usually fails: the team loses trust in the new alerts before they are tuned, and the old ones come back. A gradual path works better.

First, compute the indicators and put them on the dashboard, without alerting. A month of history shows what the service actually achieves and whether the indicators agree with the team's sense of how the service has been doing. Disagreements are informative — an indicator that looked fine during a known incident is measuring the wrong thing.

Second, add burn-rate alerts as tickets rather than pages, and compare them with the existing pages for a few weeks. Every time a cause alert pages without a burn alert, that is a candidate for demotion; every time a burn alert fires without a cause alert, that is an incident the old alerts missed.

Third, promote the fast-burn alert to a page and demote the cause alerts it has proved redundant. Keep a small number of cause alerts that predict imminent failure the indicators cannot see yet — a disk about to fill, a certificate about to expire — since those are genuinely urgent and have no user-facing symptom until it is too late.

Common mistakes

Latency objectives on percentiles. Error signature: an objective like "p99 under 300 ms" that cannot be turned into a budget. Root cause: percentiles do not aggregate or subtract. Remediation: the fraction of requests under 300 ms, from the bucket counter.

A threshold between bucket boundaries. Error signature: an indicator that shifts when traffic shape changes. Root cause: interpolation within a bucket. Remediation: a boundary exactly at the threshold.

4xx counted as failures. Error signature: budget spent by clients sending bad requests. Root cause: client errors in the bad-event count. Remediation: count 5xx, and perhaps 429 when the service throttles unfairly.

Paging on causes. Error signature: pages for high CPU on healthy services, silence during real outages. Root cause: cause metrics treated as symptoms. Remediation: tickets for causes, pages for burn rate.

Single-window burn alerts. Error signature: alerts that fire on one bad minute, or that stay firing an hour after the fix. Root cause: one window cannot be both fast and stable. Remediation: pair a long and a short window.

Objectives nobody owns. Error signature: SLOs breached for weeks without action. Root cause: no team committed to the budget policy. Remediation: fewer objectives, each with an owner and an agreed response.

Probes in the denominator. Error signature: availability that barely moves during an outage. Root cause: health checks and scrapes, which almost always succeed, diluting real traffic. Remediation: exclude them by route in the recording rule.

Indicators measured only in-process. Error signature: a latency SLO that stays green while users see timeouts during overload. Root cause: queueing before the handler is invisible to it. Remediation: measure at the ingress as well, or pair the indicator with a saturation alert.

Replacing every alert at once. Error signature: a team that turns the old alerts back on after a month. Root cause: new alerts untuned and untrusted. Remediation: run burn alerts as tickets alongside the old ones first.

Frequently Asked Questions

What is the difference between an SLI and an SLO?

An SLI is a measurement — the fraction of requests that succeeded, or that completed under a latency threshold. An SLO is a target for that measurement over a window, such as 99.9 percent of requests succeeding over thirty days.

Why alert on burn rate instead of error rate?

A fixed error-rate threshold is either too sensitive for brief spikes or too slow for sustained problems. Burn rate expresses the error rate relative to what the objective allows, so one number says how quickly the budget will run out, and multi-window rules turn that into pages that are both fast and quiet.

Should I page on high CPU or memory?

Usually not. Those are causes, and a service can run hot without users noticing, or fail users while its CPU is idle. Page on the user-facing indicators, and use cause metrics to diagnose once a page fires.

Which metrics do Python services need for SLOs?

A request counter labelled by route and status, and a latency histogram with a bucket boundary at each latency threshold used in an objective. Nothing else is required to compute availability and latency indicators.

How many SLOs should a service have?

Few — typically an availability and a latency objective for the service as a whole, and perhaps separate ones for a small number of critical routes. Every objective needs someone who cares when it is breached; objectives nobody acts on are noise.