Open source · Apache-2.0 · Single Go binary

Reliability testing for AI agents and AI applications

faultkit

Fault injection for the agent era.

Exercise the LLM, RAG, and tool-call failures that mocks can't simulate. faultkit wraps any command, injects realistic production failures into its traffic, and captures the exit code, so you run your error paths on purpose instead of discovering them in prod.

AI evals test quality. faultkit tests resilience.

Your evals tell you whether the answer was correct. faultkit tells you whether your application survives failure.


01The problem

Mocks can't simulate these.

Modern AI apps depend on fragile external systems: LLM providers, vector DBs, embedding and search APIs, tool subprocesses. SDKs handle basic retries. Most application code does not survive the failures retries can't fix.

429 mid-chain

A 429 from OpenAI mid-chain, where the retry runs on partial reasoning state and confidently returns a wrong answer.

Truncated stream

A streaming response that cuts off mid-token and gets treated as final.

Killed tool

A tool subprocess killed before it finishes, leaving the agent loop in an invalid state.

Corrupt RAG

A vector DB returning stale or shuffled results, silently corrupting RAG output.

These happen in production. Unit tests don't catch them. Integration tests against mocks test the mock. Chaos tools inject at the Kubernetes / infra boundary. faultkit injects at the SDK/API boundary, where agent failures actually live.

retry library observability tool k8s chaos monkey daemon → injects at the SDK/API boundary

02Why now

Why now.

AI applications fail in new ways, and the old testing playbook does not cover them.

Many dependencies

AI applications depend on many external systems (LLMs, vector DBs, search and embedding APIs, tool subprocesses). Every one of them can fail.

Probabilistic paths

Agent workflows are probabilistic, not deterministic. The same input can take a different path, so failures surface in places fixed tests never reach.

Testing falls short

Traditional testing falls short. Unit tests and mocks check the happy path and test the mock, not how your app behaves when a real dependency degrades.

A production problem

Reliability is becoming a production problem. As teams ship agents and AI features, "it broke weirdly in prod" is the failure mode that pages you at 3am.


03How it works

Test how your AI application behaves when the world around it breaks.

No daemon, no sidecar, no cluster. faultkit wraps the target process, injects the fault, captures the exit code. Single-shot, built for local dev and CI.

Proxy · primary

HTTPS proxy

The main mechanism. It carries the LLM, RAG, gateway, and any HTTP(S) scenario. faultkit spins up a localhost proxy with a per-run ephemeral CA; the target trusts it via standard env vars (the proxy injects HTTPS_PROXY + CA env).

  • Matches each request by host glob + path glob.
  • For matched + fired requests, synthesizes a vendor-accurate response (status, body, streaming).
  • Everything else passes through to the real upstream. The target sees a real HTTP 429 over a real TLS connection, because that's what it gets.

Some clients ignore HTTPS_PROXY entirely: Node's fetch/undici, or SDKs running in a filtered subprocess. For those, --base-url points the SDK straight at faultkit via OPENAI_BASE_URL / ANTHROPIC_BASE_URL instead of a proxy setting. No proxy env to honor, no CA to trust.

Runs on macOS and Linux. No special privileges. Serves HTTP/1.1.

Advanced execution mode

eBPF kprobes

A secondary mode for syscall-level faults on Linux, for when you need a failure below the HTTP layer. Loads small BPF programs that hook __x64_sys_* kprobes (e.g. recvmsg, recvfrom, openat) and use bpf_override_return to rewrite the syscall return for processes in the target's PID tree.

  • All decision logic (probability, matching) lives in Go.
  • The BPF program is a dumb policy enforcer reading an LRU map populated by userspace.
  • The target gets a real ECONNRESET / EACCES from the kernel.

Requires Linux 5.8+, x86-64, CAP_BPF (or root), and a kernel with CONFIG_BPF_KPROBE_OVERRIDE (most distro kernels have it).


04Scenarios

Twelve built-ins. Bring your own.

Each scenario knows exactly what to fire and how to make it look real. Pick one with --scenario, or author your own in YAML. The LLM scenarios fire against every provider faultkit knows — narrow to one with --provider.

