# FAIR, a Go library that throttles clients only when a resource is short

> FAIR is a Go library for sharing a scarce resource between many clients without letting noisy neighbours take it all. It adapts the Stochastic Fair BLUE algorithm, keeps its state in a multi-level Bloom filter so memory does not grow with the client count, and exposes a builder, a RegisterRequest call and a ReportOutcome call that is where the throttling decision comes from.

**satmihir/fair** — A Go library for serving resources fairly

- Repository: https://github.com/satmihir/fair
- Stars: 784 · Forks: 28
- Language: Go
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/satmihir-fair

## The problem FAIR solves: one client taking the whole resource

A shared resource with many clients has one recurring failure mode. Some clients ask for it constantly, others ask occasionally, and the scheduler or the queue ends up serving the noisy neighbours while the well behaved workloads starve. The conventional answer, a token bucket or a leaky bucket, throttles by rate, and it rejects a request even when the resource happens to be free.

FAIR takes the opposite position. The README states the philosophy directly: throttle only when there is a genuine shortage of resources. Requests that arrive while capacity is available are served, and throttling begins once failures indicate that the resource is actually missing. That is a behavioural difference you can feel in production. A token bucket with a conservative rate rejects work that would have succeeded; FAIR adapts to the shortage instead of to a configured guess.

The audience is therefore Go developers running a service with a shared, regenerating resource, where fairness between clients matters more than a fixed per-client rate. The library is described as framework and protocol agnostic and is meant to drop into any HTTP or gRPC service.

One thing FAIR is not is a quota system. Nothing here tells a client how many calls it is allowed per day, and nothing enforces a contract the client can read. Fairness here means getting a share of what is actually available.

## Stochastic Fair BLUE, and the multi-level Bloom filter state

The core algorithm is based on Stochastic Fair BLUE, a scheme the README says is often used for network congestion control, with a few modifications. The BLUE idea is that a probability value is maintained per flow, and that value rises when failures indicate overload and falls when requests succeed. A request is throttled with a probability drawn from that value, so throttling is probabilistic rather than a hard cut-off.

FAIR keeps that state in a multi-level Bloom Filter style structure, and this is the design decision that shapes everything else. A Bloom filter is a fixed size set of buckets addressed by hash, so the memory the library needs is constant and does not scale with the number of clients. When the FAIR configuration is right, the library scales to a very large number of clients with a low probability of false positives and a near zero probability of persistent false positives.

Bloom filters give up exactness in exchange for that constant footprint, so the errors are worth naming. A false positive means a client can be treated as a bad flow when it is not, and since the probability values decay when requests succeed, a mistaken penalty does not stick around. The result is a state structure that a service can keep in memory for millions of client identifiers without growing, and that can be thrown away and rebuilt on restart without losing correctness, since it holds learned behaviour rather than durable state.

The hashing itself uses murmur3, listed as a dependency in the module manifest.

## Hash rotation: why a few minutes is the horizon

Clients are hashed into buckets, and buckets are where the probability values live. The problem with a fixed hash is correlation. If several clients share a bucket, they share fate, and a unlucky assignment can keep penalising the same clients for as long as the structure lives.

FAIR handles this with a hash rotation mechanism that regularly rehashes clients, so no correlated behaviour lasts longer than a few minutes. The builder exposes the interval directly, and the README example sets it to one minute:

```go
trkB := tracker.NewFairnessTrackerBuilder()
// Rotate the underlying hashes every one minute to avoid correlated false positives
trkB.SetRotationFrequency(1 * time.Minute)

trk, err := trkB.Build()
defer trk.Close()
```

This is a real trade-off rather than a free win. A longer rotation interval gives clients more time to settle into a stable assignment, and a shorter one spreads the penalty more evenly but keeps resetting the learned state. The default is the automatic tuning the README advertises, so most teams never touch this value.

The builder shape matters too. The library separates the default path from the tuned path, and the default path needs one call:

```go
trkB := tracker.NewFairnessTrackerBuilder()

trk, err := trkB.BuildWithDefaultConfig()
defer trk.Close()
```

