Chaos Engineering: Breaking Things on Purpose
Chaos engineering is the practice of deliberately injecting failures into production systems to discover weaknesses before they cause real outages.

Your system looks resilient on paper — retries, circuit breakers, redundant databases. But does it actually survive failures? Chaos engineering answers that question by deliberately injecting failures in controlled experiments. Instead of waiting for 3 AM outages to discover weaknesses, you find them at 2 PM on a Tuesday when the whole team is awake.
The Chaos Experiment Framework
Every chaos experiment follows a scientific structure: form a hypothesis, define steady state, inject failure, observe, and analyze.
// Structure of a chaos experiment
interface ChaosExperiment {
name: string;
hypothesis: string;
steadyState: {
metric: string;
threshold: number;
window: string;
};
method: {
type: "latency" | "failure" | "resource" | "network";
target: string;
parameters: Record<string, unknown>;
duration: string;
};
rollback: {
automatic: boolean;
trigger: string;
};
}
const experiment: ChaosExperiment = {
name: "Database failover under load",
hypothesis: "When the primary database becomes unreachable, the application fails over to the replica within 30 seconds with less than 1% error rate increase",
steadyState: {
metric: "error_rate",
threshold: 0.01, // 1% error rate
window: "5m",
},
method: {
type: "network",
target: "primary-db.internal",
parameters: { action: "block", port: 5432 },
duration: "5m",
},
rollback: {
automatic: true,
trigger: "error_rate > 0.05 for 2m", // Abort if error rate exceeds 5%
},
};Starting Simple: Application-Level Experiments
You don't need production traffic and sophisticated tooling to start. Begin with controlled experiments in a staging environment.
// Middleware that simulates downstream service failures
// Only active when CHAOS_MODE environment variable is set
function chaosMiddleware(serviceName: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (process.env.CHAOS_MODE !== "true") {
return next();
}
const chaosConfig = getChaosConfig(serviceName);
// Simulate latency injection
if (chaosConfig.latency && Math.random() < chaosConfig.latency.probability) {
const delay = chaosConfig.latency.minMs +
Math.random() * (chaosConfig.latency.maxMs - chaosConfig.latency.minMs);
setTimeout(next, delay);
return;
}
// Simulate error injection
if (chaosConfig.error && Math.random() < chaosConfig.error.probability) {
res.status(chaosConfig.error.statusCode).json({
error: "Chaos injection: simulated failure",
service: serviceName,
});
return;
}
next();
};
}Network Partition Experiments
Network failures between services are the most common production issue. Use tc (traffic control) on Linux to simulate real network conditions.
# ❌ Testing only the happy path
# curl http://api-server:3000/health → 200 OK
# "Ship it, the service works!"
# ✅ Simulating real network conditions
# Add 200ms latency to traffic going to the database
tc qdisc add dev eth0 root netem delay 200ms 50ms distribution normal
# Simulate 10% packet loss to downstream service
tc qdisc add dev eth0 root netem loss 10%
# Simulate network partition (complete connectivity loss)
iptables -A OUTPUT -d database.internal -j DROP
# Clean up after experiment
tc qdisc del dev eth0 root
iptables -D OUTPUT -d database.internal -j DROPValidating Circuit Breakers
Circuit breakers are supposed to prevent cascade failures. Chaos experiments verify they actually work.
// ❌ Assuming the circuit breaker works because the code looks right
const breaker = new CircuitBreaker(callExternalService, {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000,
});
// ✅ Chaos experiment to verify circuit breaker behavior
async function verifyCiruitBreaker() {
const metrics = {
totalRequests: 0,
successfulRequests: 0,
circuitOpenRejections: 0,
timeouts: 0,
};
// Phase 1: Normal operation — circuit should be closed
console.log("Phase 1: Verifying normal operation");
for (let i = 0; i < 100; i++) {
try {
await breaker.fire();
metrics.successfulRequests++;
} catch (error) {
// Should not happen in normal operation
}
metrics.totalRequests++;
}
// Phase 2: Inject failures — circuit should open
console.log("Phase 2: Injecting failures");
enableChaosMode({ error: { probability: 1.0, statusCode: 500 } });
for (let i = 0; i < 50; i++) {
try {
await breaker.fire();
} catch (error) {
if (error.message === "Breaker is open") {
metrics.circuitOpenRejections++;
} else {
metrics.timeouts++;
}
}
metrics.totalRequests++;
}
// Phase 3: Recovery — circuit should close after reset timeout
console.log("Phase 3: Verifying recovery");
disableChaosMode();
await sleep(35000); // Wait for reset timeout
const recovered = await breaker.fire();
console.log("Circuit recovered:", recovered !== undefined);
return metrics;
}Gameday Exercises
A gameday is a scheduled team exercise where you run chaos experiments and practice incident response. It combines technical testing with process validation.
## Gameday Checklist
### Before
- [ ] Define 3-5 specific experiments with hypotheses
- [ ] Ensure monitoring dashboards are visible to all participants
- [ ] Verify rollback procedures for each experiment
- [ ] Brief all participants on the plan and abort criteria
- [ ] Confirm the on-call rotation is staffed
### During
- [ ] Run one experiment at a time
- [ ] Record observations in a shared document
- [ ] Note any unexpected behaviors or cascading effects
- [ ] Use the incident response process (even if the "incident" is planned)
- [ ] Abort immediately if abort criteria are met
### After
- [ ] Document findings for each experiment
- [ ] File tickets for discovered issues (with severity based on blast radius)
- [ ] Update runbooks based on what was learned
- [ ] Schedule follow-up gameday to verify fixesBlast Radius Control
Never run unlimited chaos experiments. Define scope boundaries and automatic abort conditions.
# Chaos experiment configuration with safety controls
experiment:
name: "API latency injection"
scope:
environment: "production"
percentage_of_traffic: 5 # Only affect 5% of requests
target_service: "order-api"
excluded_endpoints: # Never inject chaos on critical paths
- "/api/payments"
- "/api/health"
abort_conditions:
- metric: "error_rate"
threshold: 0.05 # Abort if errors exceed 5%
window: "2m"
- metric: "p99_latency_ms"
threshold: 5000 # Abort if p99 exceeds 5 seconds
window: "2m"
duration: "15m"
auto_rollback: trueKey Takeaways
- Chaos engineering is hypothesis-driven — define what you expect before injecting failure
- Start in staging with simple experiments — you don't need production chaos from day one
- Verify your resilience patterns — circuit breakers, retries, and failovers only work if you test them
- Control blast radius aggressively — always limit scope, set abort conditions, and have rollback ready
- Gamedays build team muscle memory — practice incident response when stakes are low
- Fix what you find immediately — a discovered weakness without a fix is worse than ignorance because you've accepted the risk


