Back to Blog
Chaos Engineering Without the Chaos
Tutorials 8 min read

Chaos Engineering Without the Chaos

You don't need Toxiproxy, Gremlin, or a distributed systems PhD to test how your app handles failure. One CLI flag is enough.

ZS
Zach Snell
February 27, 2026

Every conference talk about chaos engineering makes it sound so reasonable. “Just inject failures in production! Discover weaknesses before your users do!” The audience nods. Then they go back to work and nothing happens.

I get it. I’ve been that person in the audience. The gap between “we should test failure paths” and actually doing it is enormous. Toxiproxy needs a proxy layer in front of every service. Gremlin costs real money and needs agents running in your infrastructure. Rolling your own middleware means maintaining custom failure injection code forever. Most teams look at the setup cost, file it under “someday,” and keep shipping code that’s never seen a 503.

The result: your app handles the happy path beautifully and falls apart the first time a downstream service hiccups.

One flag

What if chaos engineering was this:

mockd serve --chaos-profile flaky

That’s it. Your mock server now fails 30% of the time. Your frontend, your mobile app, your microservice — whatever is pointed at localhost:4280 — is suddenly dealing with the real world. No proxy layer. No sidecar. No YAML files longer than your component code.

Want to stop? Disable it at runtime without restarting anything:

mockd chaos disable

Or switch profiles on the fly:

mockd chaos apply slow-api

The chaos config applies globally to all mock responses. Every endpoint you’ve configured in mockd will be affected — which is exactly what you want. Real service degradation doesn’t politely limit itself to one endpoint.

10 built-in chaos profiles

Profiles are pre-built combinations of fault rules that simulate real-world conditions. Pick one and go:

ProfileWhat it does
slow-api500–2000ms latency on every response
degraded200–800ms latency + 5% error rate
flaky20% of requests return 500/502/503 errors
offline100% 503 errors — service is down
timeout30-second delays — your loading spinners will thank you
rate-limited30% of requests get 429 Too Many Requests
mobile-3g300–800ms latency + 2% errors, simulating real mobile conditions
satellite600–2000ms latency + 5% errors — for the “our customer is on a cruise ship” scenario
dns-flaky10% intermittent 503 errors, like real DNS resolution failures
overloaded1–5 second latency + 15% errors + bandwidth throttling

The overloaded profile is my favorite for demos. Point your app at it, start clicking around, and watch your UI grind to a halt — 1–5 second response times plus 15% of requests failing outright, with bandwidth throttling on top. It’s exactly what happens when a service starts running out of memory or connections. Most apps handle the “everything is down” case fine. It’s the “everything is slow and partially broken” case that reveals the real bugs.

Custom chaos rules

Profiles are convenient, but sometimes you need specific behavior. The CLI lets you combine latency and error injection directly:

mockd chaos enable --latency 100ms-500ms --error-rate 0.1 --error-code 503

Or go deeper with the Admin API and define per-path fault rules — like a circuit breaker on your payment endpoint that trips after 5 failures:

curl -X PUT http://localhost:4290/chaos -H 'Content-Type: application/json' -d '{
  "enabled": true,
  "rules": [{
    "pathPattern": "/api/payments/.*",
    "faults": [{
      "type": "circuit_breaker",
      "probability": 1.0,
      "circuitBreaker": {
        "failureThreshold": 5,
        "recoveryTimeout": "30s",
        "halfOpenRequests": 2,
        "tripStatusCode": 503
      }
    }]
  }]
}'

The rules array lets you scope faults to specific URL patterns and stack multiple fault types on the same path. The global CLI flags are simpler; the API gives you the full power.

12 fault types

Mockd ships with 12 fault types total. Eight are stateless — they apply independently to each request:

  1. latency — Add random delay within a range
  2. error — Return error status codes
  3. timeout — Extreme delays that trigger client timeouts
  4. corrupt_body — Mangle the response body
  5. empty_response — Return a 200 with no body (the sneakiest one)
  6. slow_body — Drip-feed the response bytes at a crawl
  7. connection_reset — Drop the connection mid-response
  8. partial_response — Truncate the response body at a random point

These cover the basics. But the real differentiator is the other four.

Stateful faults — the thing nobody else has

Most chaos tools treat every request in isolation. Flip a coin, maybe fail, done. Real systems don’t work like that. Real failures have state. A circuit breaker trips after repeated failures. A rate limiter tracks request counts over a window. A memory leak gets worse over time. These aren’t random — they’re sequential, and they’re the failure modes that actually break production systems.

Mockd’s four stateful fault types maintain state across requests:

Circuit breaker

A full CLOSED → OPEN → HALF_OPEN state machine. Configure a failure threshold, and the circuit breaker trips after that many errors — just like a real one would.

curl http://localhost:4280/api/payments
# 200 OK — circuit CLOSED

