Ridwan Alam

Writing

Chaos Monkey: Breaking Your Own System in Production on Purpose

A SIGCOMM paper argues the recovery code in a network controller is only ever tested by breaking the live network on purpose: what to break, how to prove the break is survivable first, and why the bug can live entirely in the milliseconds before the system settles.

Aug 31, 2026
  • Backend Engineering
  • System Design
  • Distributed Systems
Part I / The Argument
01

One Program Decides Everything

In a traditional network, a switch decides for itself where to send packets. In Software Defined Networking, that decision moves into a single program called the controller. The controller tells every switch what to do and where each packet goes.

That makes a network far easier to manage, and it creates a new risk. One program now decides everything, which makes it a single point of failure. A bug in it can misconfigure the entire network at once.

Why the usual testing does not reach this

  • Unit tests only cover the situations a developer thought about while writing them.
  • Model checking cannot handle the complexity of a real network.

In practice, when a link goes down, the controller runs a recovery routine written months ago and almost never executed in production. That routine is where the problems hide, and the only reliable way to test it is to break the link on purpose.

The proposal

Deliberately break things in the live network, then check whether it still behaves correctly. Not in staging. In production, where the code paths you have never run actually live.

02

Five Rules for Breaking Things

The paper sets out five constraints. They read well as a checklist for any failure-testing tool, not just a network one.

1
Be realistic. Real networks lose one link far more often than a whole switch. Breaking things uniformly at random mostly simulates situations that never happen.
2
Stay manageable. Every injected fault must be bounded (limited blast radius), reversible (automated rollback) and throttle-able, so operators can dial intensity up or down instantly during peak traffic.
3
Get coverage. The goal is to exercise untested or rarely-used code paths in the controller. This conflicts directly with rule two, and the tool has to balance risk against reward on every decision.
4
Use the redundancy you have. The tool holds the live topology graph, so it can check important nodes before acting. If simulating a failure shows it would partition the network or disconnect a critical node, it rejects that target and picks a safer one.
5
Check correctness afterward. Injecting the fault is not the deliverable. The result is: does the system still satisfy its guarantees, and does it recover?
Rules three and two are enemies

Coverage wants a bigger blast radius; safety wants a smaller one. Every chaos tool is a running negotiation between them, and any design that claims to have solved both at once has quietly given up on one.

Part II / The Architecture
03

Three Components, Not One

Keeping these separate is the paper's main contribution.

Component Job
OpenVirteX Sits between the controller and the switches, relaying every message so it can watch and change what each side sees. This is where failures get injected.
NetPlumber Watches the resulting network state and reports when a rule is broken, such as a packet loop forming.
Chaos Monkey Decides what to break, how often, and what to do when something breaks badly.
The shape generalises

Strip the SDN specifics and the same three parts fall out of any chaos system: something that injects, something that verifies, and something that decides. The sections below build each one in Go.

04

Deciding What May Break

The first module picks which devices to fail, using probabilities the operator controls. The important part is that intensity can be turned down during busy hours, because this runs in production. A chaos tool that cannot be turned down is a chaos tool that gets turned off.

model.go
// Chaos runs weekdays 10:00 to 16:00 at quarter intensity and one fault at a
// time, and stops completely the first time something breaks a guarantee.

model := &chaos.Model{
    BaseIntensity:      0.25,
    Interval:           5 * time.Minute,
    SettleTime:         15 * time.Second,
    MaxConcurrent:      1,    // limit the blast radius to one node
    MaxServiceFraction: 0.34,
    StopOnViolation:    true,
}

MaxServiceFraction is the quiet one: it caps how much of any single service may be impaired at once, no matter what the dice say.

05

Breaking Something, Safely

Before breaking anything, the tool asks whether the system would survive it, simulating the failure on a copy of the topology first.

would_disconnect.go
// Simulate losing this instance. If anything important becomes unreachable,
// refuse. This check is what separates an experiment from an outage.
func (r *Registry) WouldDisconnect(service, instance string) bool {
    sim := r.copy()                 // work on a clone, never live state
    sim[service].markImpaired(instance)

    reachable := breadthFirstSearch(sim, r.entryPoint)
    for name, s := range sim {
        if s.Critical && !reachable[name] {
            return true             // this would cut something off: don't
        }
    }
    return false
}
Losing one of three database replicas is an experiment. Losing the second one is an incident. This check knows the difference, and it costs microseconds.

The other half of safety is that every failure must be undoable. That belongs in the interface, not in a runbook.

actuator.go
type Actuator interface {
    Inject(context.Context, *Fault) error
    Revert(context.Context, *Fault) error // a fault you can't revert must not be used
}
No revert, no experiment

A fault type that cannot be reverted should never be wired up. Without a revert path you are not running an experiment, you are running an outage with extra steps.

06

Checking Whether Anything Actually Broke

