Chaos Engineering with SLOs: Testing Reliability Under Real-World Failures
Chaos engineering without SLOs is just breaking things. You inject failures, watch dashboards, and write post-mortems. Chaos engineering with SLOs is different — it's a structured way to answer a specific question: "Does my system maintain its reliability commitments under real-world failure conditions?"
This guide shows how to combine chaos experiments with SLO measurement to build genuine confidence in your system's reliability.
Why Chaos Engineering Needs SLOs
Without SLOs, chaos experiments produce qualitative results: "The system seemed to handle it okay." With SLOs, they produce quantitative results:
- Before the experiment: SLO compliance at 99.97%
- During pod kill experiment: SLO compliance dropped to 99.91% for 3 minutes
- After recovery: SLO compliance at 99.97%
- Budget consumed: 0.6 minutes of downtime vs. 43.2 minute monthly budget
That's a result you can reason about, compare over time, and use to make deployment decisions.
The Chaos Engineering Loop with SLOs
1. Steady state hypothesis: "System maintains SLO during [failure]"
2. Inject failure
3. Measure: does SLO hold?
4. If SLO violated: you found a reliability gap — fix it
5. If SLO held: increase experiment scope or document as validatedSetting Up Chaos Experiments
Chaos Monkey (Netflix)
# Install Chaos Monkey via Spinnaker (original) or standalone
# Or use simian-army for specific experiments
# Alternative: kube-monkey for Kubernetes
kubectl apply -f https://raw.githubusercontent.com/asobti/kube-monkey/master/manifests/configmap.yamlLitmus Chaos (Cloud Native)
# experiments/pod-kill-experiment.yaml
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
name: pod-kill-test
namespace: production
spec:
appinfo:
appns: production
applabel: 'app=myapp'
appkind: deployment
chaosServiceAccount: litmus-admin
experiments:
- name: pod-delete
spec:
components:
env:
- name: TOTAL_CHAOS_DURATION
value: "300" # 5 minutes
- name: CHAOS_INTERVAL
value: "30" # Kill a pod every 30 seconds
- name: FORCE
value: "false" # Graceful shutdown
- name: PODS_AFFECTED_PERC
value: "50" # Kill 50% of pods
probe:
# SLO probe: check availability during chaos
- name: slo-availability-check
type: httpProbe
httpProbe/inputs:
url: "https://api.example.com/health"
insecureSkipVerify: false
method:
get:
criteria: ==
responseCode: "200"
mode: Continuous
runProperties:
probeTimeout: 5
interval: 5
retry: 2Python Chaos Testing Framework
# chaos/chaos_harness.py
import time
import threading
import requests
import statistics
from dataclasses import dataclass, field
from typing import Callable, Optional
import subprocess
import random
@dataclass
class SLOMeasurement:
timestamp: float
success: bool
latency_ms: float
@dataclass
class ChaosExperimentResult:
experiment_name: str
duration_seconds: int
measurements: list[SLOMeasurement] = field(default_factory=list)
@property
def success_rate(self) -> float:
if not self.measurements:
return 0
return sum(m.success for m in self.measurements) / len(self.measurements)
@property
def p99_latency_ms(self) -> float:
latencies = sorted(m.latency_ms for m in self.measurements if m.success)
if not latencies:
return 0
return latencies[int(len(latencies) * 0.99)]
def slo_met(self, availability_slo: float = 0.999, latency_slo_p99_ms: float = 500) -> bool:
return (
self.success_rate >= availability_slo and
self.p99_latency_ms <= latency_slo_p99_ms
)
class ChaosHarness:
def __init__(self, target_url: str):
self.target_url = target_url
def measure_slo(
self,
duration_seconds: int,
interval_seconds: float = 1.0,
) -> list[SLOMeasurement]:
"""Continuously probe the target and record SLI measurements."""
measurements = []
end_time = time.time() + duration_seconds
while time.time() < end_time:
start = time.perf_counter()
try:
resp = requests.get(self.target_url, timeout=5)
latency_ms = (time.perf_counter() - start) * 1000
measurements.append(SLOMeasurement(
timestamp=time.time(),
success=200 <= resp.status_code < 300,
latency_ms=latency_ms,
))
except Exception as e:
latency_ms = (time.perf_counter() - start) * 1000
measurements.append(SLOMeasurement(
timestamp=time.time(),
success=False,
latency_ms=latency_ms,
))
time.sleep(interval_seconds)
return measurements
def run_experiment(
self,
experiment_name: str,
failure_fn: Callable,
duration_seconds: int = 300,
) -> ChaosExperimentResult:
"""
Run a chaos experiment while measuring SLO compliance.
"""
result = ChaosExperimentResult(
experiment_name=experiment_name,
duration_seconds=duration_seconds,
)
# Start SLO measurement in background thread
measurement_thread_complete = threading.Event()
measurements = []
def measure_thread():
measurements.extend(self.measure_slo(duration_seconds))
measurement_thread_complete.set()
thread = threading.Thread(target=measure_thread)
thread.start()
# Wait a bit, then inject failure
time.sleep(10) # Let baseline establish
print(f"Injecting failure: {experiment_name}")
failure_fn()
# Wait for experiment to complete
measurement_thread_complete.wait()
result.measurements = measurements
return resultWriting SLO-Aware Chaos Tests
# tests/chaos/test_pod_failure.py
import pytest
import subprocess
import time
from chaos.chaos_harness import ChaosHarness
AVAILABILITY_SLO = 0.999
LATENCY_P99_SLO_MS = 500
@pytest.fixture
def harness():
return ChaosHarness("https://staging.example.com/api/products")
def kill_random_pod():
"""Kill a random pod in the production namespace."""
# Get pod list
result = subprocess.run(
["kubectl", "get", "pods", "-n", "production",
"-l", "app=myapp", "-o", "name"],
capture_output=True, text=True
)
pods = result.stdout.strip().split('\n')
if not pods:
pytest.skip("No pods found")
target_pod = random.choice(pods)
subprocess.run(
["kubectl", "delete", target_pod, "-n", "production", "--grace-period=0"],
check=True,
)
print(f"Killed pod: {target_pod}")
def throttle_cpu():
"""Limit CPU on all app containers to simulate resource contention."""
subprocess.run([
"kubectl", "patch", "deployment", "myapp",
"-n", "production",
"--patch", '{"spec":{"template":{"spec":{"containers":[{"name":"myapp","resources":{"limits":{"cpu":"100m"}}}]}}}}',
], check=True)
def restore_cpu():
"""Restore normal CPU limits."""
subprocess.run([
"kubectl", "patch", "deployment", "myapp",
"-n", "production",
"--patch", '{"spec":{"template":{"spec":{"containers":[{"name":"myapp","resources":{"limits":{"cpu":"1000m"}}}]}}}}',
], check=True)
@pytest.mark.chaos
def test_pod_kill_slo_maintained(harness):
"""System should maintain SLO when a pod is killed."""
result = harness.run_experiment(
experiment_name="random-pod-kill",
failure_fn=kill_random_pod,
duration_seconds=300,
)
print(f"\nChaos Experiment: {result.experiment_name}")
print(f"Success rate: {result.success_rate:.4%}")
print(f"p99 latency: {result.p99_latency_ms:.1f}ms")
print(f"SLO met: {result.slo_met(AVAILABILITY_SLO, LATENCY_P99_SLO_MS)}")
assert result.slo_met(AVAILABILITY_SLO, LATENCY_P99_SLO_MS), \
f"SLO violated during pod-kill experiment:\n" \
f" Availability: {result.success_rate:.4%} (need {AVAILABILITY_SLO:.3%})\n" \
f" p99 latency: {result.p99_latency_ms:.1f}ms (need ≤{LATENCY_P99_SLO_MS}ms)"
@pytest.mark.chaos
def test_cpu_throttle_slo_maintained(harness):
"""System should maintain SLO under CPU constraints."""
def throttle_and_restore():
throttle_cpu()
time.sleep(120) # Throttle for 2 minutes
restore_cpu()
result = harness.run_experiment(
experiment_name="cpu-throttle",
failure_fn=throttle_and_restore,
duration_seconds=300,
)
assert result.slo_met(AVAILABILITY_SLO, LATENCY_P99_SLO_MS), \
f"SLO violated under CPU throttle:\n" \
f" p99 latency: {result.p99_latency_ms:.1f}ms — CPU saturation causing latency spike"
@pytest.mark.chaos
def test_database_latency_injection(harness):
"""System should handle database latency without violating availability SLO."""
def inject_db_latency():
# Use toxiproxy or similar to add network latency to DB connection
subprocess.run([
"toxiproxy-cli", "toxic", "add",
"--toxicName", "db-latency",
"--type", "latency",
"--attributes", "latency=200,jitter=50",
"postgres"
], check=True)
def remove_db_latency():
subprocess.run([
"toxiproxy-cli", "toxic", "remove",
"--toxicName", "db-latency",
"postgres"
], check=True)
def inject_and_remove():
inject_db_latency()
time.sleep(180) # 3 minutes of database latency
remove_db_latency()
result = harness.run_experiment(
experiment_name="database-latency",
failure_fn=inject_and_remove,
duration_seconds=300,
)
# With 200ms DB latency, some latency increase is expected
# But should remain available
assert result.success_rate >= AVAILABILITY_SLO, \
f"Availability dropped under DB latency: {result.success_rate:.4%}"Chaos Test Report
def generate_chaos_report(results: list[ChaosExperimentResult]) -> str:
"""Generate a human-readable chaos experiment report."""
lines = ["# Chaos Engineering Report\n"]
passed = [r for r in results if r.slo_met()]
failed = [r for r in results if not r.slo_met()]
lines.append(f"## Summary")
lines.append(f"- Experiments run: {len(results)}")
lines.append(f"- SLO maintained: {len(passed)}")
lines.append(f"- SLO violated: {len(failed)}")
lines.append("")
if failed:
lines.append("## ⚠️ Failures (Action Required)")
for r in failed:
lines.append(f"\n### {r.experiment_name}")
lines.append(f"- Availability: {r.success_rate:.4%}")
lines.append(f"- p99 latency: {r.p99_latency_ms:.0f}ms")
lines.append(f"- **Gap**: System does not maintain SLO under this failure mode")
lines.append("\n## ✓ Passed Experiments")
for r in passed:
lines.append(f"- {r.experiment_name}: {r.success_rate:.4%} availability, {r.p99_latency_ms:.0f}ms p99")
return "\n".join(lines)CI Integration for Chaos Tests
# .github/workflows/chaos-tests.yml
name: Chaos Engineering Tests
on:
schedule:
- cron: '0 2 * * 2' # Weekly Tuesday at 2am (low traffic)
workflow_dispatch:
inputs:
experiment:
description: 'Specific experiment to run'
required: false
jobs:
chaos-staging:
runs-on: ubuntu-latest
environment: staging # Requires approval for production
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
run: |
echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config
- name: Run chaos experiments
run: |
pip install pytest requests
pytest tests/chaos/ -v \
--chaos-env staging \
-m "chaos" \
--tb=short \
--timeout=600
- name: Generate report
if: always()
run: python scripts/chaos_report.py --output chaos-report.md
- name: Post report to Slack
if: failure()
run: |
curl -X POST "$SLACK_WEBHOOK" \
-H 'Content-type: application/json' \
-d "{\"text\": \"⚠️ Chaos tests found SLO violations. See report: ${{ github.run_url }}\"}"Chaos Testing Anti-Patterns
Running chaos in production without runbooks: Know how to roll back every experiment before you run it.
Not measuring during the experiment: "We killed a pod and it recovered" without measuring SLO impact during the recovery window misses latency spikes and partial availability loss.
Only testing individual failures: Real incidents often involve multiple failures. Test combinations: kill a pod while DB is slow while network is partitioned.
Setting SLO thresholds too loose for chaos tests: If your production SLO is 99.9%, test that chaos experiments don't violate 99.9% — not 99%.
Summary
Chaos engineering with SLOs transforms "we broke things and watched dashboards" into "we systematically verified our reliability commitments":
- Define steady-state hypothesis in SLO terms before each experiment
- Measure continuously during failures — not just before and after
- Assert on SLO compliance — fail the test if SLO is violated
- Build a library of validated failure modes over time
- Schedule regular chaos runs — reliability is perishable, dependencies change
A system that passes chaos tests against its SLOs is a system you can deploy with confidence. Use HelpMeTest to schedule weekly chaos test runs and alert your team when an infrastructure change introduces new reliability gaps.