Chaos Engineering Made Easy: Fault Injection with Rift¶
Test your application's resilience without deploying to production
Your application works perfectly in ideal conditions. But what happens when: - The payment service takes 5 seconds instead of 200ms? - The authentication API returns 503 for 10% of requests? - The database connection drops mid-request?
These scenarios are hard to reproduce in testing — but they happen in production. Chaos engineering helps you prepare.
Rift makes fault injection declarative and simple. No code changes. No complex setup. Just JSON configuration.
The _rift.fault Extension¶
Rift extends Mountebank with native fault injection through the _rift namespace:
{
"is": { "statusCode": 200, "body": "OK" },
"_rift": {
"fault": {
"latency": { "probability": 0.3, "minMs": 100, "maxMs": 500 }
}
}
}
This adds 100-500ms latency to 30% of responses. The other 70% respond normally.
Latency Faults¶
Fixed Delay¶
Every request takes exactly 2 seconds.
Random Delay Range¶
50% of requests get 500-3000ms delay.
Use Case: Timeout Testing¶
{
"port": 4545,
"stubs": [{
"predicates": [{ "equals": { "path": "/api/slow-service" } }],
"responses": [{
"is": { "statusCode": 200, "body": "Success" },
"_rift": {
"fault": {
"latency": { "probability": 0.2, "ms": 10000 }
}
}
}]
}]
}
Test that your client properly times out and retries.
Error Faults¶
HTTP Error Responses¶
{
"_rift": {
"fault": {
"error": {
"probability": 0.1,
"status": 503,
"body": "Service Unavailable",
"headers": {
"Retry-After": "30"
}
}
}
}
}
10% of requests return 503 with a Retry-After header.
Multiple Error Types¶
{
"stubs": [
{
"predicates": [{ "equals": { "path": "/api/flaky" } }],
"responses": [
{
"is": { "statusCode": 200, "body": "Success" },
"_rift": {
"fault": {
"error": { "probability": 0.05, "status": 500 }
}
}
},
{
"is": { "statusCode": 200, "body": "Success" },
"_rift": {
"fault": {
"error": { "probability": 0.1, "status": 503 }
}
}
},
{
"is": { "statusCode": 200, "body": "Success" },
"_rift": {
"fault": {
"error": { "probability": 0.02, "status": 429 }
}
}
}
]
}
]
}
Response cycling with different fault probabilities simulates a realistically flaky service.
TCP Faults¶
For testing connection-level failures:
Connection Reset¶
5% of connections receive RST (connection reset by peer).
Connection Timeout¶
3% of connections hang indefinitely (until client timeout).
Connection Close¶
2% of connections close without sending any response.
Combining Faults¶
Stack multiple fault types:
{
"is": { "statusCode": 200, "body": "OK" },
"_rift": {
"fault": {
"latency": {
"probability": 0.3,
"minMs": 100,
"maxMs": 500
},
"error": {
"probability": 0.1,
"status": 503
},
"tcp": {
"probability": 0.02,
"type": "reset"
}
}
}
}
Order of evaluation: 1. TCP fault checked first (if triggered, connection fails) 2. Error fault checked (if triggered, error response sent) 3. Latency applied to successful responses
Real-World Scenarios¶
Scenario 1: Rate Limiting Simulation¶
{
"port": 4545,
"stubs": [{
"predicates": [{ "startsWith": { "path": "/api/" } }],
"responses": [
{
"is": { "statusCode": 200, "body": "OK" },
"_behaviors": { "repeat": 10 }
},
{
"is": {
"statusCode": 429,
"headers": { "Retry-After": "60" },
"body": { "error": "Rate limit exceeded" }
}
}
]
}]
}
Allow 10 requests, then return 429. Cycles back to 200 after.
Scenario 2: Cascading Failure¶
Simulate a degraded backend:
{
"port": 4545,
"name": "Degraded Service",
"stubs": [{
"predicates": [{ "equals": { "path": "/health" } }],
"responses": [{
"is": { "statusCode": 200, "body": "healthy" }
}]
}, {
"predicates": [{ "startsWith": { "path": "/api/" } }],
"responses": [{
"is": { "statusCode": 200 },
"_rift": {
"fault": {
"latency": { "probability": 0.8, "minMs": 2000, "maxMs": 5000 },
"error": { "probability": 0.3, "status": 503 }
}
}
}]
}]
}
Health check passes, but 80% of API calls are slow and 30% fail.
Scenario 3: Network Partition¶
{
"port": 4545,
"stubs": [{
"responses": [{
"is": { "statusCode": 200 },
"_rift": {
"fault": {
"tcp": { "probability": 1.0, "type": "timeout" }
}
}
}]
}]
}
All connections hang — simulating complete network partition.
Scenario 4: Retry Testing¶
{
"stubs": [{
"predicates": [{ "equals": { "path": "/api/action" } }],
"responses": [
{
"is": { "statusCode": 503, "body": "Unavailable" },
"_behaviors": { "repeat": 2 }
},
{
"is": { "statusCode": 200, "body": "Success!" }
}
]
}]
}
Fails twice, then succeeds. Perfect for testing retry logic.
Scenario 5: Intermittent Failures¶
{
"stubs": [{
"predicates": [{ "equals": { "path": "/api/unreliable" } }],
"responses": [{
"is": { "statusCode": 200, "body": "OK" },
"_rift": {
"fault": {
"error": {
"probability": 0.15,
"status": 500,
"body": { "error": "Internal server error", "transient": true }
}
}
}
}]
}]
}
15% random failure rate — matches many real-world services.
Monitoring Fault Injection¶
Rift exposes Prometheus metrics for fault injection:
# HELP rift_fault_injections_total Total fault injections
# TYPE rift_fault_injections_total counter
rift_fault_injections_total{type="latency"} 150
rift_fault_injections_total{type="error"} 42
rift_fault_injections_total{type="tcp_reset"} 3
Use these metrics to verify faults are being triggered as expected.
Comparison: Rift vs Mountebank Fault Injection¶
| Feature | Mountebank | Rift |
|---|---|---|
| Latency simulation | _behaviors.wait |
_rift.fault.latency |
| Probabilistic | Requires inject |
Native support |
| TCP faults | Not supported | _rift.fault.tcp |
| Error injection | Manual with cycling | Native _rift.fault.error |
| Metrics | None | Prometheus |
Rift's fault injection is: - Declarative: No JavaScript required - Probabilistic: Built-in random distribution - Observable: Prometheus metrics included
Integration with Tests¶
Jest Example¶
describe('Resilience Tests', () => {
beforeAll(async () => {
await server.createImposter({
port: 4545,
stubs: [{
responses: [{
is: { statusCode: 200, body: 'OK' },
_rift: {
fault: {
latency: { probability: 0.5, ms: 3000 }
}
}
}]
}]
});
});
it('handles slow responses with timeout', async () => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1000);
await expect(
fetch('http://localhost:4545/api', { signal: controller.signal })
).rejects.toThrow('aborted');
clearTimeout(timeout);
});
});
pytest Example¶
def test_handles_service_errors(mock_server):
mock_server.create_imposter({
"port": 4545,
"stubs": [{
"responses": [{
"is": {"statusCode": 200},
"_rift": {
"fault": {
"error": {"probability": 1.0, "status": 503}
}
}
}]
}]
})
with pytest.raises(ServiceUnavailable):
client.call_service()
Best Practices¶
- Start with low probabilities: Begin at 5-10%, increase gradually
- Test one fault type at a time: Isolate failure modes
- Monitor metrics: Verify faults trigger at expected rates
- Combine with response cycling: Create realistic failure patterns
- Document fault configurations: Future you will thank you
What's Next?¶
Fault injection tests single-request failures. But what about multi-step flows?
Next Post: Building Stateful Mock Services with Flow State
What resilience patterns do you test for? Share your chaos engineering strategies in the comments!
Tags: #ChaosEngineering #FaultInjection #Resilience #Testing #Rift #APITesting