DevOps & SRE No. 168

SLOs and Error Budgets skill for Claude Code

Page when the budget burns, not when a graph wiggles. Claude defines SLIs as good over valid events measured near the user, picks targets from past performance, and uses a bundled calculator to derive the budget and the 14.4x, 6x and 1x burn-rate thresholds. It writes Prometheus recording and alerting rules, backtests them against past incidents, and drafts the policy that governs releases when the budget runs low.

  • nameslo-error-budgets
  • size193 lines · ≈2,050 tokens
  • files2
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Defines service level indicators and objectives from user journeys, computes error budgets, and builds multi-window multi-burn-rate alerts as Prometheus recording and alerting rules, plus an error budget policy that says what happens when the budget runs out. Use when choosing SLIs or SLO targets, replacing noisy threshold alerts with burn-rate alerts, writing Sloth, Pyrra, or OpenSLO specs, or answering "how reliable should this service be".

Try asking

  • Define SLOs for our checkout API and write the Prometheus alerts
  • Our on-call gets paged for every error spike. Switch us to burn-rate alerting
  • We had 9,000 failed requests this month on a 99.9% SLO. How much budget is left?

Use it when

  • A service needs its first SLOs or the current ones are not trusted
  • Threshold alerts are noisy or miss real incidents
  • You need to decide how reliable a service should be
  • Release decisions should depend on reliability data

Skip it when

  • You are in the middle of an outage — use incident-response instead
  • You need to emit the underlying metrics first — use opentelemetry instead

How to check the result

Backtesting the burn-rate alerts over the last 30 to 90 days fires for every customer-visible incident and stays quiet otherwise, and the bundled calculator matches the thresholds in the rules.

The common mistake it prevents

Setting the target from aspiration instead of history, so the service starts every window already out of budget and the alerts get ignored.

~/.claude/skills/slo-error-budgets/SKILL.md

--- name: slo-error-budgets description: Defines service level indicators and objectives from user journeys, computes error budgets, and builds multi-window multi-burn-rate alerts… license: MIT ---

A good SLO measures what users experience at a boundary they touch, has a target the team can defend with data, turns its error budget into alerts that page only when the budget is genuinely at risk, and comes with a written policy for what happens when the budget is spent.

Ground rules

  • SLIs are ratios: good events / valid events. Never “CPU < 80%” or “pod is up”.
  • Measure as close to the user as practical: load balancer or API gateway beats the app’s own metrics; synthetic probes cover low-traffic paths.
  • Pick targets from history, not aspiration. A target above what the service achieved last quarter just produces permanent alerts.
  • Every dependency reduces achievable reliability; an SLO cannot exceed what critical dependencies provide without redundancy.
  • Alert on burn rate, not on instantaneous error ratio.
  • Fewer SLOs is better: 1–3 per user journey.

Workflow

- [ ] 1. List critical user journeys and where they are measured
- [ ] 2. Choose SLI type and exact good/valid event definitions
- [ ] 3. Pull 4+ weeks of history; set the target and window
- [ ] 4. Compute the budget and burn-rate thresholds (script)
- [ ] 5. Write recording rules + multi-window alerts
- [ ] 6. Write the error budget policy
- [ ] 7. Backtest alerts against past incidents; build the dashboard

2. SLI menu

Journey typeSLIGood eventValid event
Request/response APIAvailabilityResponse not 5xx (and not 429 caused by you)All requests except health checks, bots
Request/response APILatencyServed under threshold (e.g. 300 ms)Same as availability
Data pipelineFreshnessData newer than N minutes when read/checkedEach check interval
Data pipelineCorrectnessRecords passing validationAll processed records
Batch jobCompletionJob finished successfully before deadlineScheduled runs
Async queueEnd-to-end latencyProcessed within N seconds of enqueueMessages enqueued

Decide explicitly whether 4xx count as bad (usually not; client errors) and whether timeouts at the client count (yes, if you can see them at the LB).

Latency SLIs use the fraction of requests below a threshold, not a percentile, because fractions add up across instances and windows; percentiles do not. The histogram must have a bucket boundary exactly at the threshold.

3. Target and window

  • Window: rolling 28 or 30 days (28 keeps weekday mix constant).
  • Use history: if last 4 weeks were 99.95%, a 99.9% target leaves room; 99.99% does not.
TargetBudget per 30 daysImplication
99%7.2 hInternal tools, batch
99.5%3.6 hMany B2B features
99.9%43.2 minTypical customer-facing API
99.95%21.6 minCore revenue paths with redundancy
99.99%4.3 minNeeds multi-region, automated failover, very fast rollback

4. Budget and thresholds

python3 scripts/burn_rate.py --slo 99.9                           # budget + alert thresholds
python3 scripts/burn_rate.py --slo 99.9 --error-ratio 0.004       # how bad is today?
python3 scripts/burn_rate.py --slo 99.9 --good 9991000 --total 10000000 --elapsed-days 12

