This runbook describes how to run, monitor, and respond to chaos experiments in the stellar-trust-escrow backend.
Chaos engineering deliberately injects failures to verify the system handles them gracefully. The goals are:
- Confirm circuit breakers open under sustained failures
- Verify retry logic exhausts correctly before surfacing errors
- Ensure timeouts prevent indefinite hangs on external calls (Stellar RPC)
- Validate that the system recovers automatically after a fault is removed
HTTP Request
│
▼
chaosMiddleware ← injects latency / HTTP errors based on CHAOS_EXPERIMENT
│
▼
Express controller
│
▼
retryDatabaseOperation ← wraps DB calls; integrates circuit breaker
│ └── CircuitBreaker("database") CLOSED → OPEN after 5 failures / 10 s window
│ OPEN → HALF_OPEN after 30 s
│ HALF_OPEN → CLOSED after 2 successes
│
▼
stellarService ← wraps Stellar RPC; use CircuitBreaker("stellar-rpc")
│
└── CircuitBreaker("stellar-rpc") same thresholds as database breaker
- Backend running locally or in a staging environment
CHAOS_ENABLEDandCHAOS_EXPERIMENTenv vars available- Access to Prometheus/Grafana (optional but recommended)
node backend/chaos/runner.js --listnode backend/chaos/runner.js --validate db-latencyStart the server with chaos enabled:
CHAOS_ENABLED=true CHAOS_EXPERIMENT=db-latency node backend/server.jsIn a second terminal, run the runner to probe and report:
node backend/chaos/runner.js --experiment db-latency --duration 60Or point the runner at a remote staging instance:
CHAOS_TARGET_URL=https://staging.example.com \
node backend/chaos/runner.js --experiment db-latency --duration 60cd backend
npm run test:chaos| Field | Value |
|---|---|
| Fault type | latency |
| Target | database |
| Delay | 2 000 ms + 300 ms jitter |
| Probability | 100% |
| Duration | 60 s |
Hypothesis: Under sustained DB latency the circuit breaker opens within 5 slow requests and the API returns 503 instead of hanging.
Expected behaviour:
- First 5 requests time out after ~2 s each (retried 3× = ~6 s total)
- Circuit opens; subsequent requests return 503 in < 1 ms
- After 30 s timeout the circuit probes; 2 healthy probes close it
Verify in Prometheus:
circuit_breaker_state{name="database"} # should reach 1 (OPEN)
circuit_breaker_transitions_total{to="OPEN"} # should increment
http_request_duration_ms_bucket{route="/api/v1/escrows"} # spike in 2000ms bucket
| Field | Value |
|---|---|
| Fault type | error (P1001) |
| Target | database |
| Probability | 100% |
| Duration | 30 s |
Hypothesis: DB errors trigger 3 retries with backoff (1 s → 2 s → 4 s), then the circuit opens and subsequent requests fail fast.
Expected behaviour:
- Each request spends ~7 s in retry before surfacing 503
- After 5 failed requests the circuit opens
- Open circuit: 503 within < 5 ms (fail-fast)
Verify in Prometheus:
db_connection_errors_total{error_type="P1001"} # should climb
circuit_breaker_state{name="database"} # reaches 1 (OPEN)
| Field | Value |
|---|---|
| Fault type | timeout |
| Target | stellar |
| Timeout | 3 000 ms |
| Routes | /api/v1/escrows/broadcast |
| Probability | 100% |
| Duration | 30 s |
Hypothesis: Transaction broadcast times out within 3 s, returning 504 instead of polling for 60 s.
Expected behaviour:
- POST
/broadcastreturns 504 within ~3 s - No goroutine / event loop leak from the abandoned polling loop
Verify:
time curl -X POST $BASE_URL/api/v1/escrows/broadcast \
-H "Content-Type: application/json" \
-d '{"signedXdr":"AAAA..."}'
# Should complete in ~3 s with 504| Field | Value |
|---|---|
| Fault type | error (STELLAR_RPC_ERROR) |
| Target | stellar |
| Routes | /api/v1/escrows/broadcast |
| Probability | 100% |
| Duration | 30 s |
Hypothesis: Repeated RPC errors open the Stellar circuit breaker; subsequent broadcast attempts return 503 fail-fast.
Verify in Prometheus:
circuit_breaker_state{name="stellar-rpc"} # reaches 1 (OPEN)
circuit_breaker_calls_total{name="stellar-rpc",outcome="rejected"}
| Field | Value |
|---|---|
| Fault type | http-error |
| Target | api |
| Status code | 500 |
| Probability | 30% |
| Routes | /api/v1/escrows |
| Duration | 60 s |
Hypothesis: ~30% of GET /escrows fail with 500; the circuit breaker stays CLOSED (below the 5-failure threshold); healthy requests still succeed.
Verify:
- Error rate visible in Prometheus:
rate(http_requests_total{status_code="500"}[1m])≈ 30% of escrow reads - Circuit stays CLOSED:
circuit_breaker_state{name="database"} == 0
| Field | Value |
|---|---|
| Fault type | latency |
| Target | api |
| Delay | 1 000–1 500 ms |
| Routes | /api/v1/escrows |
| Probability | 100% |
| Duration | 60 s |
Hypothesis: P99 latency exceeds the 1 000 ms SLO and Grafana alert fires.
Verify:
histogram_quantile(0.99, rate(http_request_duration_ms_bucket{route="/api/v1/escrows"}[5m]))
# Should exceed 1000
| Parameter | Default | Description |
|---|---|---|
| failureThreshold | 5 | Failures within windowSize before opening |
| successThreshold | 2 | Consecutive successes in HALF_OPEN to close |
| timeout | 30 000 ms | Wait in OPEN before probing |
| windowSize | 10 000 ms | Sliding window for failure counting |
CLOSED ─(5 failures / 10 s)─▶ OPEN ─(after 30 s)─▶ HALF_OPEN
▲ │
└──────────────(2 consecutive successes)────────────────┘
│
OPEN ◀────────(any failure in HALF_OPEN)────────────────┘
curl http://localhost:3000/health | jq '.circuitBreakers'Open the STE Overview dashboard at http://localhost:3001 (or your Grafana instance).
Key panels to watch during chaos:
- HTTP Error Rate — should spike during fault injection
- DB Query Duration — should spike during db-latency experiment
- Circuit Breaker State — should transition to 1 (OPEN) during db-failure/stellar-error experiments
# Circuit breaker states (0=CLOSED, 1=OPEN, 2=HALF_OPEN)
circuit_breaker_state
# Chaos faults injected per experiment
chaos_injected_total
# Error rate
rate(http_requests_total{status_code=~"5.."}[1m])
# DB retry errors
rate(db_connection_errors_total[1m])
If the breaker stays OPEN beyond the expected recovery window:
- Check the dependency is actually healthy:
curl $DB_HOST/healthor Stellar RPC - If healthy, manually reset via the admin endpoint (if implemented) or restart the process
- Review
circuit_breaker_transitions_totalmetric to confirm the timeline
- Disable chaos: unset
CHAOS_ENABLED/CHAOS_EXPERIMENTand restart the process - Check for leaked connections in PostgreSQL:
SELECT count(*) FROM pg_stat_activity - Drain and restart the connection pool if needed
The escrow system is append-only for financial records. No data should be mutated by chaos experiments — they inject faults at the transport/connection layer only. If data inconsistency is observed:
- Check audit log:
GET /api/v1/admin/audit - Cross-reference on-chain Stellar events with the database via the event indexer
- Add an entry to backend/chaos/config/experiments.json
- Add a test case in backend/tests/chaos/chaosExperiments.test.js
- Update this runbook with the experiment's hypothesis and verification steps
- Run
npm run test:chaosto confirm the test passes