After a failure lands and the system settles, the checker compares reality against the rules it was given. When a rule is violated the event is logged, the fault is reverted, and the previous configuration is restored.

Two things matter more than the rest

  • The rollback is automatic. Not an alert, not a dashboard. The system that caused the damage repairs it immediately, without waiting for a human to read anything.
  • The rules are properties, not "zero errors." During a normal failover you will see some errors, and that is fine. "Every customer can still check out" is a property worth checking. "Zero requests failed" is not a realistic bar and is useless as a rule.
The tautological rule

If your rule is "error rate stays under 5%" and you just injected errors, the rule fails by definition and teaches you nothing. The tool is only useful when things get worse beyond the injected failure itself. When an injected failure gets re-wrapped by application code in a way that hides its origin, it surfaces as a "real" error, and that is almost always a genuine bug in error handling.

Part III / The Evidence
07

The Paper's Demonstration

The demonstration topology
A cut link, a correct backup route, and a loop that exists only in between
link cut transient loop H1 H2 S1 S2 S3 S4 S5 S6 primary path backup path other links
The final state is correct: traffic reaches H2 over S1, S6, S5. The bug lives entirely in the interval between the cut and that state settling.

Traffic runs from one host to another across six switches, with a backup path available. Chaos Monkey cuts a link, and the controller installs the backup route.

The finding

The order of rule updates decides whether the network is correct. If old rules are not withdrawn before new ones are installed, packets can briefly loop between three switches, even though the final state looks perfectly fine.

Same controller, same failure, same final state, different behaviour in between. This is why checking has to happen in real time: the bad state lasts milliseconds, and a test that only inspects the final configuration sees nothing at all.

The same failure mode shows up in ordinary software. During a failover, service A calls B, which calls A, and retries turn a small loop into a full outage in seconds.
08

Putting It to Work

Consider a simulated shop app in Go: a gateway calls an orders service, which calls payments and inventory, with payments calling a ledger. Each service has two or three replicas.

The app has a latent bug. The orders service always talks to one specific payments replica instead of load-balancing across both. Unit tests did not catch it, because they use a mock. Staging did not catch it either, because staging runs one copy of everything, so hitting one copy looks identical to load balancing.

Killing a healthy payments replica in production surfaces it in four seconds.

chaos-run.log
08:57:52  injected   ledger/ledger-c      (killed a replica)
08:57:53  verified   ledger/ledger-c      fine, system coped
08:57:55  injected   payments/payments-a  (killed a replica)
08:57:56  VIOLATION  error rate 5.74% > 5.00% threshold
08:57:56  reverted   payments/payments-a  undone automatically
08:57:58  skipped    system hasn't recovered yet, not injecting again

What that log is actually showing

  1. It stays quiet when things go well. The ledger replica died and the system coped, so the tool said nothing. No alert, no panic.
  2. It gives a specific reason when things break. Not "something broke" but "error rate hit 5.74%, over the 5.00% threshold."
  3. It fixes the damage itself, here in under a second, without waiting for an engineer.
  4. It refuses to inject again until the system has healed, so it never piles a new disaster onto a recovering one.

Re-running the same experiments against a properly load-balanced version of the app produced no violations at all.

What this is for

Chaos Monkey is not a test for typos in your code. That is what unit tests are for. It is a test for one question: if this specific part fails right now, does the whole app go down?

Part IV / Assessment
09

What the Paper Gets Wrong, or Leaves Out

No hard proof

The paper claims Chaos Monkey exercises new code paths but never measures it. No bug count, no coverage numbers, no performance data. "Testing everything" is a stated goal here, not a demonstrated result.

No clear waiting time

After injecting a failure, how long should you wait before checking recovery? Check too soon and a system that is still recovering looks broken, which is a false alarm. Check too late and the transient bug you were hunting has already disappeared. The paper offers no answer, and there is no one-size-fits-all rule.

Rollback is treated as magic

Undoing a failure is described as a single simple step. In reality modern systems span hundreds of machines, and rolling back a distributed change can partially fail. A half-working undo can leave you worse off than the original failure did.

Worth saying

This is a two-page SIGCOMM demo paper, not a full evaluation. The gaps above are the ones that matter if you are building the thing, rather than reasons the idea is wrong.

10

Conclusion

The core argument, that deliberately breaking a live system is necessary because some problems only appear at full scale, has largely been won. Chaos engineering is ordinary practice now in a way it was not in 2015.

What still holds up is the specific discipline underneath it: know your layout, simulate the cost of a failure before you pay it, verify a named guarantee afterward, and clean up your own mess automatically. Every one of those four is a design decision you can get wrong, and the tools that get them wrong are the ones that end up switched off.

11

Reference

  1. Chang, M. A., Tschaen, B., Benson, T., & Vanbever, L. (2015). Chaos Monkey: Increasing SDN Reliability through Systematic Network Destruction. SIGCOMM '15, pp. 371–372. doi:10.1145/2785956.2790038