Chaos Engineering and Observability: Testing System Resilience
Chaos engineering is the practice of deliberately injecting failures into a system to verify it behaves correctly when real failures occur. Without observability, chaos experiments are blind — you inject a fault and cannot tell whether your system degraded gracefully or silently failed. Combining chaos engineering with structured observability turns experiments into evidence.
Key Takeaways
The steady-state hypothesis is non-negotiable. Chaos experiments without a measurable baseline are science fiction. Define what "normal" looks like in metrics before injecting any fault.
Blast radius control determines whether chaos is safe. Start with a single instance, a canary deployment, or a synthetic traffic segment. Running chaos against your production database is not engineering — it is gambling.
Observability during an experiment is the experiment. Metrics, logs, and traces collected during fault injection are the data. The absence of anomalies is the proof of resilience; their presence shows you what to fix.
GameDays build institutional knowledge that documentation cannot. Running a scheduled chaos experiment with your whole team watching teaches everyone how the system fails — before customers find out.
Automated chaos in CI catches resilience regressions. A new service version that silently removes a retry mechanism will fail a chaos test in CI before it degrades production.
Why Systems Fail Unexpectedly Despite Testing
Unit tests and integration tests verify that code does what it is supposed to do under normal conditions. They do not verify what happens when a dependency is slow, a network partition isolates a service, a disk fills up, or a downstream API starts returning 503s.
Real systems operate under Murphy's Law: if something can fail, it will. The question is whether you discover failure modes during a controlled chaos experiment at 2pm on a Tuesday, or during peak traffic at 2am on a Friday.
Chaos engineering is the discipline of creating controlled failures to answer that question on your terms.
The Steady-State Hypothesis
The foundational concept in chaos engineering is the steady-state hypothesis — a measurable definition of normal system behavior. Without it, you cannot determine whether your system degraded during an experiment.
A good steady-state hypothesis is specific and metric-backed:
# chaos/hypotheses/order-service.yaml
steady_state:
description: "Order service processes requests normally"
probes:
- name: success-rate
type: prometheus
query: |
rate(http_requests_total{service="orders",status=~"2.."}[2m])
/
rate(http_requests_total{service="orders"}[2m])
threshold:
min: 0.99 # 99% success rate
- name: p95-latency
type: prometheus
query: |
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket{service="orders"}[2m])
)
threshold:
max: 0.2 # 200ms
- name: error-logs
type: loki
query: |
count_over_time({service="orders"} |= "ERROR"[2m])
threshold:
max: 10 # Fewer than 10 error logs per 2 minutesBefore injecting any fault, you verify the steady state is met. After the experiment, you verify it returns to steady state within your recovery time objective.
Chaos Tooling Overview
Several mature tools exist for chaos experiments at different layers:
Chaos Monkey (Netflix) — randomly terminates EC2 instances or containers. Simple, effective for testing service-level redundancy.
Litmus Chaos (CNCF) — Kubernetes-native chaos operator. Rich experiment library covering pod failures, network faults, node stress, and more.
Gremlin — commercial platform with a wide experiment catalog, blast radius controls, and native Kubernetes and cloud integrations.
Chaos Toolkit — open-source, declarative YAML-based framework that integrates with any system and any observability backend.
For most teams starting with chaos engineering, Litmus on Kubernetes or Chaos Toolkit is the right entry point.
A Structured Chaos Experiment: Pod Failure
Here is a complete Litmus ChaosEngine experiment for testing how the orders service handles pod termination:
# chaos/experiments/orders-pod-failure.yaml
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
name: orders-pod-failure
namespace: production
spec:
appinfo:
appns: production
applabel: "app=orders-service"
appkind: deployment
# Only affect 1 of N replicas (blast radius control)
chaosServiceAccount: litmus-admin
experiments:
- name: pod-delete
spec:
components:
env:
- name: TOTAL_CHAOS_DURATION
value: "60" # 60 seconds of chaos
- name: CHAOS_INTERVAL
value: "10" # Kill a pod every 10 seconds
- name: FORCE
value: "false" # Graceful termination
- name: PODS_AFFECTED_PERC
value: "50" # Kill up to 50% of podsBefore running this experiment, you verify the steady state. During the 60-second window, you watch your dashboards. After it ends, you verify the steady state returned.
Observability During Chaos Experiments
The chaos experiment itself is not the deliverable — the observability data collected during it is. You need to know:
- Did the steady-state metrics degrade during the fault?
- If they degraded, by how much and for how long?
- Did the system recover automatically, and how quickly?
- Were there any unexpected failure modes (cascading failures, silent data corruption)?
Grafana annotations let you mark chaos experiment windows on your dashboards:
# Mark experiment start in Grafana
curl -X POST http://grafana:3000/api/annotations \
-H "Content-Type: application/json" \
-d '{
"text": "Chaos: orders pod-delete (50% of pods, 60s)",
"tags": ["chaos", "orders-service", "pod-failure"],
"time": '$(date +%s%3N)'
}'
# Run the chaos experiment
kubectl apply -f chaos/experiments/orders-pod-failure.yaml
sleep 60
# Mark experiment end
curl -X POST http://grafana:3000/api/annotations \
-H "Content-Type: application/json" \
-d '{
"text": "Chaos end: orders pod-delete",
"tags": ["chaos", "orders-service", "pod-failure", "end"],
"time": '$(date +%s%3N)'
}'Now your Grafana dashboard shows exactly when the experiment ran, making the correlation between fault injection and metric behavior visually obvious.
Network Chaos: The More Realistic Failure Mode
Pod deaths are visible — Kubernetes reschedules and recovers quickly. Network degradation is subtler and more damaging. A service that dies returns errors immediately; a service that is slow degrades everything that calls it.
Litmus network chaos experiments:
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
name: orders-network-latency
spec:
appinfo:
applabel: "app=orders-service"
experiments:
- name: pod-network-latency
spec:
components:
env:
- name: NETWORK_LATENCY
value: "200" # Add 200ms to all network calls
- name: JITTER
value: "50" # ±50ms jitter
- name: TOTAL_CHAOS_DURATION
value: "120"
- name: DESTINATION_IPS
value: "" # Empty = all destinations
- name: DESTINATION_HOSTS
value: "payment-service" # Target only payment callsWith 200ms added latency to payment service calls, you want to verify:
- The orders service circuit breaker opens after 5 consecutive slow calls
- Orders fail fast (not after 30 seconds) with a clear error
- The orders service does not cascade — other endpoints stay healthy
- Metrics show the failure isolated to the
/checkoutpath
# Watch for circuit breaker opening during experiment
orders_circuit_breaker_state{downstream="payment-service"}
# Verify non-checkout endpoints stay healthy
rate(http_requests_total{service="orders",handler!="/checkout",status=~"5.."}[1m])Automated Chaos in CI
Running chaos experiments manually in staging is valuable but insufficient. Resilience regressions — removing a retry mechanism, accidentally setting a timeout too high — can ship in any PR. Automated chaos in CI catches these before deployment.
Chaos Toolkit integrates cleanly into CI:
# chaos/experiments/api-resilience.json
{
"title": "Order service handles payment service unavailability",
"description": "When payment service is down, orders return 503 within 500ms",
"steady-states": {
"before": {
"title": "System is healthy",
"probes": [
{
"name": "orders-healthy",
"type": "probe",
"provider": {
"type": "http",
"url": "http://orders-service/health",
"expected_status": 200,
"timeout": 1
}
}
]
}
},
"method": [
{
"type": "action",
"name": "stop-payment-service",
"provider": {
"type": "process",
"path": "kubectl",
"arguments": "scale deployment payment-service --replicas=0 -n test"
},
"pauses": { "after": 5 }
},
{
"type": "probe",
"name": "orders-fail-fast",
"provider": {
"type": "http",
"url": "http://orders-service/checkout",
"method": "POST",
"expected_status": 503,
"timeout": 0.5
}
}
],
"rollbacks": [
{
"type": "action",
"name": "restart-payment-service",
"provider": {
"type": "process",
"path": "kubectl",
"arguments": "scale deployment payment-service --replicas=2 -n test"
}
}
]
}# .github/workflows/chaos.yml
name: Resilience Tests
on:
push:
branches: [main]
jobs:
chaos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Chaos Toolkit
run: pip install chaostoolkit chaostoolkit-kubernetes
- name: Run resilience experiments
run: |
chaos run chaos/experiments/api-resilience.json
chaos run chaos/experiments/db-connection-exhaustion.jsonThe rollback section ensures the experiment always cleans up, even if the probe fails. CI chaos experiments need to be self-contained and leave no side effects.
GameDays: The Human Dimension of Chaos
Automated chaos tests what your system does. GameDays test what your team does.
A GameDay is a scheduled exercise where engineers deliberately inject failures while the whole team watches — on-call engineers, developers, product managers — to observe both system and human response. The goals are:
- Verify your runbooks are accurate and complete
- Find gaps in alert routing (who gets paged for what?)
- Build muscle memory for incident response
- Identify missing observability (failures that are hard to diagnose)
A simple GameDay agenda:
09:00 — Brief: what we're testing, steady-state definition, blast radius
09:15 — Verify steady state (all probes green)
09:20 — Inject fault: kill 2 of 3 order service replicas
09:20-09:35 — Observe: does the alert fire? Does Kubernetes recover? How long?
09:35 — Inject fault: add 500ms latency to payment service
09:35-09:50 — Observe: does circuit breaker open? Do orders queue or fail?
09:50 — Rollback, verify steady state returns
10:00 — Retrospective: what did we learn? What do we fix?The retrospective is where the value lives. Missing an alert that should have fired, a runbook step that was wrong, a metric that was not visible — these go into tickets immediately.
HelpMeTest: Validating Recovery After Chaos
After a chaos experiment, you want confidence that normal behavior has been fully restored — not just that metrics look healthy, but that the full user journey works end to end. HelpMeTest runs your critical user journey scenarios against production continuously. After a chaos experiment, a full test run in HelpMeTest confirms that checkout, authentication, order creation, and other key flows are working correctly.
This is especially valuable after GameDays: at the end of the exercise, you trigger a full HelpMeTest run to verify the system is fully recovered before declaring the exercise closed.
Common Chaos Engineering Mistakes
Starting too big. Your first chaos experiment should not target your production database. Start with a single non-critical service in a staging environment with synthetic traffic.
No observability, no experiment. If you cannot measure steady state before and after, you cannot interpret results. Set up dashboards before running experiments.
Skipping rollbacks. Every chaos action must have a rollback. If the experiment fails mid-way, you need automated cleanup. Manual cleanup after a failed experiment in production is an incident.
Chaos without hypothesis. "Let's kill some pods and see what happens" is not an experiment — it is a production incident waiting to happen. Every experiment starts with a specific hypothesis: "When X fails, Y should happen within Z seconds."
Conclusion
Chaos engineering is how you find out whether your system is as resilient as your architecture diagrams suggest. Observability is how you interpret what happens during experiments. Together, they give you evidence-based confidence in your system's failure behavior — not theoretical confidence from design reviews.
Start small: pick one service, define a measurable steady state, inject one fault, and see what you learn. The first experiment is usually the most valuable.