error-budget-tests
Build error-budget gate tests - SLO + error-budget calculation per Google SRE workbook ("difference between target uptime and actual uptime"); burn-rate alerting; monthly-budget exhaustion test; freeze-trigger when budget consumed. Per sre.google embracing-risk reference. Includes the incident-metrics reference for MTTR / MTBF / MTTD / MTTA - per-incident record schema, calculation formulae, exclusion rules, dashboards-as-code, and target-vs-actual alerting. Use when an SLO and error budget are written down but nothing verifies that burn-rate alerts fire or that the release freeze engages when the budget runs out, or when MTTR / MTBF dashboards report numbers nobody can reproduce.
Install with skills.sh (any agent)
npx skills add testland/qa --skill error-budget-testserror-budget-tests
Per Google SRE - Embracing Risk (opens in new window), "the difference between [SLO] and [actual uptime] is the 'budget' of how much 'unreliability' is remaining for the quarter." When the budget is consumed, releases freeze. Tests verify this contract is enforced.
When to use
Step 1 - Define the SLI + SLO
| Element | Example |
|---|---|
| SLI (indicator) | successful_requests / total_requests over rolling 30-day window |
| SLO (objective) | 99.9% over 30 days |
| Error budget | 100% − 99.9% = 0.1% of 30 days = ~43.2 minutes downtime allowed per 30 days |
Per Google SRE - Embracing Risk (opens in new window): "A failure affecting 0.0002% of queries consumes 20% of a 0.001% quarterly budget."
Step 2 - Test SLI calculation
def test_sli_excludes_planned_maintenance():
requests = [
# Normal traffic
Request(success=True, ts=t1, was_maintenance=False),
Request(success=False, ts=t2, was_maintenance=False),
# Planned maintenance - should NOT count against SLO
Request(success=False, ts=t3, was_maintenance=True),
]
sli = compute_sli(requests)
# 1 success / 2 non-maintenance = 0.5 (not 1/3)
assert sli == 0.5Maintenance windows + planned outages: agreed-upon exclusions matter. Test the rule.
Step 3 - Test budget consumption
def test_30_min_outage_consumes_70_percent_of_monthly_budget():
"""30 days × 0.1% = 43.2 min budget. 30 min outage = 69%."""
monthly_budget_min = 30 * 24 * 60 * 0.001 # 43.2 min
incident_duration_min = 30
consumed_pct = (incident_duration_min / monthly_budget_min) * 100
assert 65 < consumed_pct < 75Step 4 - Burn-rate alerting
Per the SRE workbook, burn-rate alerting fires when budget is being consumed faster than safe.
| Window | Burn rate | Alert |
|---|---|---|
| 1 hour | 14.4× | "Critical - page" (consumes 2% in 1 hr) |
| 6 hours | 6× | "Major - ticket" (consumes 5% in 6 hr) |
def test_critical_burn_alert_fires_at_14_4x():
# Simulate 1-hour window with 14.4× burn
error_rate_in_window = 0.0144 # 1.44%; 14.4× the 0.1% SLO threshold
alert = burn_rate_alert(window_seconds=3600, observed_rate=error_rate_in_window)
assert alert.severity == "critical"
assert alert.routes_to == "page"Test both directions: burn at 14.4× → critical; below threshold → no alert.
Step 5 - Freeze-trigger test
Per Google SRE - Embracing Risk (opens in new window): "If SLO violations occur frequently enough to expend the error budget, releases are temporarily halted."
def test_freeze_engaged_when_budget_below_zero():
# Budget tracker reports negative (over-spent)
budget_state = BudgetTracker(slo=0.999, window_days=30)
budget_state.record_outage_minutes(60) # 30-day budget is 43 min
assert budget_state.remaining_seconds < 0
assert release_gate(budget_state).should_freeze() is True
assert release_gate(budget_state).reason == "Error budget exhausted"Step 6 - Reset on rolling window
def test_budget_resets_as_old_outages_age_out():
# Outage 35 days ago; rolling 30-day window has aged it out
tracker = BudgetTracker(slo=0.999, window_days=30)
tracker.record_outage(when=now - timedelta(days=35), duration=timedelta(minutes=60))
# Window doesn't include 35-day-old event
assert tracker.remaining_seconds > 0Step 7 - Multi-window multi-burn-rate (Google SRE practice)
The SRE workbook recommends multi-window burn-rate alerts to balance sensitivity vs noise:
| Long window | Short window | Burn rate threshold | Alert |
|---|---|---|---|
| 1 hr | 5 min | 14.4× | Page |
| 6 hr | 30 min | 6× | Page |
| 3 day | 6 hr | 1× | Ticket |
The short window confirms the long window isn't a stale alert. Both must trigger.
def test_both_windows_must_trigger_to_page():
# Long window says "burn rate high"; short window says "stopped"
long_burn = 14.5
short_burn = 0.5
page_fired = multi_window_alert(long_burn, short_burn,
threshold_long=14.4, threshold_short=14.4)
assert page_fired is False # don't page when issue resolvedStep 8 - Stakeholder reporting
Per Google SRE - Embracing Risk (opens in new window): "Rather than political negotiations, teams reference objective metrics." Report budget remaining to product + leadership:
def test_weekly_budget_report_format():
report = weekly_budget_report(service="orders", week=current_week)
assert "remaining_minutes" in report
assert "burn_rate" in report
assert "incidents_this_window" in report
assert "freeze_status" in report
# Format: machine + human readable (CSV + Slack message)Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| SLO with no enforcement (no freeze) | Targets ignored; reliability degrades | Step 5 freeze-trigger |
| Single burn-rate alert | Either too noisy or too late | Step 7 multi-window |
| Include maintenance in SLI | Planned outages eat real budget | Step 2 exclusion |
| 99.999% SLO ("five nines") for everything | 26 sec/month budget; constant freeze | Tier SLOs per criticality |
| No reporting | Stakeholders don't internalize | Step 8 weekly cadence |
Limitations
References
MTTR / MTBF / MTTD / MTTA tracking
View source (opens in new window)MTTR / MTBF / MTTD / MTTA tracking
Reference for error-budget-tests - the four canonical incident-response metrics: incident-record schema, calculation formulae, dashboards-as-code, and target-vs-actual alerting. Incidents are tracked in your IR tool (PagerDuty, Opsgenie, FireHydrant, custom); this reference defines the schema + formulae so dashboards reflect reality.
When to use
How to use
Step 1 - Per-incident schema
Required fields:
{
"incident_id": "INC-2026-05-06-001",
"service": "orders",
"severity": "SEV-1",
"detected_at": "2026-05-06T10:23:14Z",
"acknowledged_at": "2026-05-06T10:25:02Z",
"mitigated_at": "2026-05-06T10:54:11Z",
"resolved_at": "2026-05-06T11:42:33Z",
"root_cause_category": "deployment-config",
"is_planned_maintenance": false,
"customer_impact": true
}Distinct timestamps for detected / acknowledged / mitigated (impact stopped) / resolved (root cause remediated). Conflating them inflates / deflates metrics.
Step 2 - Calculation formulae
MTTD = mean(detected_at − incident_start_at)
MTTA = mean(acknowledged_at − detected_at)
MTTR = mean(mitigated_at − detected_at) # OR resolved_at depending on definition
MTBF = mean(time between mitigation of one incident and detection of next)| Metric | Window | Lower / Higher |
|---|---|---|
| MTTD | rolling 90 days | Lower better (faster detection) |
| MTTA | rolling 90 days | Lower better (responsive on-call) |
| MTTR | rolling 90 days | Lower better (faster recovery) |
| MTBF | rolling 365 days | Higher better (more time between failures) |
Definition note: MTTR can mean Mitigation OR Resolution; pick one per organization and document. Mixing yields misleading trends.
Step 3 - Exclusion rules
| Should exclude | Why |
|---|---|
| Planned maintenance | Not a failure |
| Test/drill incidents | Don't pollute reliability metrics |
| Issues out of customer-trust path (internal-only) | Per organization policy - be explicit |
| Duplicates / "same root cause" within window | Inflates incident count |
Schema field is_planned_maintenance + customer_impact allow filtered queries.
Step 4 - Dashboards-as-code
# Grafana dashboard fragment
panels:
- title: "MTTR (rolling 90 days)"
targets:
- expr: |
avg_over_time(
(
incident_mitigated_ts - incident_detected_ts
)[90d:1d]
)
format: "duration"
- title: "MTBF (rolling 365 days)"
targets:
- expr: |
... (your time-series store DSL)Treat dashboards as code (versioned, reviewed). Avoid clicked-up dashboards that nobody can rebuild.
Step 5 - Target-vs-actual alert
- alert: MTTR_TARGET_BREACH
expr: avg_over_time(mttr_seconds[30d]) > 1800 # 30 min target
for: 1h
labels: { severity: warning }
annotations:
summary: "30-day MTTR exceeds 30-min target"Alert fires when the trend breaks the target - not on individual incidents.
Step 6 - ITIL alignment
ITIL 4 (Information Technology Infrastructure Library) practices incident management map to these metrics:
| ITIL term | This skill's metric |
|---|---|
| Time to detect | MTTD |
| Time to acknowledge / response | MTTA |
| Time to restore service | MTTR (mitigation) |
| Time to resolve | MTTR (resolution) |
| Mean time between failures | MTBF |
ITIL doesn't prescribe specific formulae; this skill makes them explicit. Pair with your ITSM tool (ServiceNow, Jira Service Management).
Step 7 - Postmortem integration
Each incident has a postmortem. Postmortem fields feed back into the incident schema:
| Postmortem field | Schema field |
|---|---|
| Detection mechanism | (annotation; helps drive MTTD lower) |
| Root cause | root_cause_category |
| Action items | (separate table; link by incident_id) |
| Was the runbook used? | (annotation; informs runbook-quality investment) |
Action items have due dates; track completion.
Step 8 - Distinguish MTTR mitigation vs resolution
Many organizations report only MTTR-mitigation (better numbers, truer to customer experience). Per Google SRE - Embracing Risk (opens in new window), the customer-facing metric is what matters for SLO purposes.
Document which definition your reports use; both are legitimate.
Worked example
Order service SEV-1 from the Step 1 record: detected_at 10:23:14, acknowledged_at 10:25:02, mitigated_at 10:54:11, resolved_at 11:42:33, is_planned_maintenance: false, customer_impact: true.
This organization reports MTTR-mitigation (Step 8), so the incident contributes 30m 57s. It survives the Step 3 exclusion filter (real failure, customer impact), so it enters the rolling 90-day MTTR. Against the 30-min target (Step 5), 30m 57s already sits over the line; if the 30-day average holds there, MTTR_TARGET_BREACH fires after its for: 1h window. The low MTTA (1m 48s) shows detection-to- acknowledge was healthy, so the breach points at mitigation time, not on-call responsiveness.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Mixed mitigation/resolution in MTTR | Trends incoherent | Pick one (Step 8) |
| Include maintenance / test incidents | Inflated incident count | Step 3 exclusion |
| Dashboard built once, never revisited | Stale; unrelated to current SLOs | Dashboards-as-code (Step 4) |
| MTTR target without MTTD focus | Fast recovery from things you found late ≠ fast for customer | Track all four |
| Postmortem disconnected from metrics | Action items don't reduce future MTTR | Step 7 integration |
Limitations
References
Related skills
chaos-drill-protocol
Run protocol and run workflow for a chaos experiment that has already been designed: the four pre-flight gates (non-production target, measured healthy baseline, live observability, a rollback that has actually been exercised), how to pick a conservative blast-radius bound, the sampling cadence and abort criteria fixed in writing before injection, the per-runner inject and abort commands (Chaos Mesh / Litmus / Gremlin / Toxiproxy), the refuse-to-start rules (no blast-radius bound, production context, degraded baseline, offline observability, unexercised rollback), and the recovery-validation step with its tolerance and timeout. Owns execution safety only, not experiment design: the steady-state hypothesis, the fault to inject, and the experiment file come from chaos-experiment-author. Use when an experiment definition exists and a fault is about to be injected into a running system, and the go/no-go gates, abort thresholds, and recovery check still need to be agreed and written down before the fault starts.
chaos-experiment-author
Build-an-X workflow for a chaos experiment per the Principles of Chaos Engineering - defines steady-state hypothesis, picks the variables (real-world events: network latency, node failure, region outage), sets the blast radius (which percentage / namespace / user cohort), automates execution, and emits the verdict (steady-state held / didn't hold). Includes the five-check pre-flight validation of the steady-state hypothesis (measurable, baselined, SLI-backed tolerance, defined measurement window, metric moves under the fault) with hard-reject rules, and routes the tool choice: Chaos Mesh has its own standalone skill, while LitmusChaos and Gremlin setup live in this skill's references. Use to scope and pre-flight-validate a chaos experiment before running it via Chaos Mesh / Litmus / Gremlin / Toxiproxy.
chaos-mesh
Configures Chaos Mesh for Kubernetes-native chaos engineering - picks fault types (PodChaos, NetworkChaos, StressChaos, IOChaos, TimeChaos, DNSChaos, KernelChaos, HTTPChaos), targets via label selectors, controls blast radius via namespace whitelists + selector filters, schedules via CronJobs, observes via dashboard. Distinct from Litmus by architecture (Chaos Mesh has its own dashboard + workflow orchestration; Litmus uses ChaosCenter UI). Use when the target system runs on Kubernetes and fault experiments should be declared as CRDs in the cluster alongside the workloads they target.
dr-drill-runner
The full DR-drill discipline for one service: author the runbook (per-tier RTO + RPO), pre-drill checklist (data sync state, alert silencing, customer comms), drill workflow (announce, fail-over, verify, fail-back) with timestamps, the supervised run protocol (refuse without declared RTO/RPO or against production, RTO/RPO monitoring cadence, abort-on-breach), and an auditor-ready post-drill report. Backup-integrity verification (SHA-256 + signature, restore spot checks, cross-region replication, retention, key recovery) and restore-time / RTO measurement (TTF segments, PITR latency, parallel-restore tuning, trend tracking) are worked in references. Per Google Cloud DR planning guide; covers cold / warm / hot standby tier-specific patterns. Use when a scheduled or post-incident failover drill for one service is being planned, executed, or written up, or when a new tier-1 service ships without a drill defined.
failure-injection-test-author
Orchestrates WireMock fault stubs (HTTP-level fault: 500s, malformed JSON, slow responses) with Toxiproxy (TCP-level: latency, packet loss, reset) into a single resilience test scenario - the test starts both, applies fault per scenario, runs the SUT against the impaired endpoints, verifies the SUT's resilience patterns. Use when one test must reproduce a combined network + HTTP failure - a cross-layer failure mode from an incident postmortem that neither pure HTTP fault stubs nor pure TCP chaos can cover alone, because most real failures span both layers.
toxiproxy-chaos
Configures Toxiproxy for TCP-level fault injection - runs as a sidecar / proxy between client and upstream, applies toxics (latency, bandwidth, slow_close, timeout, slicer, limit_data, reset_peer) via control API. Focused on the proxy itself rather than an API-level chaos runner, including non-test usage (chaos in dev environments, integration tests, pre-prod simulation). Use when the team needs TCP-precise fault injection in development / integration environments without K8s or commercial tooling.