ЁЯПл The SchoolтА║ЁЯй║ ObservabilityтА║ЁЯОп рдзрдбрд╛ 09 тАФ SLIs, SLOs рдЖрдгрд┐ error budgets: рдкреБрд░реЗрд╕реЗ рдЪрд╛рдВрдЧрд▓реЗ рдореНрд╣рдгрдЬреЗ рдХрд╛рдп рддреЗ рдард░рд╡рд╛
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯОп рдзрдбрд╛ 09 тАФ SLIs, SLOs рдЖрдгрд┐ error budgets: рдкреБрд░реЗрд╕реЗ рдЪрд╛рдВрдЧрд▓реЗ рдореНрд╣рдгрдЬреЗ рдХрд╛рдп рддреЗ рдард░рд╡рд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 09 ┬╖ рдорд╛рдЧреАрд▓: lesson-08-alerting ┬╖ рдкреБрдвреАрд▓: lesson-10-incident-response


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ08, рдЖрдгрд┐ рднрд╛рдЧ 3 рдЪреА рд╕реБрд░реБрд╡рд╛рдд, рд╡рд┐рд╢реНрд╡рд╛рд╕рд╛рд░реНрд╣рддреЗрдЪреЗ рдЪрдХреНрд░: SLIs, SLOs, SLAs рдЖрдгрд┐ error budget тАФ рдЖрдгрд┐ рдЖрдХрдбреНрдпрд╛рдЪреЗ рдирд┐рд░реНрдгрдпрд╛рдд рд░реВрдкрд╛рдВрддрд░ рдХрд░рдгрд╛рд░реА error budget policy. error_budget() obs/reliability.py рдордзреНрдпреЗ рдЖрд╣реЗ; obs/demo.py рдордзрд▓реЗ slo() рдирд┐рдХрд╛рд▓рд╛рдЪреНрдпрд╛ рдкреВрд░реНрдг рджрд┐рд╡рд╕рд╛рдЪреА рдХрд┐рдВрдордд рдПрдХрд╛ рдорд╣рд┐рдиреНрдпрд╛рдЪреНрдпрд╛ рддреБрд▓рдиреЗрдд рдореЛрдЬрддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдПрдЦрд╛рджреА рд╢рд╛рд│рд╛ рд╡рдЪрди рджреЗрдК рд╢рдХрддреЗ рдХрд╛ рдХреА рдХреЛрдгрддрд╛рд╣реА рд╡рд┐рджреНрдпрд╛рд░реНрдереА рдХрдзреАрдЪ рдЖрдЬрд╛рд░реА рдкрдбрдгрд╛рд░ рдирд╛рд╣реА? рдирд╛рд╣реА. рд╡рд┐рджреНрдпрд╛рд░реНрдереНрдпрд╛рдВрдирд╛ рд╕рд░реНрджреА рд╣реЛрддреЗ. "рдХрдзреАрдЪ рдирд╛рд╣реА" рд╕рд╛рдареА рдкреНрд░рдпрддреНрди рдХрд░рдгрд╛рд░реА рд╢рд╛рд│рд╛ рдЖрдкрд▓реЗ рджрд░рд╡рд╛рдЬреЗ рдмрдВрдж рдХрд░реЗрд▓ рдЖрдгрд┐ рдХреБрдгрд╛рд▓рд╛рдЪ рд╢рд┐рдХрд╡рдгрд╛рд░ рдирд╛рд╣реА.

рдореНрд╣рдгреВрди рджреАрдкрд┐рдХрд╛ рдЖрдгрд┐ рдХрддрд░рд┐рдирд╛ рддреНрдпрд╛рдРрд╡рдЬреА рдПрдХрд╛ рдЖрдХрдбреНрдпрд╛рд╡рд░ рд╕рд╣рдордд рд╣реЛрддрд╛рдд: "рд╕рд╛рдорд╛рдиреНрдп рдорд╣рд┐рдиреНрдпрд╛рдд, рдкреНрд░рддрд┐-рд╡рд┐рджреНрдпрд╛рд░реНрдереА рд╢рд╛рд│реЗрдЪреНрдпрд╛ рджрд┐рд╡рд╕рд╛рдВрдкреИрдХреА рдкреНрд░рддреНрдпреЗрдХ 1,000 рдкреИрдХреА 999 рджрд┐рд╡рд╕ рдирд┐рд░реЛрдЧреА рдЕрд╕рд╛рд╡реЗрдд." рддреЗ рд╡рдЪрди рдореНрд╣рдгрдЬреЗ рдзреНрдпреЗрдп. рдЬреЗ 1,000 рдкреИрдХреА 1 рдЪреБрдХреВ рд╢рдХрддрд╛рдд рддреА рдореБрднрд╛ тАФ рдЖрдЬрд╛рд░реА-рд░рдЬреЗрдЪреА рдореБрднрд╛.