Proxy llm-api-degraded

429 / 503 from OpenAI + Anthropic with realistic Retry-After headers.

Proxy malformed-json-response

LLM response body replaced with syntactically invalid JSON.

Proxy malformed-tool-use

Tool call with malformed or schema-violating arguments, breaking dispatch.

Proxy max-tokens-truncation

A truncated 200 (finish_reason: length / stop_reason: max_tokens) an agent treats as complete.

Proxy llm-streaming-cutoff

SSE chat completion drops mid-token without a [DONE].

Proxy · Anthropic anthropic-overloaded

HTTP 529 overloaded_error under load — no OpenAI equivalent.

Proxy · Anthropic anthropic-stream-error

SSE stream emits an error event mid-stream, with no message_stop.

Proxy · Anthropic anthropic-tool-use-cutoff

A tool_use block truncated by max_tokens — an incomplete tool call.

Proxy · Anthropic anthropic-refusal

200 with stop_reason: refusal — the model declined.

Proxy · Anthropic anthropic-request-too-large

HTTP 413 request_too_large for an oversized request.

eBPF · Linux 5.8+ flaky-network

ECONNRESET on TCP recvmsg / recvfrom.

eBPF · Linux 5.8+ tool-permission-denied

EACCES on openat.

Author your own →

Write a scenario in YAML with host/path globs and a fault probability, then pass it with --config scenario.yaml.

List them on your host with faultkit scenario list. More are on the roadmap: rag-stale-results, tool-call-flaky, gateway-timeout, and more.


05Quickstart

Install, then wrap your tests.

One binary. No config required to start: point it at a built-in scenario and a command, and read the exit code.

install
# macOS / Linux (Homebrew)
$ brew install faultkit/tap/faultkit

# Arch (AUR)
$ yay -S faultkit-bin

# Go toolchain
$ go install github.com/faultkit/faultkit/cmd/faultkit@latest

Or download a release binary from GitHub Releases.

quickstart
# Wrap your suite; faultkit injects 429s into
# the OpenAI / Anthropic calls.
$ faultkit run --scenario llm-api-degraded -- pytest tests/

# Your retry/fallback path runs for real.
# Exit code tells CI what happened:
#   0 = handled the fault
#   1 = your code broke under it
#   3 = no call matched
faultkit run: output
$ faultkit run --scenario llm-api-degraded -- pytest tests/ [faultkit] mode=proxy scenario=llm-api-degraded tests/test_chain.py::test_429_fallback FAILED tests/test_chain.py::test_partial_state PASSED [faultkit] fault fired experiment="openai-rate-limited" host=api.openai.com path=/v1/chat/completions === faultkit summary === scenario: llm-api-degraded target: pytest tests/ faults fired: 4 target exit: 1 (FAIL)

Terminal summary by default. Add --report report.json to write a JSON artifact for CI. faultkit check reports which modes are available on the host; faultkit scenario list lists built-ins.


06The contract

Exit codes.

Stable across versions, so CI scripts branch on them.

CodeMeaning
0Target passed under fault
1Target failed under fault
2faultkit internal error
3No fault fired, target never hit matched traffic
4Usage error

07Reference

Documentation.

Everything you need to run faultkit locally and wire it into CI.

#Installation

faultkit ships as a single static Go binary. Install it through a package manager, the Go toolchain, or grab a release binary.

shell
$ brew install faultkit/tap/faultkit          # macOS / Linux (Homebrew)
$ yay -S faultkit-bin                          # Arch (AUR)
$ go install github.com/faultkit/faultkit/cmd/faultkit@latest

The proxy mode runs on macOS and Linux with no special privileges. eBPF mode is Linux-only and has additional requirements (see below). Run faultkit check after install to see which modes are available on your host.

#faultkit run: usage & flags

The core command wraps a target process, injects a fault, and exits with a status code that reflects what happened. Everything after -- is the command faultkit runs.

