Skip to main content

software_engineering/
error_budget.rs

1//! # SLIs, SLOs, and Error Budgets
2//!
3//! A **service level indicator (SLI)** measures something users genuinely
4//! experience, such as request success rate or latency. A **service level
5//! objective (SLO)** sets a target for that indicator. The **error budget**
6//! is the amount of unreliability the SLO permits — a spendable resource,
7//! not something to hoard, that gives both engineering and operations a
8//! shared, objective rule for when to ship faster and when to slow down for
9//! reliability work.
10//!
11//! ## Formula
12//!
13//! ```text
14//! Error budget (minutes) = (100 − SLO%) / 100 × period days × 24 × 60
15//! Burn rate               = actual downtime / error budget
16//! Budget exhausted         when burn rate ≥ 1.0
17//! ```
18//!
19//! ## Why it matters
20//!
21//! Calculating the error budget directly from the SLO, and tracking spending
22//! against it continuously, turns an abstract reliability target into an
23//! operational rule: agree in advance, before any specific incident, what
24//! happens when the budget is exhausted (a common, effective policy is that
25//! feature work pauses and priority shifts to reliability work). This
26//! predetermined rule removes the need to relitigate the trade-off under
27//! pressure during every individual incident. A healthy, unspent budget is
28//! not something to preserve untouched — it is permission to take
29//! reasonable, deliberate risks.
30//!
31//! ## Example
32//!
33//! The chapter's own worked example: a 99.9% availability target over 30
34//! days permits roughly 43 minutes of allowed downtime.
35//!
36//! ```rust
37//! use software_engineering::error_budget::{
38//!     error_budget_minutes, error_budget_burn_rate, is_error_budget_exhausted,
39//! };
40//!
41//! let budget = error_budget_minutes(99.9, 30.0);
42//! assert!((budget - 43.2).abs() < 1e-9);
43//!
44//! // 20 minutes of actual downtime against a ~43-minute budget: not exhausted.
45//! let burn_rate = error_budget_burn_rate(20.0, budget).unwrap();
46//! assert!(burn_rate < 1.0);
47//! assert!(!is_error_budget_exhausted(20.0, budget).unwrap());
48//! ```
49//!
50//! ## Pitfalls
51//!
52//! - **Setting an aspirational SLO with no evidence behind it** — produces a
53//!   target the team cannot realistically track or act on.
54//! - **Treating the error budget as something to preserve rather than
55//!   spend** — a budget that never gets spent suggests an overly
56//!   conservative team or an SLO set too loosely relative to actual
57//!   achieved reliability.
58//! - **No predetermined response to exhaustion** — forces the trade-off to
59//!   be relitigated under pressure during every individual incident.
60//! - **Reviewing SLOs only by inertia**, never against evidence of actual
61//!   achieved reliability or changed user expectations.
62//!
63//! ## Sources
64//!
65//! - Chapter 6.1, SLIs, SLOs, and error budgets.
66//!
67//! Topic doc: software-engineering-metrics/locales/en-001/chapters/06-01-slis-slos-and-error-budgets.md
68
69/// The allowed downtime, in minutes, implied by an availability SLO over a
70/// given period.
71///
72/// `(100.0 − slo_percent) / 100.0 × period_days × 24.0 × 60.0`.
73///
74/// # Arguments
75///
76/// * `slo_percent` — the availability target, e.g. `99.9` for 99.9%.
77/// * `period_days` — the length of the budget period, in days.
78///
79/// # Returns
80///
81/// The allowed downtime for the period, in minutes.
82///
83/// # Examples
84///
85/// ```rust
86/// use software_engineering::error_budget::error_budget_minutes;
87///
88/// // A 99.9% target over 30 days permits roughly 43 minutes of downtime.
89/// let budget = error_budget_minutes(99.9, 30.0);
90/// assert!((budget - 43.2).abs() < 1e-9);
91/// ```
92#[must_use]
93pub fn error_budget_minutes(slo_percent: f64, period_days: f64) -> f64 {
94    (100.0 - slo_percent) / 100.0 * period_days * 24.0 * 60.0
95}
96
97/// How much of the error budget has been spent.
98///
99/// `actual_downtime_minutes / budget_minutes`. A value at or above `1.0`
100/// means the budget is exhausted or overspent.
101///
102/// # Arguments
103///
104/// * `actual_downtime_minutes` — actual downtime observed in the period, in
105///   minutes.
106/// * `budget_minutes` — the allowed downtime for the period, in minutes
107///   (typically from [`error_budget_minutes`]).
108///
109/// # Returns
110///
111/// The burn rate as a proportion, or `None` if `budget_minutes` is zero.
112///
113/// # Examples
114///
115/// ```rust
116/// use software_engineering::error_budget::error_budget_burn_rate;
117///
118/// assert_eq!(error_budget_burn_rate(21.6, 43.2), Some(0.5));
119/// assert_eq!(error_budget_burn_rate(1.0, 0.0), None);
120/// ```
121#[must_use]
122pub fn error_budget_burn_rate(actual_downtime_minutes: f64, budget_minutes: f64) -> Option<f64> {
123    if budget_minutes == 0.0 {
124        return None;
125    }
126    Some(actual_downtime_minutes / budget_minutes)
127}
128
129/// Whether the error budget is exhausted: burn rate at or above `1.0`.
130///
131/// # Arguments
132///
133/// * `actual_downtime_minutes` — actual downtime observed in the period, in
134///   minutes.
135/// * `budget_minutes` — the allowed downtime for the period, in minutes.
136///
137/// # Returns
138///
139/// `Some(true)` if the budget is exhausted or overspent, `Some(false)`
140/// otherwise, or `None` if `budget_minutes` is zero.
141///
142/// # Examples
143///
144/// ```rust
145/// use software_engineering::error_budget::is_error_budget_exhausted;
146///
147/// assert_eq!(is_error_budget_exhausted(50.0, 43.2), Some(true));
148/// assert_eq!(is_error_budget_exhausted(20.0, 43.2), Some(false));
149/// assert_eq!(is_error_budget_exhausted(1.0, 0.0), None);
150/// ```
151#[must_use]
152pub fn is_error_budget_exhausted(actual_downtime_minutes: f64, budget_minutes: f64) -> Option<bool> {
153    let burn_rate = error_budget_burn_rate(actual_downtime_minutes, budget_minutes)?;
154    Some(burn_rate >= 1.0)
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    // "a 99.9% availability target over 30 days permits roughly 43 minutes
162    // of allowed downtime."
163    #[test]
164    fn error_budget_matches_the_ninety_nine_point_nine_percent_worked_example() {
165        let budget = error_budget_minutes(99.9, 30.0);
166        assert!((budget - 43.2).abs() < 1e-9);
167    }
168
169    #[test]
170    fn error_budget_burn_rate_computes_proportion_spent() {
171        let burn_rate = error_budget_burn_rate(21.6, 43.2).unwrap();
172        assert!((burn_rate - 0.5).abs() < 1e-9);
173    }
174
175    #[test]
176    fn error_budget_burn_rate_is_none_for_zero_budget() {
177        assert_eq!(error_budget_burn_rate(1.0, 0.0), None);
178    }
179
180    // "Agree, in advance ... what happens when the budget is exhausted."
181    #[test]
182    fn budget_is_exhausted_once_burn_rate_reaches_one() {
183        assert_eq!(is_error_budget_exhausted(50.0, 43.2), Some(true));
184        assert_eq!(is_error_budget_exhausted(20.0, 43.2), Some(false));
185    }
186
187    #[test]
188    fn budget_exhaustion_is_none_for_zero_budget() {
189        assert_eq!(is_error_budget_exhausted(1.0, 0.0), None);
190    }
191}