рдЖрддрд╛ рд╣реА рдореБрднрд╛ рдПрдХ рд╕рд╛рдзрди рдмрдирддреЗ. рднрд░рдкреВрд░ рдореБрднрд╛ рдЙрд░рд▓реЗрд▓реА рдЕрд╕рддрд╛рдирд╛, рд╢рд╛рд│рд╛ рдирд╡реНрдпрд╛ рдЧреЛрд╖реНрдЯреА рдХрд░реВрди рдкрд╛рд╣реВ рд╢рдХрддреЗ: рд╕рд╣рд▓, рдХреНрд░реАрдбрд╛ рджрд┐рди, рдирд╡реЗ рдЦреЗрд│рд╛рдЪреЗ рдореИрджрд╛рди. рдореБрднрд╛ рдЬрд╡рд│рдЬрд╡рд│ рд╕рдВрдкрдд рдЖрд▓реА рдХреА рджреАрдкрд┐рдХрд╛ рдореНрд╣рдгрддреЗ: "рдпрд╛ рдорд╣рд┐рдиреНрдпрд╛рдд рдирд╡реЗ рд╕рд╛рд╣рд╕ рдирд╛рд╣реА. рдЖрдзреА рдЧрд│рдгрд╛рд░реЗ рдЫрдд рдЖрдгрд┐ рддреБрдЯрд▓реЗрд▓рд╛ рдирд│ рджреБрд░реБрд╕реНрдд рдХрд░реВ."