# (after repeated failures...)
curl http://localhost:4280/api/payments
# 503 Service Unavailable
# Retry-After: 30
# X-Circuit-State: OPEN

The Retry-After and X-Circuit-State headers are included automatically. Your retry logic can read them. Your monitoring dashboards can track them. This is how real services behave when they implement circuit breakers — and if your client code doesn’t handle these headers, better to find out now.

You can trip or reset circuit breakers manually through the admin API on port 4290, which is useful for testing specific recovery scenarios.

Retry-after

Returns 429 or 503 responses with a proper Retry-After header, then automatically recovers after the specified window. This tests whether your HTTP client actually respects Retry-After or just hammers the server in a tight loop (you’d be surprised how many do).

Progressive degradation

Response times increase over N requests. First request: 50ms. Hundredth request: 2 seconds. Two-hundredth request: 8 seconds. This simulates a memory leak, connection pool exhaustion, or garbage collection pressure — the kind of slow-burn failure that doesn’t trigger alerts until it’s already affecting users.

Chunked dribble

The response body streams correctly, but with long inter-chunk delays. The HTTP status is 200, the headers look fine, the Content-Type is right — but the body takes 30 seconds to fully arrive. This breaks a surprising number of HTTP clients that set timeouts on the initial connection but not on body streaming.

These four stateful fault types are why I built chaos into mockd instead of telling people to use Toxiproxy. Toxiproxy is a great TCP proxy, but it doesn’t know about HTTP semantics. It can’t send a Retry-After header. It can’t implement a circuit breaker state machine. It can’t progressively degrade over a sequence of requests. Mockd operates at the application layer, so it can simulate application-layer failures — the ones that actually matter to your code.

Runtime control

Everything is controllable at runtime. No restarts. Apply chaos, test, disable, adjust — all while your app is connected:

# Apply a profile
mockd chaos apply rate-limited

# Check what's active
mockd chaos status

# Disable everything
mockd chaos disable

The admin API on port 4290 exposes the same controls over HTTP, so your test suite can enable specific chaos configurations per test case. And if you’re using mockd’s MCP server with an AI coding agent, three MCP tools handle chaos programmatically: set_chaos_config to apply fault rules, get_stateful_faults to inspect current state, and manage_circuit_breaker to trip or reset breakers. Your agent can set up failure scenarios, run tests against them, and verify resilience — all without human intervention.

Where this fits in your workflow

I’m not suggesting you replace a mature chaos engineering platform if you already have one. If you’re running Gremlin in production against real traffic with game days and runbooks, keep doing that.

But most teams aren’t there. Most teams have zero resilience testing. Their retry logic is untested. Their timeout configurations are guesses. Their error handling code has never executed outside of a unit test with a mocked HTTP client.

For those teams — which is most teams — the best chaos engineering tool is the one you’ll actually use. And you’re a lot more likely to use it when it’s:

mockd serve --chaos-profile degraded

instead of a week-long infrastructure project.

Try it

Install mockd:

brew install getmockd/tap/mockd
# or
curl -fsSL https://get.mockd.io | sh

Start a mock server with chaos enabled:

mockd start
mockd add http --method GET --path "/api/orders" 
  --body '{"orders": [{"id": "{{uuid}}", "total": 42.99}]}'
mockd chaos apply flaky

Hit it a few times and watch the failures roll in:

for i in $(seq 1 10); do
  curl -s -o /dev/null -w "%{http_code}\n" http://localhost:4280/api/orders
done

Some 200s. Some 500s. That’s what your users experience. Now you can see it too.

The honest part

Chaos applies globally. There’s a --path flag that accepts a regex so you can scope faults to specific route patterns, but you can’t target individual mock IDs. If you need fault injection on exactly one mock and not its neighbors, you’ll need to get creative with your path regex — or restructure your mocks so they live on distinct paths.

The four stateful fault types (circuit breaker, progressive degradation, retry-after, chunked dribble) maintain their state in memory. That means when the server restarts, all state resets — the circuit breaker goes back to CLOSED, the progressive degradation counter drops to zero. For long-running soak tests this is fine, but don’t expect state to survive a mockd stop && mockd start. It won’t.

There are no network-layer faults here. No packet loss, no TCP RSTs, no connection refused before the TLS handshake. mockd operates at the HTTP layer — it can only mess with things after it has accepted the connection. If you need TCP-level chaos, Toxiproxy is the right tool for that and I’m not going to pretend otherwise. Also worth knowing: the overloaded profile ramps up over time by design, so if your test run is only a few seconds long, you might not see the full degradation curve. Give it a minute or two to really show its teeth.

Learn more

#chaos engineering#testing#resilience#fault injection#mock server

Try mockd

Multi-protocol API mock server. HTTP, gRPC, GraphQL, WebSocket, MQTT, SSE, SOAP.

curl -fsSL https://get.mockd.io | sh