The README describes the defaults as working well in most cases, with automatic tuning and minimal configuration out of the box, and full tuning available when a workload needs it.

## RegisterRequest and ReportOutcome: where the decision comes from

The API is two calls, and the second one is the one that carries the meaning.

RegisterRequest is called for every incoming request with a flow identifier, and it returns a result whose ShouldThrottle field tells you whether to reject the request. The identifier is the granularity at which fairness is maintained, and the README suggests a client ID:

```go
ctx := context.Background()
id := []byte("client_id")

resp := trk.RegisterRequest(ctx, id)
if resp.ShouldThrottle {
    throttleRequest()
}
```

ReportOutcome is how the library learns. For a failure that indicates a shortage of the resource, you report a failure outcome, and for a success you report a success outcome:

```go
ctx := context.Background()
id := []byte("client_id")

trk.ReportOutcome(ctx, id, request.OutcomeFailure)
```

```go
ctx := context.Background()
id := []byte("client_id")

trk.ReportOutcome(ctx, id, request.OutcomeSuccess)
```

The distinction between the two kinds of failure is the part that needs care from the integrator. Business logic failures that have nothing to do with a shortage should not be reported at all, because reporting them inflates the penalty on a client that is not misbehaving. The README is explicit about this: report failures that indicate a shortage of the resource, report nothing for other failures.

That means the library only knows about shortages that your application actually reports, and a service that swallows its errors will never throttle.

## Instrumentation with IncludeStats and ResultStats

Debugging a fairness decision is otherwise awkward, since the state is a hashed probability structure rather than a readable table. The configuration exposes IncludeStats, which is false by default, and when it is enabled the result of a registered request carries per bucket statistics.

```go

conf := config.DefaultFairnessTrackerConfig() //default IncludeStats=false
conf.IncludeStats = true
tracker, _ := tracker.NewFairnessTracker(conf)
```

ResultStats holds probabilities and other debugging information collected while registering a request, and it is accessible after registering requests or reporting outcomes:

```go
result := tracker.RegisterRequest(ctx, clientID)
if result.Stats != nil {
    log.Printf("Bucket: %v, Requests: %d, Throttled: %d", 
        result.Stats.BucketID, result.Stats.RequestCount, result.Stats.ThrottledCount)
}
```

The bucket identifier in the log line is the useful part when investigating a complaint. It tells you which bucket a client landed in, and with how many requests and how many throttles, which is enough to tell a genuine noisy neighbour apart from an unlucky hash collision after a rotation.

Turning statistics on in production is a cost decision rather than a free diagnostic. The structure exists to keep memory constant, and per bucket counters add to that. The README suggests the setting for debugging and for monitoring workload behaviour, so the sensible path is to enable it while investigating and leave it off otherwise.

## Measuring it: the fairness evaluator and Jain's index

The repository includes a deterministic evaluator in cmd/fairness_eval that compares a shared token bucket with and without FAIR. It reports Jain's fairness index over each client's allocation relative to its max-min-fair entitlement, plus resource utilisation as a throughput guardrail:

```bash
go run ./cmd/fairness_eval
```

The README publishes the headline results for the default FAIR configuration:

| Scenario | No FAIR Jain | FAIR Jain | Improvement | Utilization with FAIR |
| --- | ---: | ---: | ---: | ---: |
| API noisy neighbors | 0.2184 | 0.6673 | +205.5% | 99.97% |
| Batch surge | 0.7497 | 0.9944 | +32.6% | 89.39% |

Those are the project's own numbers from its own benchmark, not an independent measurement, and they should be read that way. What the table shows is the shape of the trade: a large fairness gain on the noisy neighbour workload with utilisation held at 99.97 percent, and a smaller gain on the batch surge workload with a noticeable utilisation cost.

The README goes further with a focused configuration sweep on the noisy neighbour workload. The strongest practical point used MinFinalProbabilityFunction, tolerableBadRequestsPerBadFlow of 60, Lambda of 0, and at least 1,500 buckets per level, reaching a Jain score of 0.8498 with 99.28 percent utilisation. That is described as a +289.1 percent improvement over no FAIR and about +27.3 percent over the default configuration for that workload.

