01Authority is leased, not commanded
A signed 15-second lease, renewed every 5 seconds, is the only thing that keeps a fault alive. Every failure of the platform ends in expiry.
ADR-0121 views, in reading order. Every view ships three ways: an HTML page, an SVG that re-opens in diagrams.net fully editable, and draw.io source.
An internal platform that injects bounded, hypothesis-driven failure into production and removes it automatically the moment real customers start to hurt. Read it in order: the boundary first, then who uses it and what they get to do, then the structure, the data, what happens during a run, how it is operated, and finally why it is safe. One decision runs through every view — injection authority is a lease held in the data plane, not a command issued from the control plane — and each page either honours that boundary or is wrong.
Injection authority is a lease held in the data plane, not a command issued from the control plane.
Outages are almost never the failure that was rehearsed. They are the retry storm behind a slow recommendation call, the cache tier that was supposed to be optional, the failover that worked in the runbook and not in the region. A chaos engineering platform exists to find those failures on a Tuesday afternoon instead of on a Friday night — which means deliberately breaking production, in front of the people who own the code, without breaking it for customers. The hard part is not creating a fault. Any script can add packet loss. The hard part is guaranteeing that the fault goes away: when the service starts hurting, when the operator says stop, when the control plane crashes, when the network partitions, and when the telemetry that was supposed to notice goes dark.
The control plane decides that an experiment may run, computes its blast radius from the dependency graph, and issues a signed, target-bound, time-bound lease. An in-cluster agent is the only component that touches a target, and it holds the fault only while that lease is valid and renewed every five seconds. A dedicated high-resolution evaluator watches the target's own SLIs on ten-second windows and, on a breach, simply stops confirming steady state — the renewal is withheld and the fault expires. Nothing in the system tells a target to stop being broken; the permission to be broken runs out.
A signed 15-second lease, renewed every 5 seconds, is the only thing that keeps a fault alive. Every failure of the platform ends in expiry.
ADR-01Lease issuer and kill switch are a separate deployment with their own identity, their own store and a 60-second RTO against the control plane's 30 minutes.
ADR-02If the platform cannot evaluate the hypothesis, it cannot hold the fault. The verdict is INCONCLUSIVE, never a pass.
ADR-03Three measures — replica share, request-volume share, downstream closure — and a veto on any run reaching a shared single point of failure.
ADR-05Ten per cent of the service's own monthly SLO error budget, debited per run, refused at exhaustion.
ADR-07Inject, verify, revert, report_state. An adapter whose revert has not been exercised in the last 30 days is ineligible for production.
ADR-09Shadow and synthetic traffic systematically miss retry storms and cache stampedes, which is the class of failure worth finding.
ADR-12Where a mitigation can be observed — breaker opened, fallback served, retry budget held — the hypothesis must assert it, not merely the absence of harm.
ADR-14Signature and scope are checked in the data plane, so a compromised control plane cannot widen a blast radius.
ADR-15A chaos platform is unusual in that its central guarantee is about what happens when everything else is broken. That guarantee is not a technology choice, which is why most of this design should outlive the services it tests and all of the Google Cloud products named in it.
The lease is what makes every failure mode converge on the same safe outcome. No new fault class, cloud migration or scale change alters that, and nothing in the lease mechanism depends on Google Cloud.
Coverage — which dependencies have never been exercised — stays useful long after any individual verdict expires. Teams change, services are rewritten, and the gap report keeps answering the same question.
Injection mechanisms will change: eBPF will displace traffic control, meshes will gain and lose features. The capability-declaring adapter contract means that churn never reaches the control plane.
Chaos error budgets, ownership-based authorisation and findings tracked to closure exist because the real constraint is remediation capacity, not injection capacity. That will still be true.
A feature request for a fault that cannot be reverted — a destructive data operation, an irreversible schema change — would put the first hole in the fail-closed guarantee. The adapter contract exists so that request has to be refused explicitly rather than accommodated quietly.
Every number here is a stated assumption from the requirement document, chosen to be defensible and arguable rather than measured. The view column points at the page where the number is visible in the design rather than merely asserted.
| Quality | Target | How it is met | View |
|---|---|---|---|
| Abort path availability | ≥ 99.99% monthly, failing closed to "no fault" | Separate deployment, own service account and Spanner instance, regional partitioning, 60 s RTO; unavailability means leases stop being renewed and faults expire | 16 |
| Control plane availability | ≥ 99.9% monthly | Cloud Run and GKE across three zones in the control region, warm replica in a second region with a 30 min RTO | 16 |
| Abort latency | p99 ≤ 10 s, hard ceiling 30 s | 10 s evaluation windows, 5 s renewal interval, 15 s lease TTL, no cross-region hop in the abort path; measured on the canary as a release gate | 13 |
| Kill switch reach | All injection halted fleet-wide ≤ 30 s | Separately authorised endpoint reaching every agent's watchdog directly, exercised on a schedule of its own | 20 |
| Signal freshness | p95 ≤ 15 s at the evaluator | Dedicated high-resolution collection path over Cloud Monitoring, windowed to baseline and injection only | 18 |
| Blast radius cap | ≤ 5% of healthy replicas, ≤ 1 zone, ≤ 1% of request volume | Three independently computed measures plus a dependency closure veto, re-checked during injection on scale events | 14 |
| Chaos error budget | ≤ 10% of a service's monthly error budget | Debited at lease issue against the SLO store's budget, settled to actual spend at abort, refused at exhaustion | 14 |
| Concurrency | 200 runs fleet-wide, 50 in one game day | Radius reservations in Spanner as the contended resource; intersecting runs queue rather than fail | 14 |
| Lease renewal throughput | ~2,000/s expected, 40,000/s at saturation | Regionally partitioned lease store, renewal as the only hot write path, agents batching per node | 16 |
| Durability of the record | RPO 0 for runs, findings and audit | Synchronous commit to Spanner before acknowledgement; evidence bundles sealed to Cloud Storage and indexed in BigQuery | 10 |
| Signal durability | RPO 60 s for high-resolution series | Loss degrades a verdict to INCONCLUSIVE rather than corrupting it — the one place where losing data is an accepted outcome | 10 |
| Retention | Runs 24 months, signals 90 days, audit 7 years | BigQuery partition expiry for signals with downsampled summaries retained for the life of the run record | 10 |
| Agent footprint | ≤ 1% node CPU, ≤ 64 MB resident when idle | DaemonSet in a dedicated namespace with explicit limits, so the envelope is visible to node autoscaling | 07 |
| Reversion verification | 100% verified; any failure is an incident | Re-read of target state after revert, bounded retry, then out-of-band reverter, a page and fleet-wide suspension of that fault class | 13 |
| Platform cost | ≤ 0.4% of fleet production compute spend | High-resolution collection only inside baseline and injection windows, early termination once the hypothesis is decided, per-team attribution | 18 |
Four claims in this architecture are cheap to test now and very expensive to discover wrong after thirty teams depend on the platform. Everything else can be built incrementally against a working answer to these.
| Risk | If it lands | Response |
|---|---|---|
| The abort budget is not achievable on the target's real SLI pipeline | Ten seconds from breach to fault-cleared assumes p95 signal freshness of 15 s at the evaluator. If the underlying metrics for most services are only available at 60-second resolution, the abort budget becomes a minute and every safety claim in this package loosens by the same factor — including the blast-radius caps, which were chosen on the assumption that a breach is caught quickly. | Measure it first, per SLI family, before committing to the number. The lever is ADR-13: if the fast path cannot be fast, the honest response is to widen the abort budget in the requirement and shrink the default blast radius to compensate, not to keep the 10 s figure and hope. |
| The dependency graph is wrong often enough to make closures useless | ADR-05 makes blast radius depend on a mesh-derived graph. If the graph misses edges, the platform under-reports reach and a compliant run touches a shared tier. If it invents edges, over-conservative closures refuse legitimate runs and teams route around the platform — the quieter and more damaging failure. | Validate twenty real closures by hand in the prototype and record the direction of each error. If the graph is unreliable, fall back to the declared selector plus an explicit shared-tier registry, and say so in the requirement rather than shipping a guardrail nobody trusts. |
| Remediation capacity, not injection capacity, is the real constraint | The loop on view 19 stalls at remediation. An assumed 61% of findings closed means four in ten weaknesses stay open, and a platform that produces more findings than the organisation can fix converts into a backlog generator that teams learn to ignore. | Report findings closed and time-to-close from day one, alongside runs executed. If closure rates fall, the correct response is to run fewer, better-targeted experiments from the untested-dependency set rather than to increase throughput. |
| The agent's node-level privilege becomes the largest unreviewed capability in the estate | ADR-10 puts traffic-control and node-level actions in a DaemonSet on all 40,000 pods' nodes. A vulnerability in the agent, or a lease-verification regression, is a fleet-wide capability. The failure would not look like a chaos experiment; it would look like an unexplained multi-service degradation. | Treat privileged adapters as a separate authorisation with their own review cadence, count scope refusals as a monitored signal (view 18), and keep the agent's release gated on the revert-proof and lease-expiry suites in ADR-11 and ADR-17's pipeline. |
| Unattended and continuous runs make the aggregate cost invisible | Per-run cost is small and the total is not. Without per-team attribution, a thousand low-value scheduled experiments arrive as a central telemetry bill that looks like a platform problem, and the only available control is a cap that penalises the teams using the platform well. | Ship attribution with the first scheduled run, not after continuous verification lands. ADR-16 makes ingestion a function of concurrency rather than fleet size; attribution is what makes concurrency somebody's decision. |
Sixteen decisions make up this architecture. Everything else across the twenty-one views is either a consequence of one of them or a detail that could be decided differently next month without anybody noticing. Each record states the question as it was actually open, the options that were weighed, what was chosen, how it is realised on Google Cloud, what the choice costs, and the conditions under which a different answer would be right.
Status of this document. This is a design, not a report on a running system. Every rate, latency, cap and retention figure is a stated assumption chosen to be defensible and arguable rather than measured — the operating context is a consumer streaming service of roughly 140 million subscribers, 900 services and 40,000 pods, which is itself an assumption. Where a number is load-bearing it is named in the record that depends on it, so a reviewer can change the number and see which decisions move with it.
Where the permission to keep a fault alive lives, and how that permission ends when everything else is broken.
ADR-01Injection authority is a lease held in the data plane, not a command issued from the control plane ADR-02The abort path is deployed separately and holds fewer dependencies than the injection path ADR-03Telemetry blindness aborts the run, and the verdict is INCONCLUSIVE rather than a pass ADR-04The global kill switch is separately authorised and independently reachableHow far a run is allowed to reach, who is allowed to say no, and what the run spends to get its answer.
ADR-05Blast radius is a computed downstream closure, not the selector the author declared ADR-06Intersecting blast radii are resolved by reservation, and the later run queues rather than fails ADR-07Chaos spends a carved-out share of the service's own error budget ADR-08The incident feed can veto injection without a human in the loopHow a fault is created, at which layer, and how the platform proves it is gone.
ADR-09Every fault is a capability-declaring adapter, and no adapter ships without a proven revert ADR-10Faults are injected at the mesh and node layers; in-application libraries are deferred ADR-11Reversion is verified by re-reading target state, and an unconfirmed revert is an incidentWhat the run is measured against, what traffic it affects, and what counts as a pass.
ADR-12Production experiments affect real customer traffic on a bounded slice ADR-13Steady state is evaluated on a dedicated high-resolution path over the owners' own SLIs ADR-14A pass requires a positive recovery observation, not merely the absence of a breachWho the agent proves it is, and where the money goes.
ADR-15Agents hold per-cluster workload identity and verify their own lease ADR-16High-resolution collection is windowed, and cost is attributed per teamEvery capability on the twenty-one views, the Google Cloud service chosen for it, the alternative that was weighed, and the reason. Google Cloud was chosen for rotation rather than necessity: chaos engineering belongs to no cloud, and the previous use cases in this practice leaned on Azure, AWS and bare open source. Where the choice is genuinely cloud-independent — the lease, the adapter contract, the verdict model — the record says so, because those are the parts a reader should be able to lift onto any platform.
| Capability | Choice | Origin | Credible alternative | Why this one | Record |
|---|---|---|---|---|---|
| Definition and run API | Cloud Run — request-scoped, scales to zero between game days | Google Cloud | GKE Deployment behind an internal load balancer | The API's load is bursty and human-driven; paying for idle capacity between sessions buys nothing. Nothing on the abort path lives here. | ADR-02 |
| Lease issuance and renewal | Cloud Run service with its own service account, per region | Google Cloud | A GKE workload co-located with the run controller | Separate deployment and identity is the whole point of ADR-02; co-location would put the brake in the accelerator's failure domain. | ADR-02 |
| Lease signing | Cloud KMS asymmetric key, verified in the agent | Google Cloud | A shared secret distributed to agents | Agents must verify without being able to mint. A shared secret in 40,000 pods is a minting capability in 40,000 pods. | ADR-15 |
| Guardrail state — leases, reservations, budgets | Spanner, regional configuration | Google Cloud | Firestore, or Memorystore for Redis | Reservation and budget debits need a real transaction, and the abort path needs a strongly consistent regional read. Firestore's consistency model would work; Redis's would not survive a failover. | ADR-06 |
| Run records, findings, audit | Spanner, synchronous commit before acknowledgement | Google Cloud | Cloud SQL for PostgreSQL | RPO 0 with regional replication and no failover story to write by hand. Cloud SQL would be cheaper and would make the multi-region story the platform team's problem. | ADR-14 |
| Run controller and evaluator | GKE Autopilot, three zones in the control region | Google Cloud | Cloud Run jobs | Both are long-lived, stateful over a run's duration, and need predictable scheduling on 10-second windows — which is exactly what a request-scoped runtime does not offer. | ADR-13 |
| High-resolution signal collection | Cloud Monitoring read API on a dedicated windowed path | Google Cloud | Reading the SLO store's aggregates | The SLO pipeline's aggregation lag is minutes; the abort budget is ten seconds. The cost of a second path is accepted and bounded by windowing. | ADR-13 |
| Evidence warehouse and coverage marts | BigQuery with partition expiry, bundles in Cloud Storage | Google Cloud | Keeping series in the monitoring backend | Coverage and cost reporting are analytical queries over 24 months of runs, and partition expiry expresses the 90-day signal retention as configuration rather than as a cleanup job. | ADR-16 |
| Run event distribution | Pub/Sub topic per event class | Google Cloud | Consumers polling the platform API | Three independent consumers — incident platform, catalogue, reporting — need the same events without coupling to the API's availability. | ADR-08 |
| Approvals and game-day sequencing | Workflows | Google Cloud | State machine inside the run controller | Both are long-running, human-paced and need durable resumption, which is the one place in this design where a durable workflow engine is the right answer rather than the wrong one. | ADR-06 |
| Scheduling | Cloud Scheduler triggering the run API | Google Cloud | Kubernetes CronJobs in each cluster | Schedules must be evaluated against exclusion windows and budgets centrally. A per-cluster cron cannot see either, and would skip or fire without knowing why. | ADR-07 |
| Injection agent | DaemonSet in a dedicated namespace, Go binary | This design | Sidecar injected per workload | Node-level faults need a node-level agent, and a per-workload sidecar multiplies the standing privilege by the number of pods rather than by the number of nodes. | ADR-01 |
| Network and dependency-response faults | Mesh fault filters plus tc/netem at the node | Open source | In-application fault libraries | Mesh filters cover what the proxy sees and are trivially reversible; netem covers what it does not. Application libraries would need 900 services to adopt them. | ADR-10 |
| Resource and lifecycle faults | cgroup limits and the Kubernetes API, via the agent | Open source | A third-party chaos operator | Both are well-trodden; owning the thin implementation keeps the adapter contract — inject, verify, revert, report_state — uniform across every class. | ADR-09 |
| Identity for people | Organisation identity provider over OIDC | Enterprise standard | Platform-local accounts | Authorisation is resolved against service ownership at run time; a local account would be a second place where who-owns-what is decided. | ADR-15 |
| Identity for agents | GKE Workload Identity, cluster-scoped | Google Cloud | Long-lived service account keys | An agent's identity should not be exfiltratable, and should be scoped so that a compromised cluster cannot request a lease against another one. | ADR-15 |
| Kill switch | Dedicated Cloud Run endpoint with its own IAM binding | Google Cloud | A flag on the platform API | It must work when the platform does not, which means it must not share the platform's failure domain or its authorisation path. | ADR-04 |
| Release pipeline | Cloud Build with SLSA provenance, Artifact Registry, signed images | Google Cloud | Any CI runner plus a plain registry | The agent is the highest-privilege binary in the estate. Provenance and signature verification at admission is proportionate to what it is allowed to do. | ADR-11 |
When a fault must stop, does something have to successfully tell the target to stop — or does the permission to be broken simply run out?
Do the components that start experiments and the components that stop them ship and fail together?
When the platform can no longer see whether the target is healthy, does the experiment continue?
When somebody needs to stop everything, does the thing they press depend on the platform that is misbehaving?
Is the blast radius what the definition says it is, or what the dependency graph says the definition reaches?
What happens when two experiments would affect overlapping parts of the fleet at the same time?
Does deliberate failure cost anything, and if so, whose budget does it come out of?
During a real production incident, who decides whether experiments keep running?
How does a new kind of failure get added, and what stops one being added that cannot be undone?
At which layer is a fault created — the proxy, the kernel, or the application's own code?
Is a fault gone because the revert call returned, or because the platform looked?
Does a production experiment degrade requests that real customers made, or a copy of them?
Does the platform reuse the production SLO pipeline to judge the hypothesis, or build a faster path of its own?
Does "no SLO breach during the injection window" mean the mitigation worked?
Where is a lease checked — at the control plane that issued it, or at the agent that will act on it?
What stops a resilience platform's telemetry bill from exceeding the value of the findings it produces?
Eleven terms that mean something specific in this package. Where a term is used loosely elsewhere in the industry, the definition here is the one the views and records assume.
| Package | What it is | What it does here | Considered instead |
|---|---|---|---|
| Steady-state hypothesis | A falsifiable statement about named signals with thresholds and a window, written before the run. | The only thing that distinguishes an experiment from an outage. A definition without one is rejected at validation. | "Run the fault and see what happens", which produces an event nobody can grade and nothing anyone can disagree with. |
| Lease | A signed, target-bound, time-bound permission to hold one fault class against one resolved target set. | The unit of injection authority. Renewed every 5 s, expiring after 15 s, and the sole reason a fault continues to exist. | A revert command, which depends on delivery during exactly the failures that disrupt delivery. |
| Blast radius | Three computed measures — replica share, request-volume share, downstream reachable set — for one run. | The enforced limit. Computed from the dependency graph before injection and re-checked during it. | The replica percentage the author declared, which is a lie whenever the affected replicas share a downstream tier. |
| Safe abort | Withholding a lease renewal so the fault expires, then verifying reversion by re-reading target state. | The default outcome of anything going wrong, including the platform itself failing. | Sending a stop instruction, and treating its acknowledgement as proof the fault is gone. |
| Game day | A facilitated session of sequenced experiments with human participants and a shared timeline. | Measures time to detect and time to mitigate — the response, not the service. | A batch of automated runs on a Thursday, which tests the system and not the people who have to notice. |
| Fault adapter | A component implementing inject, verify, revert and report_state for one fault class. | The extension point. An adapter with no proven revert is ineligible for production, which is how irreversible faults are kept out. | A script per fault, where reversibility is a property of whoever wrote it that day. |
| Chaos error budget | A carved-out share — assumed 10% — of a service's monthly SLO error budget, reserved for deliberate failure. | Bills the cost of learning in the same currency as the cost of failing, and gives the owner the ceiling. | A separate allowance invented for the platform, which has no meaning and so gets renegotiated every quarter. |
| INCONCLUSIVE | A verdict recorded when telemetry coverage during the injection window fell below the completeness threshold. | Keeps unobserved runs out of the pass column. Its rate is a published metric. | Recording an unobserved run as a pass, which converts a degraded metrics pipeline into false confidence. |
| Finding | A refuted hypothesis, with a lifecycle independent of the run that produced it and a linked remediation reference. | The unit of value. The platform is measured on findings closed, not on runs executed. | A failed run in a list, which is an event rather than a commitment. |
| Resilience coverage | Which failure classes each service has been exercised against, when, with what verdict — and which declared dependencies never have. | The durable product. The untested set is the most useful output the platform produces. | A count of experiments run, which rewards volume and says nothing about what is still a guess. |
| Out-of-band reverter | A separately identified component able to clear a stuck fault when the normal agent path is unavailable. | Break glass. Every use is an incident, and it triggers fleet-wide suspension of that fault class. | Manual kubectl by whoever is awake, which leaves no record and no suspension. |
svg/<view>.svg or drawio/<view>.drawio in draw.io Desktop or at app.diagrams.net to edit. The SVG carries the diagram inside it, so it is both the picture and the source. This folder is self-contained — copy it whole and every link still resolves.