Library / SDK
lingrino/go-fault avatar
lingrino/go-fault

go-fault injects HTTP failures into Go services with middleware

go fault injection library

556 stars24 forksGoMIT

At a glance

What is it?
go-fault is a small Go package that wraps net/http handlers with fault injection middleware, so you can add latency, HTTP errors and rejected requests to a percentage of live traffic and watch how the rest of the stack reacts. It is a deliberately narrow tool, and the dropped request case falls outside what it can reproduce.
Who is it for?
Adopt go-fault when you run a net/http based Go service and want to rehearse slow, erroring or rejected requests against real traffic; skip it if you need dropped connections, non-HTTP protocols or a broad chaos testing platform. Before wiring it into anything, check the participation and path filter settings in fault.NewFault against your own handlers, since a fault that fires on a health check or a metrics path is a self-inflicted outage.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 46 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A Go middleware package for failing HTTP requests on purpose

go-fault is a Go package for injecting faults into HTTP services, and its scope is narrower than the name alone suggests. The README states that the package provides go http middleware that makes it easy to inject faults into your service, and it lists four concrete actions: reject an incoming request, respond with an HTTP error, inject latency into a percentage of requests, or inject a custom fault of your own. The intended user is an engineer who needs to see how a service behaves when part of it fails, and who already serves traffic through net/http handlers and middleware. Anyone looking for a general chaos testing framework, a process killer, or a tool that corrupts packets at the network level will not find those things here. The module is github.com/lingrino/go-fault, its go.mod asks for Go 1.25.7, and the only direct dependency is testify, which the package uses for its own test suite.

Injectors carry the failure, the Fault decides when it fires

The design splits the work into two layers, and that split is the reason the package is easy to extend. You first create an Injector, which is a middleware holding the code to run when the fault fires. You then wrap that Injector in a Fault, and the Fault owns the logic about when the injector should run. Three injectors ship with the package: SlowInjector, ErrorInjector and RejectInjector. Each of them is configured through a Fault, which means the same failure behaviour can be driven by different policies without rewriting the failure itself. The configuration options visible in the usage example are WithEnabled, WithParticipation, which sets the fraction of requests that get the fault, and WithPathBlocklist, which excludes specific paths. The README also says a Fault can allowlist paths instead, so the filtering works in both directions. The file list at the repository root is consistent with this layering, since injector_chain.go, injector_error.go, injector_reject.go, injector_slow.go and injector_random.go sit alongside fault.go and injector.go, and reporter.go suggests that injectors can report what they did when they fire.

Adding two seconds of latency to a quarter of your requests

The fastest way to see the mechanism is the example in the README, which registers the package as a module and builds a slow injector wrapped in a Fault. Here is the handler and import setup, unchanged from the example:

go
// main.go
package main

import (
        "net/http"
        "time"

        "github.com/lingrino/go-fault"
)

var mainHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        http.Error(w, http.StatusText(http.StatusOK), http.StatusOK)
})

The main function turns the injector into a middleware chain and listens on a local address:

go
func main() {
        slowInjector, _ := fault.NewSlowInjector(time.Second * 2)
        slowFault, _ := fault.NewFault(slowInjector,
                fault.WithEnabled(true),
                fault.WithParticipation(0.25),
                fault.WithPathBlocklist([]string{"/ping", "/health"}),
        )

        // Add 2 seconds of latency to 25% of our requests
        handlerChain := slowFault.Handler(mainHandler)

        http.ListenAndServe("127.0.0.1:3000", handlerChain)
}

The fault is created with WithEnabled(true), WithParticipation(0.25) and WithPathBlocklist for the ping and health paths, and the resulting handler chain is served on 127.0.0.1:3000. Because the middleware is a plain net/http handler, you would load the service in a browser or with curl and watch roughly a quarter of the requests take two seconds longer while the blocked paths stay responsive. Note that both constructor calls discard their error value in the example, so in your own code you will want to handle the returned error rather than ignore it.

Dropped requests are the failure mode go-fault cannot reproduce

