Design a chaos engineering platform
The interviewer asks
“Your company runs 500 microservices on Kubernetes across two cloud regions, including an LLM provider dependency that fails every week. Design an internal chaos engineering platform that lets teams safely inject failures (killed pods, added latency, dead dependencies, zone loss) in staging and production, and tells them whether users would have noticed.”
What a strong answer looks like
A strong answer starts with safety, because a chaos platform that causes an outage is worse than none. The control plane stores experiments as versioned definitions (target, fault, duration, steady-state hypothesis tied to SLO metrics, abort conditions), checks guardrails before running (blast radius limits, change freezes such as sale days, active incidents, approvals for production), and a scheduler starts runs. Fault injection happens through agents: a DaemonSet on nodes for network faults (tc/netem) and resource stress, the Kubernetes API for pod kills, the service mesh for L7 faults like HTTP 503s or delays on a percentage of requests, and cloud fault APIs (AWS FIS) for zone or instance loss. Every injected fault has a lease: if the agent loses contact with the control plane, or the run ends, the fault reverts on its own. A monitor compares SLO metrics during the run against a baseline or a control group and aborts automatically on breach. Senior candidates add experiment/control comparison on a small traffic slice, reporting of findings into a backlog, game days, and SLO burn-rate based abort rules.
Concepts to mention
1 Requirements
Functional
- Define experiments as code: targets (service, labels, percentage of pods or traffic), fault type, parameters, duration, schedule
- Fault types: pod/process kill, CPU and memory stress, network latency/loss/partition, DNS failure, HTTP error or delay injection for specific routes, dependency blackhole (Redis, Kafka, LLM provider), zone or node loss
- Steady-state hypothesis per experiment using SLO metrics (success rate, p99 latency, orders per minute), checked before, during and after
- Automatic abort and rollback when an abort condition is breached, plus a global kill switch
- Guardrails: blast radius limits, blocked windows (sale days, change freeze), no runs during active incidents, approval for production experiments
- Run reports with metric graphs and findings; nightly automated suites in staging; game-day mode
- Full audit of who ran what, where and when
Non-functional
- Abort detection and full fault rollback within 30 s of an SLO breach
- Faults revert by themselves within 60 s if the control plane or agent loses connectivity
- Agents on ~20,000 nodes with negligible overhead (under 1% CPU when idle)
- ~2,000 experiment runs per week, up to 50 concurrent runs, never two overlapping on the same service unless allowed
- Control plane availability 99.9%; its failure must never leave faults active
Out of scope (say it out loud)
- Load testing tools themselves (assume k6 or similar exists)
- Security penetration testing
- Automatic fixing of the weaknesses found
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
PUT/v1/experiments/{id} | Create or update an experiment definition (YAML in the service repo, synced by CI): targets, fault, hypothesis queries, abort thresholds, schedule, environment. |
POST/v1/experiments/{id}/runs | Start a run (dry_run flag available). Runs pre-flight guardrail checks; returns run_id or a list of blocking reasons. |
POST/v1/runs/{run_id}/abort | Abort one run; agents revert faults immediately. |
POST/v1/killswitch | Global or per-environment stop: abort all runs and block new ones. |
GET/v1/runs/{run_id} | Status, timeline of events, hypothesis results, links to dashboards and traces. |
gRPCAgent.Stream(node_id) ↔ FaultCommand / LeaseRenewal / Ack | Bidirectional stream between agent and control plane: apply fault with lease, renew lease, revert, report status. |
4 Data model
experimentsPostgres (definitions also in Git)experiment_id (PK)owner_teamserviceenvironmentfault_spec jsonbtargets jsonbhypothesis jsonb (queries + thresholds)abort_rules jsonbscheduleapproval_requiredversionGit is the source of truth for reviewable changes; Postgres holds the synced, versioned copy the scheduler uses.
runsPostgresrun_id (PK)experiment_idversionstate (pending|preflight|injecting|running|aborting|reverted|completed|failed)started_bystarted_atended_atresult (pass|fail|aborted)abort_reasonState machine for each run; only valid transitions allowed.
fault_leasesetcd (or Postgres with TTL logic)lease_idrun_idnode_idtarget (pod/container)faultexpires_atStrongly consistent so two runs cannot claim the same target; lease expiry triggers revert on the agent side even without the control plane.
target_locksetcdservice/env → run_idPrevents overlapping experiments on one service, so results are interpretable and damage is bounded.
run_eventsPostgres / ClickHouserun_idtstype (preflight_ok, fault_applied, metric_sample, abort_triggered, reverted)payloadTimeline for reports and audit.
findingsPostgres + issue trackerfinding_idrun_idservicedescriptionseverityticket_urlstatusWeaknesses found (missing timeout, retry storm) become tracked tickets; the experiment re-runs to verify the fix.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Production latency experiment on the checkout service
- 1The payments team's experiment: add 800 ms latency to calls from checkout to the coupon service for 5% of checkout pods, for 10 minutes. Hypothesis: checkout success rate stays ≥ 99.5% and p99 under 2 s, because checkout has a 300 ms timeout and a fallback of 'no coupon applied'.
- 2Pre-flight: the control plane checks the freeze calendar (not a sale day), no open SEV1/SEV2 incident, checkout's error budget above 25%, the target lock for checkout is free, and approval from the on-call is recorded.
- 3It measures the steady state for 5 minutes before injecting; if the system is already outside the hypothesis, the run is cancelled (you cannot learn from a sick system).
- 4It selects 3 of 60 checkout pods, takes the target lock and leases in etcd, and sends a fault command to the agents on those nodes: netem delay of 800 ms on egress to the coupon service's IPs, lease 30 s, renewed every 10 s.
- 5The monitor evaluates the hypothesis queries every 5 s, comparing the 3 experiment pods against the 57 control pods.
- 6Result: experiment pods show p99 of 3.1 s, because the coupon client has a 5 s timeout, not 300 ms as the team believed. The abort rule (p99 > 2 s for 30 s on experiment pods) triggers.
- 7The control plane sends revert to the agents, stops renewing leases (belt and braces), marks the run aborted, and the report shows the graphs and the finding 'coupon client timeout is 5 s'. A ticket is filed; after the fix the experiment re-runs and passes.
Control plane dies mid-experiment
- 1A network partition cuts the agents off from the control plane during a run that is injecting 20% packet loss.
- 2Agents stop receiving lease renewals. When the lease expires (at most 30 s), each agent reverts its tc/netem rules locally and records the revert.
- 3When connectivity returns, agents report their state; the control plane marks the run 'reverted: lease expired' and alerts the owning team.
- 4No fault can outlive its lease, so the platform's own failure is never the cause of a long outage.
7 Deep dives
Safety first: guardrails, blast radius and the dead man's switch
The platform must be safer than the failures it injects. Layer the protections. Before a run: policy checks refuse production runs during freezes (Big Billion Days, IPL final, month-end payment peaks), during active incidents, for services whose error budget is nearly spent, or without the owning team's approval; target locks stop two experiments overlapping on one service. Blast radius is limited by default to a few pods or a few percent of traffic, and widened only step by step (1% → 5% → one zone) after passes. During a run: automated abort rules on SLO metrics (success rate, latency, business metrics such as orders per minute), evaluated every few seconds, plus a human kill switch available to anyone on call. Faults must revert without the control plane: every fault is applied with a lease, agents renew it by heartbeat, and when the lease lapses the agent removes the fault on its own; a dead man's switch, not a request. Agents also record applied faults on local disk so a restarted agent cleans up after itself. Finally, start in staging and preview environments, run production experiments in business hours with people watching, and make every action auditable.
How faults are injected at each layer
Different faults need different mechanisms. Process and pod faults: the control plane calls the Kubernetes API to delete pods or the agent kills a container process; this tests restarts, readiness probes and replica counts, but clean crashes are the least interesting failure. Resource faults: the agent runs stress processes in the target container's cgroup for CPU, memory or disk pressure. Network faults: the agent enters the pod's network namespace and uses tc/netem for latency, jitter, packet loss and bandwidth limits, and iptables to drop traffic to specific IPs or ports (blackholing Redis, Kafka or the LLM provider's endpoint), which tests timeouts and circuit breakers. Application-level faults: the service mesh (Envoy/Istio fault injection) returns HTTP 503 or adds delay for a percentage of requests on a route, even only for requests with a test header, which allows very precise targeting. Infrastructure faults: cloud fault services (AWS Fault Injection Service) stop instances, simulate AZ power loss or throttle APIs. Open-source tools such as Chaos Mesh and LitmusChaos implement many of these as Kubernetes custom resources; the platform can drive them and add guardrails, scheduling and reporting on top rather than reinventing the injectors.
Steady-state hypothesis and experiment vs control
A chaos run is an experiment, so it needs a hypothesis and a measurement that can say no. Define steady state in terms users and the business feel: checkout success rate, p99 latency, orders per minute, stream start failures, not CPU or pod restarts. The hypothesis says these stay within bounds while the fault is active. Measure a baseline before injecting; if the system is already unhealthy, cancel. The best design compares an experiment group with a control group at the same time, which removes noise from daily traffic patterns: inject the fault into a small set of pods or a small slice of traffic (routed by the mesh), keep a same-sized control slice untouched, and compare their metrics. Netflix's ChAP worked this way, routing a tiny fraction of traffic into experiment and control clusters. Abort rules come from the same metrics with tighter windows: for example abort if the experiment group's error rate exceeds control by 0.5 points for 30 s. Use SLO burn rate for longer runs: abort if the run is burning budget more than ~10× faster than allowed. Store graphs and the decision with the run, so the report says clearly whether the hypothesis held.
From one-off tests to continuous verification
Chaos is valuable only if it changes the system. Every failed hypothesis produces a finding: the weakness, the evidence, the owner, and a ticket. After the fix, the same experiment re-runs to verify it and then joins an automated suite. Teams keep experiments as YAML in their service repos, reviewed like code; CI syncs them to the platform. A nightly suite runs the standard catalogue against staging or preview environments with a load test running in parallel: kill 50% of a consumer group's pods (lag rises, autoscaler reacts, nothing is lost), make Redis unreachable (rate limiter falls back to local limits), add 500 ms to Postgres (admission control sheds load instead of cascading timeouts), return 503 from the LLM provider for 10 minutes (gateway fails over), kill a Temporal worker (workflows resume), cut the API off from Kafka (outbox buffers and the relay catches up). Results feed a resilience scorecard per service. Game days add the human side: a surprise fault in a controlled window tests alerts, runbooks and on-call response, which automated runs cannot. The platform measures success by bugs found and fixed before customers found them, not by number of runs.
8 Trade-offs
Build vs adopt (Chaos Mesh, LitmusChaos, Gremlin, AWS FIS)
Existing injectors are mature; building your own netem wrappers adds risk without value. What companies usually build is the layer on top: guardrails tied to their incident and freeze systems, SLO-based abort rules, scheduling, ownership and reporting. Adopt injectors, build the control plane where the off-the-shelf one does not fit.
Production vs staging only
Staging is safe but differs from production in traffic, data and configuration, so many weaknesses only appear in production. Production chaos finds real problems but needs strong guardrails and tiny blast radius. Do both: broad automated suites in staging, small, supervised experiments in production for services that have passed in staging.
Infrastructure-level vs application-level faults
Network and node faults test real behaviour of clients, kernels and libraries but are blunt. Mesh-level faults target precisely (one route, one header, 1% of requests) but skip lower-layer behaviour such as TCP retransmits. Use both for different questions.
9 Bottlenecks
Metric query load and lag
At hundreds of concurrent runs, querying raw metrics every 5 s overloads Prometheus and slows abort detection. Use pre-computed recording rules for SLIs, a dedicated query path, and push-based alerts for abort conditions.
Approvals slow adoption
If every production run needs a meeting, teams stop running them. Auto-approve experiments that passed several times in staging, stay within a small blast radius and run in business hours; require approval only for widening scope.
10 Failure handling
Agent crashes while a fault is active
Network rules applied with tc/iptables persist in the pod's namespace after the agent dies. The restarted agent reads its local record of applied faults and removes them; if the node is unreachable, the control plane cordons it and the pods are rescheduled, which removes the namespace and the fault with it.
Abort fails to revert because the target is overloaded
A CPU-starved node may not process the revert quickly. Faults have hard maximum durations enforced by the agent's own timer, and the control plane escalates by deleting the target pods, which always removes pod-scoped faults.
Experiment causes a real incident
The kill switch stops all runs; the incident process takes over; the run's audit trail shows exactly what was injected and when. The post-incident review checks why guardrails did not catch it, and the blast radius policy is tightened.
11 What impresses
- Makes fault revert independent of the control plane through leases
- Ties guardrails to error budgets, incidents and sale-day freezes
- Uses an experiment vs control comparison instead of eyeballing dashboards
- Knows latency and partial failure find more bugs than pod kills
- Measures the platform by bugs fixed, not runs executed
- Proposes adopting existing injectors and building the safety layer
12 Mistakes
- Only killing pods and calling it chaos engineering
- No baseline or hypothesis, so results cannot be interpreted
- Faults that persist when the control plane or agent dies
- Running production experiments during peak sales or incidents
- Overlapping experiments on the same service
- Running chaos without the observability to see the effect
13 Follow-ups
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.