рдХреЛрдгреАрд╣реА рднрд╛рд╡рдирд╛рдВрд╡рд░реВрди рд╡рд╛рдж рдШрд╛рд▓рдд рдирд╛рд╣реА. рддреНрдпрд╛ рдореБрднреЗрдХрдбреЗ рдкрд╛рд╣рддрд╛рдд.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    sli["ЁЯУП SLI<br/>good requests ├╖ all requests"]
    slo["ЁЯОп SLO<br/>99.9% over 30 days"]
    sla["ЁЯУЬ SLA<br/>a contract, with a penalty<br/>(looser, e.g. 99.5%)"]
    bud["ЁЯТ░ error budget = 1 тИТ SLO<br/>43,200 of 43,200,000 requests<br/>тЙИ 43.2 min of full outage"]
    day["ЁЯУЕ results day used 4,608 тЖТ 10.7%<br/>89.3% left"]
    pol{"тЪЦя╕П budget policy"}
    sli --> slo --> bud --> day --> pol
    slo -.-> sla
    pol -->|"budget left"| ship["ЁЯЪА ship features"]
    pol -->|"budget gone"| fix["ЁЯз░ freeze risky changes<br/>fix reliability first"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/observability/lesson-diagrams.html#l09

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг "рд╡рд┐рд╢реНрд╡рд╕рдиреАрдп" рд╣рд╛ рд╢рдмреНрдж рдЖрдХрдбрд╛ рдмрдиреЗрдкрд░реНрдпрдВрдд рдХрд╛рд╣реАрдЪ рдЕрд░реНрде рдирд╕рддреЛ, рдЖрдгрд┐ "рдЬрд▓рдж ship рдХрд░рд╛" рдЖрдгрд┐ "рдХрд╛рд│рдЬреА рдШреНрдпрд╛" рдпрд╛рдВрддрд▓рд╛ рд╡рд╛рдж рдЖрдХрдбреНрдпрд╛рд╢рд┐рд╡рд╛рдп рдХрдзреАрдЪ рд╕рдВрдкрдд рдирд╛рд╣реА. Budget рддреЛ рд╕рдВрдкрд╡рддреЛ: reliability рдПрдХ рд╕рдВрд╕рд╛рдзрди рдмрдирддреЗ рдЬреЗ team рдЦрд░реНрдЪ рдХрд░рддреЗ. Budget рдЙрд░рд▓реЗрд▓рд╛ рдЕрд╕рддрд╛рдирд╛, рдмрджрд▓рд╛рдЪреА рднреАрддреА рд╡рд┐рдирд╛рдХрд╛рд░рдг рдкреИрд╕рд╛ рдЦрд░реНрдЪ рдХрд░рддреЗ. Budget рд╕рдВрдкрд▓реНрдпрд╛рд╡рд░, рдЖрдгрдЦреА features users рдирд╛ рджрд┐рд▓реЗрд▓реЗ рд╡рдЪрди рдореЛрдбрддреАрд▓. рд╣рд╛рдЪ рдЖрдХрдбрд╛ рдзрдбрд╛ 08 рдордзрд▓реЗ burn-rate alerts рдЪрд╛рд▓рд╡рддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

obs/reliability.py рдордзрд▓реНрдпрд╛ error_budget(slo, total_events=None, bad_events=0, days=30) рдЪреЗ рджреЛрди modes рдЖрд╣реЗрдд. рдлрдХреНрдд SLO рджрд┐рд▓рд╛ рддрд░ рддреЗ budget рдкреВрд░реНрдг outage рдЪреНрдпрд╛ minutes рдореНрд╣рдгреВрди рджреЗрддреЗ: (1 тИТ slo) ├Ч days ├Ч 24 ├Ч 60. Event counts рджрд┐рд▓реЗ рддрд░ рддреЗ allowed_bad (total ├Ч (1 тИТ slo)), used, рдЖрдгрд┐ left_pct рджреЗрддреЗ. obs/demo.py рдордзрд▓реЗ slo() рдорд┐рдирд┐рдЯрд╛рд▓рд╛ 1,000 requests рдкреНрд░рдорд╛рдгреЗ 30 рджрд┐рд╡рд╕рд╛рдВрдЪрд╛ рдорд╣рд┐рдирд╛ (43,200,000) рдЖрдгрд┐ рджрд┐рд╡рд╕рднрд░рд╛рддреАрд▓ failed requests (sum(ERR) = 4,608) рдШреЗрддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

рдЖрдгрдЦреА SLOs рдЖрдгрд┐ 7-day window; рдордЧ рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ рд╡рд╛рдИрдЯ рджрд┐рд╡рд╕рд╛рдВрдирдВрддрд░рдЪрд╛ budget; рдордЧ рддреЛрдЪ рджрд┐рд╡рд╕ рдЕрдзрд┐рдХ рдХрдбрдХ SLO рд╡рд┐рд░реБрджреНрдз рдореЛрдЬрд▓реЗрд▓рд╛:

python3 obs/demo.py slo
python3 - <<'EOF'
import sys; sys.path.insert(0, "obs"); from reliability import error_budget
for slo in (0.99, 0.995, 0.999, 0.9995, 0.9999):
    print(f"SLO {slo:.2%} тЖТ {error_budget(slo)['minutes']:>6} min per 30 days ┬╖ {error_budget(slo, days=7)['minutes']:>5} min per 7 days")
month = 1000 * 60 * 24 * 30
for bad in (4_608, 21_600, 43_200):
    b = error_budget(0.999, month, bad)
    print(f"{bad:>6,} bad of {month:,} тЖТ allowed {b['allowed_bad']:,} ┬╖ left {b['left_pct']}%")
print("same day on a 99.99% SLO тЖТ", error_budget(0.9999, month, 4_608))
EOF
python3 obs/test_obs.py

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

slo рд╣реЗ print рдХрд░рддреЗ:

тФАтФА SLI: good requests / all requests ┬╖ SLO: 99.9% over 30 days ┬╖ SLA: the contract with a penalty
   SLO 99.00% тЖТ  432.0 minutes of full outage per 30 days
   SLO 99.90% тЖТ   43.2 minutes of full outage per 30 days
   SLO 99.99% тЖТ    4.3 minutes of full outage per 30 days
тФАтФА a 30-day window of 43,200,000 requests may have 43,200 failures ┬╖ today's 8 hours failed 4,608
   one bad day used 10.7% of the month's budget тЖТ 89.3% left
   budget left тЖТ ship faster ┬╖ budget gone тЖТ freeze risky changes and fix reliability first

рддреБрдордЪрд╛ snippet рд╣реЗ print рдХрд░рддреЛ:

SLO 99.00% тЖТ  432.0 min per 30 days ┬╖ 100.8 min per 7 days
SLO 99.50% тЖТ  216.0 min per 30 days ┬╖  50.4 min per 7 days
SLO 99.90% тЖТ   43.2 min per 30 days ┬╖  10.1 min per 7 days
SLO 99.95% тЖТ   21.6 min per 30 days ┬╖   5.0 min per 7 days
SLO 99.99% тЖТ    4.3 min per 30 days ┬╖   1.0 min per 7 days
 4,608 bad of 43,200,000 тЖТ allowed 43,200 ┬╖ left 89.3%
21,600 bad of 43,200,000 тЖТ allowed 43,200 ┬╖ left 50.0%
43,200 bad of 43,200,000 тЖТ allowed 43,200 ┬╖ left 0.0%
same day on a 99.99% SLO тЖТ {'allowed_bad': 4320, 'used': 4608, 'left_pct': -6.7}

Tests рдордзреНрдпреЗ тЬЕ L09 a 99.9% SLO allows 43.2 minutes a month рдЕрд╕рддреЛ.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

99.9% рд▓рд╛, рдирд┐рдХрд╛рд▓рд╛рдЪреНрдпрд╛ рджрд┐рд╡рд╕рд╛рдиреЗ тАФ рдПрдХ slow leak рдЖрдгрд┐ рдПрдХ bad deploy тАФ рдорд╣рд┐рдиреНрдпрд╛рдЪрд╛ 10.7% рд╡рд╛рдкрд░рд▓рд╛: рд╡рд╛рдИрдЯ, рдкрдг policy рдореНрд╣рдгрддреЗ "ship рдХрд░рдд рд░рд╛рд╣рд╛, рдХрд╛рд│рдЬреАрдкреВрд░реНрд╡рдХ". рддреЛрдЪ рджрд┐рд╡рд╕ 99.99% рд╡рд┐рд░реБрджреНрдз рдореЛрдЬрд▓рд╛ рддрд░ рддреНрдпрд╛рдиреЗ рдкреВрд░реНрдг рдорд╣рд┐рдиреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рд╡рд╛рдкрд░рд▓реЗ (тИТ6.7% рдЙрд░рд▓реЗ): рдПрдХрд╛рдЪ рджрд┐рд╡рд╕рд╛рдиреЗ freeze рд╕реБрд░реВ рд╣реЛрдИрд▓. рдкреНрд░рддреНрдпреЗрдХ рдЬрд╛рджрд╛ nine budget рд▓рд╛ рджрд╣рд╛рдиреЗ рднрд╛рдЧрддреЛ, рдореНрд╣рдгреВрди users рдирд╛ рдЬреЛ SLO рд▓рд╛рдЧрддреЛ рддреЛ рдирд┐рд╡рдбрд╛, рд╕рдЧрд│реНрдпрд╛рдд рдЫрд╛рди рд╡рд╛рдЯрдгрд╛рд░рд╛ рдирд╡реНрд╣реЗ. рдЖрдгрд┐ 99.95% рдЪреА 7-day window рдлрдХреНрдд 5 рдорд┐рдирд┐рдЯреЗ рджреЗрддреЗ тАФ рд▓рд╣рд╛рди windows рдЬрд▓рдж рдкреНрд░рддрд┐рд╕рд╛рдж рджреЗрддрд╛рдд рдЖрдгрд┐ рдХрдореА рдорд╛рдл рдХрд░рддрд╛рдд.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ 30 рджрд┐рд╡рд╕рд╛рдВрд╡рд░рдЪрд╛ availability SLI Prometheus recording rule рдореНрд╣рдгреВрди, рдЖрдгрд┐ рдЙрд░рд▓реЗрд▓рд╛ budget Grafana panel рд╡рд░ рд▓рд╛рд╡рддрд╛ рдпреЗрдИрд▓ рдЕрд╢рд╛ series рдореНрд╣рдгреВрди (SLO 99.9%):

groups:
  - name: results-api-slo-budget
    rules:
      - record: job:slo_availability:ratio_rate30d
        expr: |
          1 - (
            sum by (job) (increase(http_requests_total{job="results-api", status=~"5.."}[30d]))
            /
            sum by (job) (increase(http_requests_total{job="results-api"}[30d]))
          )
      - record: job:slo_error_budget_remaining:ratio
        expr: 1 - ((1 - job:slo_availability:ratio_rate30d) / (1 - 0.999))

Sloth рдЖрдгрд┐ Pyrra рд╕рд╛рд░рдЦреА tools рд╣реЗ rules тАФ рдЖрдгрд┐ рдзрдбрд╛ 08 рдЪреЗ burn-rate alerts тАФ рдПрдХрд╛ рдЫреЛрдЯреНрдпрд╛ SLO file рдордзреВрди рддрдпрд╛рд░ рдХрд░рддрд╛рдд:

version: prometheus/v1
service: results-api
slos:
  - name: requests-availability
    objective: 99.9
    sli:
      events:
        error_query: sum(rate(http_requests_total{job="results-api",status=~"5.."}[{{.window}}]))
        total_query: sum(rate(http_requests_total{job="results-api"}[{{.window}}]))
    alerting:
      name: ResultsApiAvailability
      page_alert: {labels: {severity: page}}
      ticket_alert: {labels: {severity: ticket}}

Grafana (Grafana Cloud SLO), Datadog SLOs рдЖрдгрд┐ CloudWatch Application Signals рд╕реБрджреНрдзрд╛ рддреНрдпрд╛рдВрдЪреНрдпрд╛ UI рдордзреНрдпреЗ SLOs рдард░рд╡рддрд╛рдд рдЖрдгрд┐ рдЙрд░рд▓реЗрд▓рд╛ budget рджрд╛рдЦрд╡рддрд╛рдд.

рд╕рд╣рдордд рд╣реЛрдКрди рд╕рд╣реА рдХрд░рд╛рдпрдЪреА рдПрдХрд╛ рдкрд╛рдирд╛рдЪреА error budget policy:

# Error budget policy тАФ results-api (SLO 99.9% availability, rolling 30 days)
- Budget > 25% left: normal releases.
- Budget < 25% left: releases need a canary of 30 minutes; no big-bang changes.
- Budget spent: feature freeze. Only reliability and security fixes ship until the 30-day SLI is back above 99.9%.
- Any single incident that used > 20% of the budget: postmortem within 5 working days; its P0 action items come before features.
- Disagreements go to: the head of engineering and the product owner, together.
Signed: Dipika (engineering) ┬╖ Aishwarya (product) ┬╖ date: 2026-09-27

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: policy рд╢рд┐рд╡рд╛рдпрдЪрд╛ SLO рдореНрд╣рдгрдЬреЗ рдлрдХреНрдд рдПрдХ graph. Budget рд╕рдВрдкрд▓реНрдпрд╛рд╡рд░ team рдХрд╛рдп рдХрд░рдгреЗ рдерд╛рдВрдмрд╡реЗрд▓ рддреЗ рдЖрдзреАрдЪ рд▓рд┐рд╣реВрди рдареЗрд╡рд╛ тАФ рдЖрдгрд┐ engineering рд╡ product рджреЛрдШрд╛рдВрдиреАрд╣реА рддреНрдпрд╛рд╡рд░ рд╕рд╣реА рдХрд░реВ рджреНрдпрд╛.

тПня╕П рдкреБрдвреЗ

Minute 409 рд▓рд╛ page рд╡рд╛рдЬрддреЗ. рдЖрддрд╛ рдХрд╛рдп? рдиреЗрддреГрддреНрд╡ рдХреЛрдг рдХрд░рддреЗ, рджреБрд░реБрд╕реНрддреА рдХреЛрдг рдХрд░рддреЗ, рдкрд╛рд▓рдХрд╛рдВрдирд╛ рдХреЛрдг рд╕рд╛рдВрдЧрддреЗ? рдЕрдЧреНрдирд┐рд╢рдорди рд╕рд░рд╛рд╡.

git checkout lesson-10-incident-response

ЁЯОп Lesson 09 тАФ SLIs, SLOs & error budgets: decide what good enough means

ЁЯУН You are here: Lesson 09 of 12 ┬╖ Previous: lesson-08-alerting ┬╖ Next: lesson-10-incident-response


ЁЯУж What's in this branch

Lessons 01тАУ08, plus the start of Part 3, the reliability loop: SLIs, SLOs, SLAs and the error budget тАФ and the error budget policy that turns a number into a decision. error_budget() lives in obs/reliability.py; slo() in obs/demo.py prices the whole results day against a month.

ЁЯзТ Explain like I'm 5

Can a school promise that no pupil is ever ill? No. Pupils catch colds. A school that tries for "never" would close its doors and teach nobody.

So Dipika and Katrina agree on a number instead: "In a normal month, 999 of every 1,000 school days-per-pupil should be healthy ones." That promise is the target. The 1 in 1,000 that may go wrong is the allowance тАФ the sick-day allowance.

Now the allowance becomes a tool. While there is plenty left, the school can try new things: a trip, a sports day, a new playground. When the allowance is nearly gone, Dipika says: "No new adventures this month. We fix the leaking roof and the broken tap first."

Nobody argues about feelings. They look at the allowance.

ЁЯЧ║я╕П Diagram

flowchart LR
    sli["ЁЯУП SLI<br/>good requests ├╖ all requests"]
    slo["ЁЯОп SLO<br/>99.9% over 30 days"]
    sla["ЁЯУЬ SLA<br/>a contract, with a penalty<br/>(looser, e.g. 99.5%)"]
    bud["ЁЯТ░ error budget = 1 тИТ SLO<br/>43,200 of 43,200,000 requests<br/>тЙИ 43.2 min of full outage"]
    day["ЁЯУЕ results day used 4,608 тЖТ 10.7%<br/>89.3% left"]
    pol{"тЪЦя╕П budget policy"}
    sli --> slo --> bud --> day --> pol
    slo -.-> sla
    pol -->|"budget left"| ship["ЁЯЪА ship features"]
    pol -->|"budget gone"| fix["ЁЯз░ freeze risky changes<br/>fix reliability first"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/observability/lesson-diagrams.html#l09

тЭУ What

ЁЯдФ Why

Because "reliable" means nothing until it is a number, and the argument between "ship faster" and "be careful" never ends without one. The budget ends it: reliability becomes a resource that the team spends. With budget left, fear of change costs money for no reason. With the budget gone, more features would break a promise to users. The same number drives the burn-rate alerts in lesson 08.

ЁЯФз How (in this repo)

error_budget(slo, total_events=None, bad_events=0, days=30) in obs/reliability.py has two modes. With only an SLO it returns the budget as minutes of complete outage: (1 тИТ slo) ├Ч days ├Ч 24 ├Ч 60. With event counts it returns allowed_bad (total ├Ч (1 тИТ slo)), used, and left_pct. slo() in obs/demo.py takes a 30-day month at 1,000 requests a minute (43,200,000) and the day's failed requests (sum(ERR) = 4,608).

ЁЯзк Try it

More SLOs and a 7-day window; then the budget after different bad days; then the same day measured against a stricter SLO:

python3 obs/demo.py slo
python3 - <<'EOF'
import sys; sys.path.insert(0, "obs"); from reliability import error_budget
for slo in (0.99, 0.995, 0.999, 0.9995, 0.9999):
    print(f"SLO {slo:.2%} тЖТ {error_budget(slo)['minutes']:>6} min per 30 days ┬╖ {error_budget(slo, days=7)['minutes']:>5} min per 7 days")
month = 1000 * 60 * 24 * 30
for bad in (4_608, 21_600, 43_200):
    b = error_budget(0.999, month, bad)
    print(f"{bad:>6,} bad of {month:,} тЖТ allowed {b['allowed_bad']:,} ┬╖ left {b['left_pct']}%")
print("same day on a 99.99% SLO тЖТ", error_budget(0.9999, month, 4_608))
EOF
python3 obs/test_obs.py

тЬЕ Verify тАФ what you should see

slo prints:

тФАтФА SLI: good requests / all requests ┬╖ SLO: 99.9% over 30 days ┬╖ SLA: the contract with a penalty
   SLO 99.00% тЖТ  432.0 minutes of full outage per 30 days
   SLO 99.90% тЖТ   43.2 minutes of full outage per 30 days
   SLO 99.99% тЖТ    4.3 minutes of full outage per 30 days
тФАтФА a 30-day window of 43,200,000 requests may have 43,200 failures ┬╖ today's 8 hours failed 4,608
   one bad day used 10.7% of the month's budget тЖТ 89.3% left
   budget left тЖТ ship faster ┬╖ budget gone тЖТ freeze risky changes and fix reliability first

Your snippet prints:

SLO 99.00% тЖТ  432.0 min per 30 days ┬╖ 100.8 min per 7 days
SLO 99.50% тЖТ  216.0 min per 30 days ┬╖  50.4 min per 7 days
SLO 99.90% тЖТ   43.2 min per 30 days ┬╖  10.1 min per 7 days
SLO 99.95% тЖТ   21.6 min per 30 days ┬╖   5.0 min per 7 days
SLO 99.99% тЖТ    4.3 min per 30 days ┬╖   1.0 min per 7 days
 4,608 bad of 43,200,000 тЖТ allowed 43,200 ┬╖ left 89.3%
21,600 bad of 43,200,000 тЖТ allowed 43,200 ┬╖ left 50.0%
43,200 bad of 43,200,000 тЖТ allowed 43,200 ┬╖ left 0.0%
same day on a 99.99% SLO тЖТ {'allowed_bad': 4320, 'used': 4608, 'left_pct': -6.7}

The tests include тЬЕ L09 a 99.9% SLO allows 43.2 minutes a month.

ЁЯПБ What you just proved

At 99.9%, results day тАФ a slow leak and a bad deploy тАФ used 10.7% of the month: bad, but the policy says "keep shipping, carefully". The same day against 99.99% used more than the whole month (тИТ6.7% left): one day would trigger a freeze. Every extra nine divides the budget by ten, so choose the SLO users need, not the one that sounds best. And a 7-day window at 99.95% allows only 5 minutes тАФ shorter windows react faster and forgive less.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ the availability SLI as a Prometheus recording rule over 30 days, and the budget left as a series you can put on a Grafana panel (SLO 99.9%):

groups:
  - name: results-api-slo-budget
    rules:
      - record: job:slo_availability:ratio_rate30d
        expr: |
          1 - (
            sum by (job) (increase(http_requests_total{job="results-api", status=~"5.."}[30d]))
            /
            sum by (job) (increase(http_requests_total{job="results-api"}[30d]))
          )
      - record: job:slo_error_budget_remaining:ratio
        expr: 1 - ((1 - job:slo_availability:ratio_rate30d) / (1 - 0.999))

Tools such as Sloth and Pyrra generate these rules тАФ and the lesson 08 burn-rate alerts тАФ from a short SLO file:

version: prometheus/v1
service: results-api
slos:
  - name: requests-availability
    objective: 99.9
    sli:
      events:
        error_query: sum(rate(http_requests_total{job="results-api",status=~"5.."}[{{.window}}]))
        total_query: sum(rate(http_requests_total{job="results-api"}[{{.window}}]))
    alerting:
      name: ResultsApiAvailability
      page_alert: {labels: {severity: page}}
      ticket_alert: {labels: {severity: ticket}}

Grafana (Grafana Cloud SLO), Datadog SLOs and CloudWatch Application Signals also define SLOs in their UI and show the budget left.

A one-page error budget policy to agree and sign:

# Error budget policy тАФ results-api (SLO 99.9% availability, rolling 30 days)
- Budget > 25% left: normal releases.
- Budget < 25% left: releases need a canary of 30 minutes; no big-bang changes.
- Budget spent: feature freeze. Only reliability and security fixes ship until the 30-day SLI is back above 99.9%.
- Any single incident that used > 20% of the budget: postmortem within 5 working days; its P0 action items come before features.
- Disagreements go to: the head of engineering and the product owner, together.
Signed: Dipika (engineering) ┬╖ Aishwarya (product) ┬╖ date: 2026-09-27

ЁЯПн Why this matters in production: an SLO without a policy is a graph. Write down, in advance, what the team will stop doing when the budget is gone тАФ and have both engineering and product sign it.

тПня╕П Next

The page fires at minute 409. Now what? Who leads, who fixes, who tells the parents? The fire drill.

git checkout lesson-10-incident-response
тЖР PreviousalertingNext тЖТincident response

This page is the lesson's README from the lesson-09-slos branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.