Testland
Browse all skills & agents

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-tests
View source

error-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

  • Adopting SLO-based reliability discipline.
  • Auditing whether the error-budget process actually fires (often: defined but never triggers).
  • After an incident: did the budget burn correctly? Did alerts fire? Did the freeze take effect?

Step 1 - Define the SLI + SLO

ElementExample
SLI (indicator)successful_requests / total_requests over rolling 30-day window
SLO (objective)99.9% over 30 days
Error budget100% − 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.5

Maintenance 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 < 75

Step 4 - Burn-rate alerting

Per the SRE workbook, burn-rate alerting fires when budget is being consumed faster than safe.

WindowBurn rateAlert
1 hour14.4×"Critical - page" (consumes 2% in 1 hr)
6 hours"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 > 0

Step 7 - Multi-window multi-burn-rate (Google SRE practice)

The SRE workbook recommends multi-window burn-rate alerts to balance sensitivity vs noise:

Long windowShort windowBurn rate thresholdAlert
1 hr5 min14.4×Page
6 hr30 minPage
3 day6 hrTicket

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 resolved

Step 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-patternWhy it failsFix
SLO with no enforcement (no freeze)Targets ignored; reliability degradesStep 5 freeze-trigger
Single burn-rate alertEither too noisy or too lateStep 7 multi-window
Include maintenance in SLIPlanned outages eat real budgetStep 2 exclusion
99.999% SLO ("five nines") for everything26 sec/month budget; constant freezeTier SLOs per criticality
No reportingStakeholders don't internalizeStep 8 weekly cadence

Limitations

  • SLOs measure success rate; latency-based SLOs (P99 < X ms) need similar but distinct calculation.
  • Budget calculations assume independent failures; correlated failures (region-wide outage) eat budget faster than statistics predict.
  • Real freezes need org-level discipline; tests can't enforce cultural change.

References

  • Google SRE - Embracing Risk (opens in new window) - error budget concept, SLO enforcement, freeze trigger
  • Google SRE Workbook - Implementing SLOs (consult sre.google for the full workbook chapter)
  • references/mttr-mtbf.md - incident metrics that consume budget: MTTR / MTBF / MTTD / MTTA schema, formulae, and dashboards-as-code
  • dr-drill-runner - drills that intentionally affect SLI

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

  • Standing up incident reporting from scratch.
  • Auditing existing incident metrics - are MTTR / MTBF actually being computed correctly?
  • Setting reliability targets against the error budget (host SKILL.md).

How to use

  1. Record one schema entry per incident (Step 1) in your IR tool, with distinct detected / acknowledged / mitigated / resolved timestamps.
  2. Pick ONE MTTR definition - mitigation or resolution (Step 8) - and document which your reports use.
  3. Apply the exclusion rules (Step 3) so planned maintenance, drills, and duplicates never enter the metric.
  4. Compute MTTD / MTTA / MTTR / MTBF with the Step 2 formulae over their rolling windows.
  5. Version the Grafana panels as code (Step 4) and wire the target-vs-actual alert (Step 5) so the trend, not a single incident, pages.
  6. Feed postmortem fields back into the schema (Step 7) and track action-item completion by incident_id.
  7. Pair the means with P95 / P99 duration views (Limitations) so tail incidents stay visible.

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)
MetricWindowLower / Higher
MTTDrolling 90 daysLower better (faster detection)
MTTArolling 90 daysLower better (responsive on-call)
MTTRrolling 90 daysLower better (faster recovery)
MTBFrolling 365 daysHigher 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 excludeWhy
Planned maintenanceNot a failure
Test/drill incidentsDon't pollute reliability metrics
Issues out of customer-trust path (internal-only)Per organization policy - be explicit
Duplicates / "same root cause" within windowInflates 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 termThis skill's metric
Time to detectMTTD
Time to acknowledge / responseMTTA
Time to restore serviceMTTR (mitigation)
Time to resolveMTTR (resolution)
Mean time between failuresMTBF

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 fieldSchema field
Detection mechanism(annotation; helps drive MTTD lower)
Root causeroot_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

  • MTTR-mitigation: stop customer impact (rollback, traffic shift, scale up). Prioritized in incident response.
  • MTTR-resolution: fix the root cause permanently. May happen days/weeks later.

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.

  • MTTA = acknowledged - detected = 1m 48s.
  • MTTR-mitigation = mitigated - detected = 30m 57s.
  • MTTR-resolution = resolved - detected = 1h 19m 19s.

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-patternWhy it failsFix
Mixed mitigation/resolution in MTTRTrends incoherentPick one (Step 8)
Include maintenance / test incidentsInflated incident countStep 3 exclusion
Dashboard built once, never revisitedStale; unrelated to current SLOsDashboards-as-code (Step 4)
MTTR target without MTTD focusFast recovery from things you found late ≠ fast for customerTrack all four
Postmortem disconnected from metricsAction items don't reduce future MTTRStep 7 integration

Limitations

  • MTTR / MTBF are means: they hide tail behavior. Pair with P95 / P99 incident-duration views for the worst case.
  • Single-team services have low N; statistics jittery.
  • Some organizations report "Mean Time To Innocence" (time until someone proves a service isn't at fault) - not in this skill's scope.

References

  • Google SRE - Embracing Risk (opens in new window) - incident-metrics framing
  • ITIL 4 incident management - ITSM standard
  • ISO/IEC 20000 service management - high-level governance
  • The host error-budget-tests SKILL.md - per-incident budget consumption
  • dr-drill-runner - drills produce incidents with is_planned_maintenance: true

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.