Standard multi-window, multi-burn-rate policy (30-day window):

SeverityLong windowShort windowBurn rateBudget consumed when it fires
Page1 h5 min14.42%
Page6 h30 min65%
Ticket3 d6 h110%

Burn rate = observed error ratio / (1 − SLO). The short window makes the alert reset quickly once the problem stops.

5. Prometheus rules

groups:
  - name: slo-checkout-availability
    rules:
      - record: slo:sli_error:ratio_rate5m
        expr: |
          sum(rate(http_requests_total{job="checkout",code=~"5.."}[5m]))
          /
          sum(rate(http_requests_total{job="checkout"}[5m]))
        labels: { slo: checkout-availability }
      - record: slo:sli_error:ratio_rate30m
        expr: |
          sum(rate(http_requests_total{job="checkout",code=~"5.."}[30m]))
          / sum(rate(http_requests_total{job="checkout"}[30m]))
        labels: { slo: checkout-availability }
      - record: slo:sli_error:ratio_rate1h
        expr: |
          sum(rate(http_requests_total{job="checkout",code=~"5.."}[1h]))
          / sum(rate(http_requests_total{job="checkout"}[1h]))
        labels: { slo: checkout-availability }
      - record: slo:sli_error:ratio_rate6h
        expr: |
          sum(rate(http_requests_total{job="checkout",code=~"5.."}[6h]))
          / sum(rate(http_requests_total{job="checkout"}[6h]))
        labels: { slo: checkout-availability }

      - alert: CheckoutErrorBudgetBurnFast
        expr: |
          (slo:sli_error:ratio_rate1h{slo="checkout-availability"} > (14.4 * 0.001))
          and
          (slo:sli_error:ratio_rate5m{slo="checkout-availability"} > (14.4 * 0.001))
        labels: { severity: page }
        annotations:
          summary: "Checkout burning 2% of monthly error budget per hour"
          runbook_url: https://runbooks.example.com/checkout-availability
      - alert: CheckoutErrorBudgetBurnSlow
        expr: |
          (slo:sli_error:ratio_rate6h{slo="checkout-availability"} > (6 * 0.001))
          and
          (slo:sli_error:ratio_rate30m{slo="checkout-availability"} > (6 * 0.001))
        labels: { severity: page }

Add 2h/1d/3d rules the same way for the ticket alert (or compute long windows from the 5m rule with avg_over_time if query cost matters). Latency SLI error ratio:

1 - (
  sum(rate(http_request_duration_seconds_bucket{job="checkout",le="0.3"}[5m]))
  /
  sum(rate(http_request_duration_seconds_count{job="checkout"}[5m]))
)

Generators remove this boilerplate: Sloth and Pyrra take a short SLO spec and emit all recording rules and alerts. OpenSLO is a vendor-neutral spec format. Prefer a generator when there are more than two SLOs.

Low traffic (under ~1 request/min): burn-rate alerts flap on single errors. Add a minimum-event-count condition, use longer windows, or rely on synthetic probes.

6. Error budget policy

## Error budget policy — checkout (SLO 99.9% availability, 30d rolling)
- Budget remaining > 50%: normal feature velocity.
- 25–50%: each release requires a tested rollback; risky migrations need sign-off.
- < 25%: feature releases need approval from the service owner; reliability work first.
- Exhausted: freeze non-critical releases until the 30-day SLI recovers above target;
  incident action items take priority over roadmap work.
- A single incident consuming > 20% of budget requires a postmortem.
- Exceptions: security fixes and changes that improve reliability are always allowed.
Owner: checkout team lead. Reviewed quarterly.

7. Backtest and dashboard

  • Replay the alert expressions over the last 30–90 days of data (Grafana explore or promtool with recorded data). Every past customer-visible incident should have fired a page; quiet periods should not.
  • Dashboard per SLO: SLI over the window vs. target, budget remaining (%), burn rate 1h/6h, and the events that consumed budget.

Gotchas

  • Measuring at the app misses failures before the app (LB, DNS, TLS, crashed pods).
  • Including health checks and bots in valid events inflates the SLI.
  • rate() over a window with no traffic returns no data; alerts silently never fire. Decide how to treat no-data (often absent() alerts on the SLI itself).
  • Changing the SLI definition resets comparability; annotate the dashboard.
  • 100% is never the target; it forbids change and has no budget to spend.
  • An SLO nobody consults when planning is decoration. Tie it to the release policy.

Output

## SLO: checkout availability
- SLI: non-5xx responses / all requests at the ALB, excluding /healthz
- Target: 99.9% over rolling 30 days (last 90 days achieved: 99.96%)
- Budget: 43.2 min-equivalent / 0.1% of requests
- Alerts: page at 14.4x (1h & 5m) and 6x (6h & 30m); ticket at 1x (3d & 6h)
- Backtest: fired for both incidents in the last 90 days, 0 false pages
- Policy: linked; owner checkout team lead

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

