DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

Building a Practical Chaos Testing Kit for Backend Services

Building a Practical Chaos Testing Kit for Backend Services

Building a Practical Chaos Testing Kit for Backend Services

Chaos is inevitable in distributed systems. Instead of fearing it, you can design a lightweight, maintainable chaos testing kit that helps your team discover and fix reliability gaps early. This tutorial walks you through creating a repeatable chaos testing workflow focused on backend services, with concrete code, tooling choices, and a step-by-step rollout plan.

1) Define goals and boundaries

  • Objective: Reveal system weak points (timeouts, retries, circuit breakers, failure modes) in a safe, controlled way before production incidents occur.
  • Scope: Focus on service-level robustness (latency, availability, correctness) for core backend endpoints, plus downstream dependencies (databases, caches, external APIs).
  • Boundaries:
    • No blast radius: tests must run in a staging environment or a dedicated chaos namespace.
    • Observability-first: ensure metrics, traces, and logs are the primary signals.
    • Safety guards: rollback/auto-disable upon abnormal explosion metrics.

Illustration: Think of chaos testing like stress-testing a bridge. You test under controlled weights and vibrations, with automatic shutdowns if stress exceeds safe thresholds.

2) Choose a lightweight chaos framework

Options you can adopt quickly:

  • Open-source: Chaos Mesh (Kubernetes-based), Gremlin, or a minimal DIY approach using a small controller.
  • DIY approach: Use a simple Python or Go agent that injects faults via your service’s fault-injection points (e.g., HTTP middleware, database call wrappers).

For a starter kit, a lightweight, language-agnostic approach is often best:

  • Run on Kubernetes in a separate namespace.
  • Use a central policy engine to describe fault scenarios.
  • Implement fault injection at boundaries (HTTP client, downstream calls) rather than inside business logic.

Recommended starting stack (simple, observable, portable):

  • Environment: Kubernetes (minikube or kind for local testing)
  • Language: Go or Python microservices
  • Fault types: latency, aborts, throttling, resource pressure (simulated CPU/memory)
  • Observability: Prometheus metrics, OpenTelemetry traces, centralized logs (Loki, Elasticsearch) ### 3) Design a fault catalog

Create a small, versioned catalog of fault scenarios. Each scenario should include:

  • Name and description
  • Trigger condition (time window, load level, randomize)
  • Fault type (latency, error, drop, resource pressure)
  • Scope (which service, which dependency)
  • Expected signals (latency thresholds, error rate targets)
  • Rollback behavior (auto-remediation, circuit breaker open)
  • Safety stop rules (max duration, health checks, auto-disable)

Example catalog entries:

  • High latency on downstream DB calls (50-200 ms additional delay for 5 minutes)
  • 20% random request aborts to a downstream auth service for 10 minutes
  • Read-heavy CPU spike on the cache layer to test eviction under load for 3 minutes

Tip: Store the catalog in a version-controlled YAML or JSON file and expose a small CLI to list and run scenarios.

4) Instrumentation and observability plan

You must be able to tell, without guesswork, when chaos scenarios are harming or revealing issues. Implement:

  • Metrics:
    • Service latency percentiles (p50, p95, p99)
    • Error rates (HTTP 5xx, downstream errors)
    • Saturation indicators (queue lengths, thread pool backlogs)
    • Fault injection counters (how many times a fault was triggered)
  • Tracing:
    • End-to-end traces with context for requests affected by faults
  • Logs:
    • Structured logs indicating start/stop of each scenario, fault type, duration, and rollback status
  • Dashboards:
    • A single chaos cockpit per environment with Scenario status, health indicators, and auto-remediation status

Example metrics to surface in Prometheus:

  • service_downstream_latency_ms{scenario="db_latency"}
  • service_error_rate{scenario="downstream_auth", unit="percent"}
  • chaos_active{namespace="chaos", scenario="db_latency"} ### 5) Implement a minimal, safe fault injection mechanism

Create a small library or middleware that can:

  • Decide when to inject a fault (policy + randomization)
  • Apply a fault at a boundary (HTTP client, gRPC call, DB call wrapper)
  • Record the fault activation in metrics/logs/traces
  • Roll back automatically when the scenario ends or health degrades

Two practical patterns:

A. Proxy-based fault injection

  • Deploy a sidecar or a lightweight proxy (e.g., Envoy or a simple HTTP middleware) that can:
    • Intercept outbound requests
    • Inject latency or abort responses
    • Tag requests with chaos metadata for traceability

B. Client-side fault injection

  • Wrap external calls with a decorator that can:
    • Introduce latency: sleep before sending the request
    • Abort: throw an exception or return an error status
  • Ensure the wrapper is transparent to business logic and measurable via tracing.

Code sketch (Go-like pseudocode for a client wrapper):

type FaultSpec struct {
LatencyMs int
AbortChance float64
Scope string
}

type FaultInjectingClient struct {
inner http.Client
spec FaultSpec
rand *rand.Rand
}

