_______ _______ _______ _______
( )( ___ )|\ /||\ /|( ____ \( )
| () () || ( ) |( \ / )| ) ( || ( \/| () () |
| || || || (___) | \ (_) / | (___) || (__ | || || |
| |(_)| || ___ | \ / | ___ || __) | |(_)| |
| | | || ( ) | ) ( | ( ) || ( | | | |
| ) ( || ) ( | | | | ) ( || (____/\| ) ( |
|/ \||/ \| \_/ |/ \|(_______/|/ \|
HTTP failure injection proxy for testing resilience.
Mayhem sits between client and server, matches requests against rules, injects failures with configured probability. Basically tests if your application handles errors correctly.
This tool breaks HTTP traffic deliberately:
- Drops connections
- Returns errors
- Adds latency
- Corrupts responses
- Throttles bandwidth
Do not:
- Run against production without approval and rollback plan
- Use on third-party APIs you don't control
- Deploy without monitoring
- Enable without understanding blast radius
Requires Go 1.22+
git clone <repository>
cd malice
make buildBinary: ./malice
Create config.yaml:
version: v1
rules:
- name: api-errors
matcher:
paths: ["/api/*"]
probability: 0.1
action:
type: error
status_code: 503Start proxy:
./malice start -f config.yaml -t http://localhost:3000 -l :8080Traffic to :8080 proxies to localhost:3000 with failures injected per rules.
Rules match on:
matcher:
methods: [GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS]
paths: ["/api/*", "/users/*/profile"] # Wildcards supported
headers:
Content-Type: "application/json"
X-API-Key: "test-*"
query_params:
format: "json"
body:
json_path: "$.amount"
operator: ">"
value: 100All conditions are AND. First matching rule wins. Rules evaluated in order.
Add latency:
action:
type: delay
duration: 500ms # Fixed
# OR
duration_min: 100ms # Random range
duration_max: 2sReturn HTTP error:
action:
type: error
status_code: 503
body: '{"error": "unavailable"}'
headers:
Content-Type: "application/json"Close connection:
action:
type: abort
timing: before_response # before_response | after_headers | mid_bodyLimit bandwidth:
action:
type: throttle
bytes_per_second: 10240 # 10KB/sDamage response:
action:
type: corrupt
mode: truncate # truncate | flip_bits | invalid_json | random_bytes
rate: 0.01 # For flip_bits/insert_garbageHold connection then close:
action:
type: timeout
duration: 30sPick random action:
action:
type: random
choices:
- type: error
status_code: 500
- type: delay
duration: 5s
- type: abortExplicitly allow through (no failure):
action:
type: pass0.0 to 1.0:
1.0= always inject0.5= 50% chance0.1= 10% chance0.0= never (same asenabled: false)
./malice validate -f config.yaml # Validate config
./malice start -f config.yaml -t URL # Start proxy
./malice start -f config.yaml -t URL -l :9000 # Custom listen port
./malice status # Show metrics (table)
./malice status -o json # Metrics as JSON
./malice status -o yaml # Metrics as YAML
./malice version # Show versionTracked:
- Requests (total, current, by method)
- Rules (loaded, enabled, matched)
- Injections (total, by rule, by action type, active, dropped)
- Delays (duration by rule)
- Errors (by status code)
- Aborts (by timing)
- Throttled bytes
View: ./malice status
Prometheus-compatible gauges and counters available via internal collector.
Structured JSON via zerolog.
export MALICE_LOG_LEVEL=debug # trace | debug | info | warn | error
export MALICE_LOG_PRETTY=true # Human-readable console outputAll logs include:
component- which package loggedtimestamp- RFC3339request_id- trace requests (when applicable)
Example:
{
"level": "info",
"component": "proxy",
"request_id": "abc123",
"method": "GET",
"path": "/api/users",
"matched_rule": "api-errors",
"action": "error",
"status_code": 503,
"timestamp": "2025-01-01T12:00:00Z"
}examples/simple.yaml - Minimal starter
examples/comprehensive.yaml - All action types, advanced matching
go test ./... # All tests
go test -race ./... # With race detector
make test # Via Makefile
make lint # Run linters267 tests. Full coverage of config, matcher, metrics, runner, proxy.
No built-in guardrails. Does exactly what you configure.
Best practices:
-
Validate config before starting:
./malice validate -f config.yaml -
Start with low probability (
< 0.1) -
Test in staging first
-
Monitor metrics during injection
-
Have rollback plan ready
-
Use health check pass-through:
- name: health-pass matcher: paths: ["/health", "/ready"] probability: 1.0 action: type: pass
internal/
├── cli/ # Cobra command definitions
├── config/ # YAML loading, validation, schema
├── matcher/ # Request matching, probability evaluation
├── metrics/ # Prometheus-style collectors, snapshots
├── runner/ # Action executors (delay, error, abort, etc.)
├── proxy/ # HTTP reverse proxy, action coordination
├── log/ # Structured logging setup
└── version/ # Build version info
Request flow:
- Client → Proxy (
:8080) - Proxy evaluates rules in order
- First match triggers probability check
- If injected: execute action (delay, error, abort, etc.)
- Proxy → Backend (if not aborted)
- Backend → Client (possibly corrupted/throttled)
- name: test-user-chaos
matcher:
headers:
X-User-ID: "test-*"
probability: 0.5
action:
type: random
choices:
- type: error
status_code: 500
- type: delay
duration: 10s- name: dependency-timeout
matcher:
paths: ["/api/orders/*"]
probability: 0.3
action:
type: delay
duration: 30s
- name: circuit-breaker-open
matcher:
paths: ["/api/payments"]
probability: 0.5
action:
type: error
status_code: 503- name: partition
matcher:
paths: ["/*"]
probability: 0.2
action:
type: abort
timing: before_responseSee docs/config-schema-v1.md for complete schema.
See docs/metrics.md for metric definitions and export formats.