scripts/burn_rate.py 116 lines · 4.6 KB
#!/usr/bin/env python3
"""Error budget and burn-rate calculator (stdlib only).

Examples:
  python3 scripts/burn_rate.py --slo 99.9
  python3 scripts/burn_rate.py --slo 99.9 --window-days 28 --error-ratio 0.004
  python3 scripts/burn_rate.py --slo 99.5 --good 9951234 --total 10000000

Prints the error budget, the standard multi-window multi-burn-rate alert thresholds
(Google SRE Workbook), and, if given, the current burn rate and budget remaining.
Exit code 1 when the budget is exhausted or the current burn rate would exhaust it
before the window ends; 0 otherwise; 2 on bad input.
"""
import argparse
import sys

# (budget fraction consumed, long window hours, short window hours, action)
POLICY = [
    (0.02, 1, 5 / 60, "page"),
    (0.05, 6, 0.5, "page"),
    (0.10, 72, 6, "ticket"),
]


def fmt_duration(minutes):
    if minutes < 120:
        return f"{minutes:.1f} min"
    hours = minutes / 60
    if hours < 72:
        return f"{hours:.1f} h"
    return f"{hours / 24:.1f} days"


def fmt_window(hours):
    if hours < 1:
        return f"{round(hours * 60)}m"
    if hours < 24 or hours % 24:
        return f"{hours:g}h"
    return f"{hours / 24:g}d"


def main():
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument("--slo", type=float, required=True, help="SLO target in percent, e.g. 99.9")
    p.add_argument("--window-days", type=float, default=30, help="SLO window in days (default 30)")
    p.add_argument("--error-ratio", type=float, help="current error ratio over a recent period (0-1)")
    p.add_argument("--good", type=float, help="good events so far in the window")
    p.add_argument("--total", type=float, help="valid events so far in the window")
    p.add_argument("--elapsed-days", type=float, help="days elapsed in the window (with --good/--total)")
    a = p.parse_args()

    if not 0 < a.slo < 100:
        print("error: --slo must be between 0 and 100 (exclusive)", file=sys.stderr)
        return 2
    budget = 1 - a.slo / 100
    window_h = a.window_days * 24
    window_min = window_h * 60

    print(f"SLO {a.slo}% over {a.window_days:g} days")
    print(f"  error budget: {budget:.4%} of events")
    print(f"  as downtime-equivalent: {fmt_duration(budget * window_min)} per window")
    print()
    print("Multi-window burn-rate alerts (alert when BOTH windows exceed the threshold):")
    print(f"  {'action':7} {'long':>5} {'short':>6} {'burn':>6} {'error ratio >':>14}  budget used at trigger")
    for frac, long_h, short_h, action in POLICY:
        burn = frac * window_h / long_h
        print(
            f"  {action:7} {fmt_window(long_h):>5} {fmt_window(short_h):>6} {burn:>6.1f} "
            f"{burn * budget:>14.5f}  {frac:.0%}"
        )
    print()
    print("PromQL shape (replace the recording rule names with yours):")
    frac, long_h, short_h, _ = POLICY[0]
    burn = frac * window_h / long_h
    print(
        f"  (slo:sli_error:ratio_rate{fmt_window(long_h)} > ({burn:.1f} * {budget:.6f}))\n"
        f"  and\n"
        f"  (slo:sli_error:ratio_rate{fmt_window(short_h)} > ({burn:.1f} * {budget:.6f}))"
    )

    status = 0
    if a.error_ratio is not None:
        if not 0 <= a.error_ratio <= 1:
            print("error: --error-ratio must be within 0..1", file=sys.stderr)
            return 2
        rate = a.error_ratio / budget if budget else float("inf")
        print()
        print(f"Current error ratio {a.error_ratio:.5f} -> burn rate {rate:.2f}x")
        if rate > 0:
            exhaust_h = window_h / rate
            print(f"  a full budget would be exhausted in {fmt_duration(exhaust_h * 60)} at this rate")
        if rate > 1:
            status = 1
            print("  WARNING: burning faster than the budget allows")

    if a.good is not None or a.total is not None:
        if a.good is None or a.total is None or a.total <= 0 or a.good > a.total:
            print("error: --good and --total must both be set with 0 <= good <= total, total > 0", file=sys.stderr)
            return 2
        bad = a.total - a.good
        allowed = budget * a.total
        remaining = 1 - (bad / allowed) if allowed else float("-inf")
        print()
        print(f"Window so far: {a.good / a.total:.5%} good ({bad:,.0f} bad of {a.total:,.0f})")
        print(f"  budget remaining: {remaining:.1%}")
        if a.elapsed_days:
            expected_used = a.elapsed_days / a.window_days
            print(f"  elapsed {a.elapsed_days:g}/{a.window_days:g} days; on-pace budget use would be {expected_used:.0%}")
        if remaining <= 0:
            status = 1
            print("  BUDGET EXHAUSTED: apply the error budget policy")
    return status


if __name__ == "__main__":
    sys.exit(main())

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc