system-design

Chaos Engineering

Deliberately breaking your system in production to find out how it fails — before it fails on its own at 3am. Counter-intuitive, and one of the most effective reliability practices there is.

Prerequisites: Failure Modes, Resilience Patterns Time to read: ~16 minutes


The core idea

You built resilience — retries, circuit breakers, failover, redundancy. But you don’t actually know it works until something fails. And the first time it fails for real is the worst possible time to discover your failover doesn’t work or your retry logic causes a cascade.

🚨 Chaos engineering flips this: deliberately inject failures, on purpose, during business hours, when everyone is available and watching — so you discover the problems on a calm Tuesday afternoon instead of at 3am during a real outage.

The insight: untested failure handling is broken failure handling. Code paths that only run during failures never run in testing, so they’re the least exercised and most likely to be broken — exactly the ones you’re relying on when things go wrong.

🎙️ “You don’t know your resilience works until something fails. Chaos engineering makes failure happen on your terms — during business hours with everyone watching — so you find the broken failover on a Tuesday afternoon, not at 3am in a real incident.”


The method

Chaos engineering is a scientific experiment, not random destruction:

1. HYPOTHESIS   — "If we kill an instance, the load balancer reroutes and users see no impact."
2. DEFINE STEADY STATE — what "healthy" looks like (error rate, latency, throughput).
3. INJECT FAILURE — kill the instance.
4. OBSERVE       — did the system behave as hypothesized? Did users notice?
5. LEARN & FIX   — if the hypothesis was wrong, you found a real weakness. Fix it.

🚨 The hypothesis is what separates chaos engineering from vandalism. You predict the outcome based on your belief that the system is resilient, then test that belief. When reality contradicts the hypothesis, you’ve found a real gap — the whole point.

Start small, expand carefully:


What you inject

Failures map to real-world failure modes (Failure Modes):

Injection Simulates Tests
Kill an instance Server crash Load balancer failover, redundancy
Add latency Slow dependency / degraded network 🚨 Timeouts, circuit breakers — the slow failure that’s worse than crashing
Inject errors Downstream service failing Retry logic, graceful degradation, fallbacks
Network partition Split cluster Split-brain handling, consensus, quorum behaviour
Resource exhaustion CPU/memory/disk pressure Autoscaling, load shedding, backpressure
Region/AZ failure Datacenter outage Multi-region failover, DR
Clock skew NTP failure Time-dependent logic, token expiry
Dependency down Third-party outage Circuit breakers, degraded mode

🚨 Latency injection is the most valuable and underused — because a slow dependency is more dangerous than a dead one (it ties up threads while looking alive), and it’s the failure teams most often haven’t handled. Killing an instance is the famous one, but injecting latency finds more real bugs.


What you typically discover

Chaos experiments reliably surface problems that testing misses:

🚨 Every one of these is far cheaper to find in a controlled experiment than in a real outage. And finding that your alerting didn’t fire is itself hugely valuable — it means a real incident would go undetected.


Game days

A game day is a scheduled, planned chaos exercise — often a broader simulated incident rather than a single injection. The team practices the full incident response: detect, declare, assign an IC, mitigate, resolve, and postmortem — on a simulated incident.

🚨 Game days test the humans and the process, not just the system. They reveal whether people know the runbooks, whether escalation paths work, whether communication flows, and whether the team stays calm — all things you can’t learn from automated injection. Practicing the incident when it’s not real is how you’re competent when it is.


The prerequisites

🚨 You should not do chaos engineering until you have:

🎙️ “Chaos engineering validates resilience, it doesn’t create it — so you need observability to see the impact, an abort button, and basic redundancy already in place before you start. Injecting failures into a fragile system just causes an outage.”


The trust problem

⚖️ Deliberately breaking production is scary, and getting buy-in is a real challenge. Approaches:


In the real world


🚨 Interview traps


🎙️ Soundbites


🛠️ Try it

1. Kill an instance under load. Run three service instances behind a load balancer, put them under load, and kill one. Form the hypothesis first (“the LB reroutes, users see no errors”), then test it. Measure how many requests actually failed and how long recovery took. Was your hypothesis right? (Often the answer is “more requests failed than expected.”)

2. Inject latency — the valuable one. Add 5 seconds of latency to a dependency (with tc netem or Toxiproxy). Hypothesis: “our timeout and circuit breaker contain it.” Test it. You’ll very likely find a missing timeout or a retry storm — the exact bugs latency injection is best at surfacing.

3. Test your alerts. Break something and check whether your monitoring actually alerts. A surprising number of teams discover their alerts don’t fire — which means a real incident would go undetected. Finding that gap in an experiment is a big win.

4. Run a mini game day. Break something in staging without telling the team the cause, and have them respond as if it’s real. Time the detection, the mitigation, and whether the runbook worked. The gaps you find are what a real 3am incident would have hit.


Check yourself

1. Why deliberately break a working system? Because you don't actually know your resilience works until failure occurs, and the first real failure is the worst time to discover that your failover is broken, your retry logic causes a cascade, or your "non-critical" dependency is actually in the critical path. The code paths that handle failures only run *during* failures, so they're the least exercised by normal testing and the most likely to be broken — precisely the paths you're depending on when things go wrong. Chaos engineering inverts the timing: it injects failures deliberately, during business hours, with the whole team available and watching, so you discover and fix these gaps in a controlled experiment on a calm Tuesday afternoon rather than in a real, high-stress outage at 3am with users affected and information scarce. Untested failure handling is, in effect, broken failure handling.
2. What distinguishes chaos engineering from just breaking things randomly? The hypothesis and the scientific method around it. Chaos engineering is an experiment: you first define your system's healthy "steady state" (error rate, latency, throughput), then form a specific, falsifiable hypothesis about how the system will handle a particular failure ("if we kill an instance, the load balancer reroutes and users see no impact"), *then* inject that failure and observe whether reality matches the prediction. When the system behaves as hypothesized, you've confirmed resilience; when it doesn't, you've found a real, actionable weakness — which is the entire value. Random destruction with no hypothesis and no observation is just vandalism: you break things, cause harm, and learn nothing systematic. The prediction-then-test structure, plus limited blast radius, an abort button, and careful observation, is what makes it a disciplined engineering practice rather than recklessness.
3. Why is latency injection considered more valuable than killing instances? Because a slow dependency is more dangerous than a dead one, and it's the failure teams most often haven't handled. A killed instance produces a clean, obvious failure that health checks detect and load balancers route around — the common, well-tested case. A *slow* dependency keeps responding, so it passes health checks and keeps receiving traffic, while each call ties up a thread, connection, and timeout budget on the caller for the full slow duration — silently exhausting the caller's resources until it stops serving everything, including requests that don't touch the slow dependency. This is exactly the failure mode that missing timeouts and absent circuit breakers fail to contain, and it's subtle enough that teams frequently overlook it. So injecting latency reliably surfaces missing timeouts, retry storms, and cascade risks — more real bugs than the famous instance-killing, which tests the case people usually already handle.
4. What must you have in place before starting chaos engineering? Chaos engineering *validates* resilience — it doesn't create it — so you need the resilience and the tooling to observe it already present. Specifically: **good observability** (metrics, logs, traces) so you can actually see the impact of an experiment and tell whether the system handled the failure; **monitoring and alerting** to detect if an experiment goes worse than expected; **an abort button** to stop the experiment instantly; **basic resilience already** (redundancy, failover, timeouts) — injecting failures into a system with no redundancy just produces an outage and teaches nothing; and **organizational buy-in** with a blameless culture, since deliberately causing failures requires cultural acceptance. Without observability especially, you can't distinguish "the system handled it" from "the system broke and we didn't notice," which defeats the purpose. You build the resilience and the visibility first, then use chaos engineering to prove they work.
5. What do game days test that automated failure injection doesn't? The humans and the process. Automated injection (like Chaos Monkey) tests whether the *system* technically survives a failure — whether the load balancer reroutes, whether the circuit breaker trips. A game day simulates a full incident and tests whether the *team and its procedures* work: whether people notice the problem (detection), whether they declare an incident and someone takes the Incident Commander role, whether they know and correctly follow the runbooks, whether escalation paths function, whether communication flows to stakeholders, whether the documented mitigation actually works, and whether the team stays coordinated and calm under pressure. These are things you can only learn by practicing the human response, and they're often where real incidents go wrong — the system might have recovered fine, but the people fumbled the response, communicated poorly, or followed a runbook that turned out to be incorrect. Practicing the incident when it isn't real is how the team performs competently when it is.

Further reading