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}