Module 2: Slis Slos And The Error Budget
4. Hands-on: the error budget calculator
Description
This is the lesson where the vocabulary turns into a tool. You're going to write scripts/error_budget_calculator.py, run it for real with python3 over a fixed 30-day dataset of process-shipment-manifest traffic, and get a literal output: the measured SLI, the comparison against the SLO, and the exact minutes of error budget left. No figure in this lesson is invented or rounded by hand — the result you're going to read is exactly what that script produces, always, every time you run it.
Connection to the module
Lesson 3 defined, in writing, what counts as a good event and what counts as a valid event for process-shipment-manifest. This lesson applies that definition over 30 days of real data — not hypothetical like Module 1 lesson 6's 99.9%, but a hand-designed dataset with the same discipline this whole guide demands: fixed, committed, no random. This lesson's result is the foundation lessons 5, 6, and 7 of this module build directly on, without rewriting the script from scratch.
Step 1 — The fixed dataset: 30 days of process-shipment-manifest traffic
Before the code, the data. Each row represents a day, with two numbers you already know from lesson 3: valid events (invocations triggered by real manifest uploads) and good events (the ones that finished with no exception, within the timeout, with a correct record in Shipments). The weekly pattern is real: Andes Cargo receives more manifests on weekdays (its carrier customers' business days) and fewer on weekends — and there's a deliberate incident on days 17 and 18, a two-day partial degradation lesson 6 of this module is going to use to demonstrate burn rate.
| Week | Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|---|
| 1 (days 1-7) | 430 | 445 | 452 | 438→437 (1 error) | 460 | 310 | 295 |
| 2 (days 8-14) | 430 | 445 | 452 | 438 | 460 | 310 | 295 |
| 3 (days 15-21) | 430 | 445 | 452→446 (6 errors) | 438→435 (3 errors) | 460 | 310 | 295 |
| 4 (days 22-28) | 430 | 445→444 (1 error) | 452 | 438 | 460 | 310 | 295 |
| 5 (days 29-30) | 430 | 445 | — | — | — | — | — |
Each cell is the number of valid events that day; where there's an arrow, the second number is how many of those events were good — the rest, the difference, are bad events (exceptions, timeouts, or throttled invocations, per lesson 3). Days with no arrow had zero bad events: every valid event was good.
Step 2 — The complete script
Create scripts/error_budget_calculator.py at the root of andes-cargo-infra/:
# error_budget_calculator.py
# Real error budget calculator: SLI, comparison against the SLO, minutes of budget.
# Deterministic: same dataset + same SLO = same result, always. No random, no datetime.now().
# --- The fixed 30-day process-shipment-manifest traffic dataset ---
# Each tuple: (day, valid_events, good_events)
TRAFFIC_30_DAYS = [
(1, 430, 430), (2, 445, 445), (3, 452, 452), (4, 438, 437), (5, 460, 460),
(6, 310, 310), (7, 295, 295), (8, 430, 430), (9, 445, 445), (10, 452, 452),
(11, 438, 438), (12, 460, 460), (13, 310, 310), (14, 295, 295), (15, 430, 430),
(16, 445, 445), (17, 452, 446), (18, 438, 435), (19, 460, 460), (20, 310, 310),
(21, 295, 295), (22, 430, 430), (23, 445, 444), (24, 452, 452), (25, 438, 438),
(26, 460, 460), (27, 310, 310), (28, 295, 295), (29, 430, 430), (30, 445, 445),
]
SLO = 0.999 # 99.9% monthly, the same reference level from Module 1
SLO_WINDOW_DAYS = 30 # the window the SLO promises
SLO_WINDOW_MINUTES = SLO_WINDOW_DAYS * 24 * 60
def compute_sli(dataset):
"""SLI = good events / valid events, summed over the whole dataset."""
total_valid = sum(row[1] for row in dataset)
total_good = sum(row[2] for row in dataset)
return total_good / total_valid, total_good, total_valid
def error_budget_minutes(slo=SLO, window_minutes=SLO_WINDOW_MINUTES):
"""error budget = (1 - SLO) x minutes of the window the SLO promises."""
return (1 - slo) * window_minutes
def budget_report(dataset, slo=SLO, window_minutes=SLO_WINDOW_MINUTES):
sli, total_good, total_valid = compute_sli(dataset)
observed_error_rate = 1 - sli
budget_minutes = error_budget_minutes(slo, window_minutes)
minutes_consumed = observed_error_rate * window_minutes
minutes_remaining = budget_minutes - minutes_consumed
consumed_pct = (minutes_consumed / budget_minutes) * 100
return {
"total_valid": total_valid,
"total_good": total_good,
"sli": sli,
"slo": slo,
"observed_error_rate": observed_error_rate,
"budget_minutes": budget_minutes,
"minutes_consumed": minutes_consumed,
"minutes_remaining": minutes_remaining,
"consumed_pct": consumed_pct,
}
def print_report(report, label):
print(f"--- {label} ---")
print(f"Valid events: {report['total_valid']}")
print(f"Good events: {report['total_good']}")
print(f"Measured SLI: {report['sli']:.4%}")
print(f"Target SLO: {report['slo']:.1%}")
print(f"Budget (monthly): {report['budget_minutes']:.1f} minutes")
print(f"Consumed this period: {report['minutes_consumed']:.2f} minutes ({report['consumed_pct']:.1f}% of budget)")
print(f"Remaining budget: {report['minutes_remaining']:.2f} minutes")
if __name__ == "__main__":
report = budget_report(TRAFFIC_30_DAYS)
print_report(report, "process-shipment-manifest -- 30 days of traffic")
Each function has exactly one job, deliberately: compute_sli implements exactly lesson 3's formula (good ÷ valid); error_budget_minutes is the same formula from Module 1, lesson 6, now as a reusable function instead of loose constants; budget_report is this lesson's new piece — it translates an SLI measured with events (a ratio) into minutes consumed over a time window (the same kind of number you already used in Module 1). This translation is the bridge between "how many manifests failed" and "how many minutes of allowed downtime got spent," and it's exactly what makes this calculator generalize Module 1's manual calculation instead of repeating it.
Step 3 — Running the calculator
python3 scripts/error_budget_calculator.py
What to expect (literal — run this script exactly as it is, and this is the result, always, with no variation):
--- process-shipment-manifest -- 30 days of traffic ---
Valid events: 12195
Good events: 12184
Measured SLI: 99.9098%
Target SLO: 99.9%
Budget (monthly): 43.2 minutes
Consumed this period: 38.97 minutes (90.2% of budget)
Remaining budget: 4.23 minutes
Reading the result: why 4.23 minutes is the number that matters
Out of 12,195 valid invocations in 30 days, 11 finished badly — the errors from days 4, 17, 18, and 23 you already saw in the dataset table. That gives an SLI of 99.9098%, just above the 99.9% SLO. Translated into minutes over a monthly window: out of the 43.2 minutes of budget the SLO allows, this month consumed 38.97 — 90.2% of the entire budget — leaving 4.23 minutes for the four days remaining until the month closes.
This isn't a comfortable result, and that discomfort is exactly this lesson's point: an SLI of 99.91% "sounds" good — it is, after all, higher than the SLO — but looked at through the error budget, this month was one more bad day away from exhausting the entire budget. It's the same distinction lesson 1 of this module already previewed with the data plan analogy: the balance is still positive, but positive isn't the same as comfortable. Lesson 6 of this module is going to show, with the same precision, when during the month that budget got spent fastest — information that "SLI: 99.9098%" alone never reveals.
Common mistakes
Reading "99.9098% > 99.9%" and concluding the month was completely healthy (looking only at the SLI, not the budget). What happens: someone sees the measured SLI beat the SLO and closes the analysis there, without checking the percentage of budget consumed. How to spot it: if your conclusion about this result is "all good, we beat the SLO" with no mention of the 90.2% consumed. How to fix it: an SLI just above the SLO can, at the same time, represent an almost-exhausted budget — those are two ways of reading the same number, and the second one (remaining minutes) is the one that really matters for deciding whether there's margin to take risk for the rest of the month. budget_report calculates both on purpose, so neither one gets read alone.
Modifying TRAFFIC_30_DAYS to "try other numbers" before running the script as-is (skipping verification). What happens: someone, before confirming the script produces exactly this lesson's output, is already experimenting with their own values. How to spot it: if your first run of this script doesn't match this lesson's "What to expect" block. How to fix it: run the script exactly as it is first — if your output doesn't match this lesson's digit for digit, there's a transcription error in your copy of the dataset or the code, not an expected variation (this script has no possible source of variation). Confirm the exact match before modifying anything.
Confusing SLO_WINDOW_DAYS (the window the SLO promises) with the number of days in the dataset (assuming they're always the same number). What happens: someone assumes SLO_WINDOW_DAYS always has to match len(dataset). How to spot it: if your mental model of this function can't explain what would happen if you passed it a 7-day dataset. How to fix it: budget_report calculates the budget over the window the SLO promises (30 days, monthly) regardless of how many days of real data you pass it — this separation is intentional, and it's exactly what lesson 7 of this module is going to take advantage of, when measuring a single bad week against the full monthly budget, not against a seven-day budget.
Exercises
Exercise 1 — Verify minutes_consumed's calculation by hand for a single day, without running the script. Using only day 17 of the dataset (452 valid events, 446 good), calculate what error percentage that specific day had, and what fraction of the monthly budget (43.2 minutes) it would represent if the whole month had had that same error rate.
See solution
Day 17's error rate: (452 − 446) / 452 = 6/452 ≈ 1.327%. If that rate held for all 30 days, the minutes consumed would be: 1.327% × 43,200 minutes (the full window in minutes) ≈ 573.5 minutes — more than 13 times the full 43.2-minute monthly budget. This confirms, with a manual calculation, the intuition from the "Reading the result" section: a single day like day 17, sustained, would exhaust the month's budget many times over — the same burn rate idea lesson 6 of this module formalizes with a formula and a dedicated function.
Exercise 2 — Modify the script, without running it yet, so it also prints how many days in the dataset had at least one bad event. Write the line or lines of code you'd add, using the structures already present in the script.
See solution
bad_days = [row[0] for row in TRAFFIC_30_DAYS if row[2] < row[1]]
print(f"Days with at least one bad event: {len(bad_days)} -> {bad_days}")
With this lesson's dataset, that line would print Days with at least one bad event: 4 -> [4, 17, 18, 23] — the same four days the Step 1 table already marked with an arrow. The exercise practices reading the dataset as a list of tuples and filtering on the difference between row[1] (valid) and row[2] (good), the same operation compute_sli already does at the aggregate level.
Exercise 3 — Explain why budget_report receives slo and window_minutes as parameters with a default value, instead of directly using the SLO and SLO_WINDOW_MINUTES constants inside the function. What does this design let you do that you couldn't do if the function used the global constants directly?
See solution
Using parameters with a default value (slo=SLO, window_minutes=SLO_WINDOW_MINUTES) lets you call budget_report(dataset) with nothing specified — it uses this lesson's reference values, 99.9% over 30 days — but it also lets you call budget_report(dataset, slo=0.99) to compare a different SLO, with no need to edit any global constant or duplicate the function. If the function read SLO directly from the global scope instead of receiving it as a parameter, the only way to test a different SLO would be changing the global constant — which would affect any other call to the function in the same program, not just the one you want to test. This design is, literally, what makes lesson 5 of this module possible: running the same calculator, on the same dataset, with three different SLOs, with no code change except the argument passed in.
Summary and next step
In this lesson you built and really ran the first version of scripts/error_budget_calculator.py: over 30 days of fixed process-shipment-manifest traffic, with 11 bad events out of 12,195 valid, the measured SLI was 99.9098% — just above the 99.9% reference SLO, but with only 4.23 minutes of a 43.2-minute budget remaining, 90.2% already consumed. You confirmed the script is fully deterministic — the same dataset and the same SLO always produce exactly the same result — and you understood why budget_report separates the SLO's window from the dataset's number of days, a design decision lessons 5 and 7 of this module are going to take direct advantage of.
Before moving on you should be able to: run the script and get exactly 99.9098% and 4.23 minutes; explain the difference between "the SLI beat the SLO" and "there's real margin left in the budget"; and modify the script to add a simple derived metric, like the one in Exercise 2.
Lesson 5 uses this same calculator, without changing the dataset, to answer a different question: what would have happened with this same real traffic if the chosen SLO had been 99% instead of 99.9%? What about 99.99%?
Resources
- Google — The Art of SLOs (Participant Handbook) — the SLI formula
compute_sliimplements. - Google SRE Workbook — Implementing SLOs — the error budget formula
error_budget_minutesimplements. - This same repository, Module 1, lesson 6 (
06-hands-on-the-error-budget-the-claude-code-incident-burned.md) — the single-use manual calculation this calculator generalizes. - This same repository, Module 2, lesson 3 (
03-hands-on-your-first-sli-definition.md) — the exact definition of "good" and "valid" this lesson's dataset applies day by day.