usage
$ faultkit run [flags] -- <command> [args…]
  • --scenario <name>  Run a built-in scenario by name (see Scenarios).
  • --config <file>  Load a scenario you authored in YAML instead of a built-in.
  • --provider <id>  Limit a fixture-driven LLM scenario to one provider (e.g. openai, anthropic); the default fans out across all of them.
  • --base-url  Point the SDK at faultkit via OPENAI_BASE_URL / ANTHROPIC_BASE_URL instead of HTTPS_PROXY — for clients (Node fetch, filtered subprocesses) that ignore proxy env.
  • --mode <auto|proxy|ebpf>  Force an injection mechanism. Default auto picks by scenario.
  • --report <file.json>  Write a JSON report artifact in addition to the terminal summary.
  • --verbose  Log every matched request and fired fault for debugging.

By default faultkit prints a terminal summary: which faults fired, how the target behaved, and the final exit code.

#The proxy mechanism

For HTTP(S) scenarios (LLM providers, RAG / vector DBs, gateways), faultkit spins up a localhost proxy backed by a per-run ephemeral CA. It injects HTTPS_PROXY and the CA into the target's environment so the target trusts it through standard env vars; no system keychain changes, nothing left behind after the run.

Each request is matched by a host glob and path glob. For requests that match and fire, faultkit synthesizes a vendor-accurate response: correct status, body shape, and streaming behavior. Everything else passes straight through to the real upstream. The target sees a real HTTP 429 over a real TLS connection, because that is exactly what it receives. The proxy serves HTTP/1.1.

#The eBPF mechanism & requirements

For syscall-level faults, faultkit loads small BPF programs that hook __x64_sys_* kprobes (for example recvmsg, recvfrom, and openat) and use bpf_override_return to rewrite the syscall return value for processes in the target's PID tree. All decision logic (probability, matching) lives in Go; the BPF program is a dumb policy enforcer reading an LRU map populated by userspace. The target gets a real ECONNRESET or EACCES straight from the kernel.

Requirements

  • Linux 5.8+, x86-64.
  • CAP_BPF (or root).
  • A kernel built with CONFIG_BPF_KPROBE_OVERRIDE, which most distro kernels have.

#Authoring scenarios in YAML

Built-ins cover common cases, but you can describe your own with host and path globs and a fault that fires with a given probability. Pass it with --config.

scenario.yaml
name: stale-vector-search
experiments:
  - name: shuffled-results
    match:
      host: '*.pinecone.io'
      path: /query*
    fault:
      http_status: 200
      response_body: '{"matches":[]}'
    probability: 0.5      # fire on half of matched requests
run it
$ faultkit run --config scenario.yaml -- pytest tests/test_rag.py

The example above is the raw fault + match form for any host. Built-in LLM scenarios instead use a higher-level failure: mode with an optional provider:, which resolves to a per-provider response fixture — so one scenario covers OpenAI, Anthropic, and any provider added later.

#CI usage

Because faultkit is single-shot and communicates through exit codes, it drops straight into CI. Run it as a step and let the runner branch on the code: exit 1 means your code broke under the fault; exit 3 means no matched traffic ever hit the proxy, which usually means the test didn't call the API you targeted.

.github/workflows/fault.yml
- name: Fault injection
  run: |
    faultkit run --scenario llm-api-degraded --report fault.json -- pytest tests/
    code=$?
    if [ $code -eq 3 ]; then
      echo '::warning::no matched traffic, fault never fired'
    fi
    exit $code

#Troubleshooting

Start with faultkit check, which reports which modes are available on the host, including whether the kernel supports eBPF kprobe override. If a run exits 3, the target never hit matched traffic: confirm your host/path globs against the real requests with --verbose, which logs every matched request and fired fault.

  • Exit 3, expected a fault: globs didn't match; check hostnames and paths with --verbose.
  • eBPF unavailable: run faultkit check; you likely lack CAP_BPF or CONFIG_BPF_KPROBE_OVERRIDE.
  • TLS errors in the target: the target must inherit the env faultkit injects; don't strip HTTPS_PROXY in a wrapper script.