func (c *FaultInjectingClient) Do(req *http.Request) (*http.Response, error) {
// decide to inject
if c.rand.Float64() < c.spec.AbortChance {
log.Infof("injecting abort for %s", req.URL)
recordFault("abort", c.spec.Scope)
return nil, fmt.Errorf("injected fault: abort")
}
if c.spec.LatencyMs > 0 {
delay := time.Duration(c.spec.LatencyMs) * time.Millisecond
time.Sleep(delay)
recordFault("latency", c.spec.Scope)
}
return c.inner.Do(req)
}

Ensure you can enable/disable the fault at runtime via a ConfigMap or API.

6) Create a safe run protocol

A disciplined flow reduces risk:

  • Preparation
    • Spin up a dedicated chaos namespace or environment
    • Deploy services with the fault injection agent enabled
    • Ensure observability is collecting data
  • Run
    • Pick a scenario from the catalog
    • Set a TTL window (start and end times)
    • Start with a low severity and gradually escalate
    • Monitor dashboards for stickiness (latency, errors)
  • Observe
    • Collect metrics and traces
    • Look for regression indicators or unexpected cascades
  • Rollback
    • Stop injection automatically at the window end
    • If health degrades beyond a threshold, rollback immediately
  • Learn
    • Update the fault catalog with findings
    • Share postmortems and adjust resilience patterns

Example run script (high level):

  • kubectl apply -f chaos-config.yaml
  • chaosctl start scenario=db_latency duration=15m
  • chaosctl status
  • chaosctl stop force-if-needed

    7) Safety and governance

  • Guardrails:

    • Auto-disable on critical alarm thresholds (e.g., sustained 5xx rate above 2 minutes)
    • Automatic rollback if service health deteriorates beyond a safe margin
  • Access control:

    • Limit who can trigger chaos in non-production environments
    • Require a two-person sign-off for scenarios that exceed a defined severity
  • Documentation:

    • Maintain a living README with runbooks, expected outcomes, and rollback steps
    • Document learnings and changes to resilience patterns after each run ### 8) Quick-start blueprint you can copy

Files you’ll create:

  • chaos/catalog.yaml: catalog of fault scenarios
  • chaos/mission.yaml: current mission (scenario, duration, targets)
  • lib/chaos/injector.go (or injector.py): fault injection library
  • infra/chaos-proxy or sidecar config: if using a proxy approach
  • dashboards/chaos-dashboard.yaml: Grafana/Prometheus panels

Minimal example entries:

chaos/catalog.yaml

  • name: downstream-db-latency description: inject 100 ms additional latency to DB calls for 5 minutes trigger: time-window latency_ms: 100 abort_chance: 0.0 scope: downstream-db signals: latency_p95: "<= 350" error_rate: "<= 0.5%"

chaos/mission.yaml

  • scenario: downstream-db-latency duration: 5m namespace: chaos-testing enable: true

lib/chaos/injector.go
package chaos

type FaultSpec struct {
LatencyMs int
AbortChance float64
Scope string
}

type Injector struct { Spec FaultSpec }

func (i *Injector) Wrap(run func() error) error {
// apply fault before run
// run the operation
// rollback if needed
}

Dashboards

  • Create a simple panel that shows:

    • Active scenario
    • Latency delta vs baseline
    • Error rate delta
    • Auto-remediation status ### 9) Step-by-step rollout plan
  • Week 1: Instrumentation and baseline

    • Add metrics and tracing hooks to your services
    • Deploy a minimal injector in a staging namespace
    • Run one safe, low-impact scenario and observe
  • Week 2: Catalog expansion

    • Add 3-5 scenarios focusing on latency, aborts, and resource pressure
    • Build a simple CLI to run catalog entries
  • Week 3: Automation and safety

    • Integrate auto-disable rules
    • Add alerting for chaos-related anomalies
  • Week 4: Expand coverage

    • Apply chaos to more services
    • Publish a resilience postmortem template and runbooks ### 10) A concrete example walk-through

Suppose you have a user-service that calls an authentication service and a database for profile data.

  • Catalog entry: auth-service latency spike
    • Latency addition: 120 ms on calls to auth-service
    • Duration: 7 minutes
    • Scope: user-service -> auth-service
  • Run protocol:
    • Start chaos in staging
    • Observe p95 latency across user-service. Expect moderate increase but within SLO
    • If p99 latency or error rate spikes beyond threshold, stop the run
  • Observations:
    • If auth-service latency spike propagates to user-service, trace shows longer user-service spans with child auth-service spans delayed
    • If user-service has a retry mechanism, verify it doesn’t cause cascading timeouts
  • Learnings:
    • If failures escalate, consider adding circuit-breaking or backoff strategies around auth-service calls If you’d like, I can tailor this chaos testing kit to your stack (e.g., Kubernetes with specific tech like Go microservices, Python services, or Node.js), and draft starter code and YAML templates customized for your environment. Would you like a language-specific starter (Go, Python, or Node.js) and a minimal Kubernetes manifest set to begin?

-

Rizwan Saleem | https://rizwansaleem.co

Top comments (0)