The practical reading is that the default configuration is not the best configuration for every workload, and the evaluator is the tool for finding the better one. The bundled browser demo in demo/fairness_visual shows the same noisy neighbour case visually.

## Limitations, alternatives and the shape of the project

The clearest alternative is the token bucket, and the difference is in the trigger rather than in the arithmetic. A token bucket admits a client according to a configured rate, so it rejects requests while the resource is still available and treats the configured rate as the truth. FAIR infers the shortage from reported failures and only throttles when the resource is genuinely missing. If you need a predictable, per client rate that a client can plan around, a token bucket is the better tool. If you need every client to keep a share of a scarce resource without knowing the right rate in advance, FAIR is built for that.

The limitations follow from the design. The structure is probabilistic, so a well behaved client can occasionally be throttled, and the near zero probability of persistent false positives is a claim about the configuration, not a guarantee. FAIR also does nothing about the ordering of requests it admits, and it has no view of priority, so a paid tier and a free tier are distinguished only by which flow identifier you pass in. Finally, the whole mechanism depends on outcomes being reported accurately; a service that reports success for a request that actually failed will teach the tracker that the resource is plentiful.

The repository is small and self contained. The module targets Go 1.22.2, depends on murmur3 for hashing and protobuf for serialization, and the Makefile drives protobuf generation, the build, the tests and golangci-lint:

```bash
# Run tests
test: proto
    @echo "Running tests..."
    @go test -v ./...
```

The last push was on 2026-07-06, and the release history shows v1.0.0 on 2025-06-06 after an initial alpha in November 2024. The README carries a coverage badge at 50.4 percent, which is worth knowing before you treat the library as a finished component in a critical path. The licence is MIT, so there is no copyleft obligation on code that uses it. That is a licensing note rather than legal advice.

## Conclusion

FAIR fits Go services where one tenant can crowd out the others, especially noisy neighbour workloads on an API where a token bucket would reject requests that could have been served, and it is a poor fit when you need per-client quotas you can reason about, when you want to reject a request before doing any work, or when you cannot afford to carry a probabilistic structure in front of a regulated endpoint. Verify first that the flow identifier you pass to RegisterRequest and ReportOutcome is the same granularity at which you want fairness, run the bundled evaluator with go run ./cmd/fairness_eval against your own workload shape, and confirm that the bucket count and rotation frequency in your configuration match the client population you expect.

## FAQ

### How do I install and set up the FAIR Go library?

The README gives the install command go get github.com/satmihir/fair, then creates a tracker with tracker.NewFairnessTrackerBuilder() and BuildWithDefaultConfig(), and closes it with defer trk.Close(). The module requires Go 1.22.2.

### How does FAIR decide to throttle a request?

You pass a flow identifier to RegisterRequest and read the ShouldThrottle field of the result. The learned probability comes from outcomes you report with ReportOutcome, where a failure that indicates a shortage raises the penalty and a success lowers it.

### Which failures should I report to FAIR?

Only failures that indicate a shortage of the resource. The README states that for other failures that are failures in your business logic but do not indicate a resource shortage, you should not report any outcome at all.

### How much memory does FAIR use as the number of clients grows?

The state is kept in a multi-level Bloom Filter style structure, so the memory needed is constant and does not scale with the number of clients. The README says that when properly configured FAIR can scale to a very large number of clients with a low probability of false positives.

### What is the difference between FAIR and a token bucket?

FAIR only throttles when there is a genuine shortage of resources, whereas token bucket and leaky bucket approaches may reject requests even when the resource is still available. The README adds that a creative configuration of FAIR can produce that behaviour, but it does not encourage it.

## Sources

- [Issues](https://github.com/satmihir/fair/issues)
- [License: MIT](https://github.com/satmihir/fair/blob/main/LICENSE)
- [README](https://github.com/satmihir/fair/blob/main/README.md)
- [Releases](https://github.com/satmihir/fair/releases)
- [satmihir/fair on GitHub](https://github.com/satmihir/fair)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/satmihir-fair