The README states the limitation plainly: one common failure scenario the package cannot perfectly simulate is dropped requests. The reason is structural. The RejectInjector always returns immediately to the user, but a real dropped request is one where the client sees nothing at all, no status line and no body, because the connection died before a response was written. A middleware that always answers cannot imitate that. The workaround the project offers is a chain, specifically a SlowInjector with a very long wait time placed in front of an eventual RejectInjector, which is meant to approximate the client experience of a request that never came back. That is a simulation of the symptom rather than of the network condition, so if you are testing a client's timeout and retry logic, treat the result as indicative. This is also the clearest signal about where go-fault sits: it operates above the transport, inside your own process, and it cannot reach the layer where packets are lost, connections are reset or a proxy times out.

A disabled fault still occupies the request path

The project takes performance claims seriously enough to publish a comparison across three states: no fault at all, the fault wired in but disabled, and the fault enabled. The README says the package is safe to leave implemented even when no fault is being injected, and that while the fault is disabled the degradation is negligible compared with removing the package from the request path. It also says that while enabled there may be minor performance differences, but only in the window where you are already injecting faults. The benchmark names in the published output make the comparison explicit, with BenchmarkNoFault, BenchmarkFaultDisabled and entries at zero and one hundred percent participation. The numbers in that output are from a single machine and one run, so they are best read as a shape rather than a guarantee. You can reproduce the same set locally with the standard Go tooling:

shell
$ go test -v -cover -race ./...
$ go test -run=XXX -bench=.

The test suite is described as extensive, with code coverage of 100 percent published as an artifact on every GitHub Actions run, and a linter job running golangci-lint after each push.

Stable scope, no releases, and a last push on 2026-09-07

Maintenance has to be read from two facts: the repository is not archived, and the last push was on 2026-09-07. The project also has no GitHub releases, so there is no tagged version stream to track and no changelog of published versions in the usual sense, although a CHANGELOG.md file sits at the repository root. The README describes the project as in a stable and supported state, with no plans to introduce significant new features, while still welcoming ideas and contributions under the guidelines in .github/CONTRIBUTING.md. In practice that reads as a finished, deliberately bounded tool rather than a growing platform. The dependency set is small and current, with testify v1.12.1 as the only direct requirement, and renovate.json at the root indicates automated dependency updates. Licensing is the MIT License, which is permissive and carries no copyleft obligation on code that links the package.

What a hand-rolled middleware gives up when you use go-fault

The obvious alternative is to write the injection middleware yourself, because the shape of the example is short enough to copy. The difference is not the amount of code, it is the policy layer. A hand-rolled version would need its own random number draw, its own path matching, its own on and off switch, and its own way of excluding health and metrics endpoints, and every service in the estate would end up with a slightly different implementation. go-fault centralises those decisions in the Fault, so a participation rate and a path blocklist are expressed once as options and applied identically wherever the middleware is mounted. The counterargument is real: with only three built in injectors, a custom failure that does not fit the Slow, Error or Reject shapes means writing an Injector and wiring it in yourself, and a non-HTTP service cannot use the package at all. For a fleet of net/http services that want a shared, reviewable failure rehearsal story, the shared policy is worth the dependency.

Editorial conclusion

Adopt go-fault when you run a net/http based Go service and want to rehearse slow, erroring or rejected requests against real traffic; skip it if you need dropped connections, non-HTTP protocols or a broad chaos testing platform. Before wiring it into anything, check the participation and path filter settings in fault.NewFault against your own handlers, since a fault that fires on a health check or a metrics path is a self-inflicted outage.

Frequently asked questions

How do I add go-fault to a Go service?

The module path is github.com/lingrino/go-fault and the example imports it directly as that path. The go.mod file requires Go 1.25.7, and the README points readers to the godoc page for further documentation.

Which fault types does go-fault support?

Three kinds ship with the package: SlowInjector, ErrorInjector and RejectInjector. The README also says you can inject a custom fault of your own, and that a Fault can run its injector on a small percent of requests with path blocklist or allowlist filtering.

Can go-fault simulate a dropped request?

Not directly. The RejectInjector always returns immediately, so the closest approximation the project describes is chaining a SlowInjector with a very long wait in front of an eventual RejectInjector.

Does go-fault slow my service down when fault injection is turned off?

The README says the package is safe to leave in place, and that while the fault is disabled the degradation is negligible compared with removing the package from the request path. Benchmarks compare no fault, disabled fault and enabled fault, and are available as GitHub Actions artifacts.

Official sources

  1. Issues
  2. License: MIT
  3. lingrino/go-fault on GitHub
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/lingrino-go-fault.svg)](https://hysenlabs.com/projects/lingrino-go-fault)

Was this useful?