# go-limiter: rate limiting in Go keyed on anything you choose

> sethvargo/go-limiter is a token-bucket rate limiter for Go with HTTP middleware, an in-memory store, a Redis store and a noop store, and it depends on nothing outside the standard library. The catch is that the fastest store cannot be shared across containers, so the deployment shape decides the design.

**sethvargo/go-limiter** — A supersonic rate limiting package for Go with HTTP middleware.

- Repository: https://github.com/sethvargo/go-limiter
- Stars: 726 · Forks: 53
- Language: Go
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/sethvargo-go-limiter

## Rate limiting on a key you choose, not an IP address

sethvargo/go-limiter is a rate limiting package for Go with HTTP middleware. The design decision that sets it apart from the rest of the ecosystem is stated plainly in the README: most rate limiting libraries assume rate limiting is only for HTTP services, and bake in further assumptions such as limiting by IP address or limiting at a resolution of per second. This package rate limits on an arbitrary string key you supply, so it can limit a tenant, a device, an API key or an IP address, and it does not care whether the thing being limited is an HTTP request at all.

The audience is a Go service that needs a limiter it can configure rather than one that has to be bent to fit. The author frames the goal as configurability and flexibility without giving up throughput, and states that existing Go packages either lacked flexibility or traded flexibility for performance.

The dependency story is part of the pitch. The README says go-limit uses only the Go standard library, on the grounds that a rate limiter should not be pulling external packages. That is a real constraint on the design, and it is also the reason the store abstraction exists at all.

## Take returns the whole state of the bucket in one call

The mechanism is a token bucket with an explicit store interface. A store holds the bucket state, and the caller drives it through a Take method that returns everything needed to make a decision. The README example creates an in-memory store configured with a number of tokens per interval:

```go
    store, err := memorystore.New(&memorystore.Config{
      // Number of tokens allowed per interval.
      Tokens: 15,

      // Interval until tokens reset.
      Interval: time.Minute,
    })
    if err != nil {
      log.Fatal(err)
    }
```

Then the key identifies the bucket, and Take reports the outcome:

```go
    ctx := context.Background()
    // key is the unique value upon which you want to rate limit, like an IP or
    // MAC address.
    key := "127.0.0.1"
    tokens, remaining, reset, ok, err := store.Take(ctx, key)
    // tokens is the configured tokens (15 in this example).
    _ = tokens
    // remaining is the number of tokens remaining (14 now).
    _ = remaining
    // reset is the unix nanoseconds at which the tokens will replenish.
    _ = reset
    // ok indicates whether the take was successful. If the key is over the
    // configured limit, ok will be false.
    _ = ok
    if !ok {
      return fmt.Errorf("rate limited: retry at %v", reset)
    }
```

The five return values are the whole API surface most callers need. Tokens is the configured ceiling, remaining is what is left in the current interval, reset is the unix nanosecond timestamp at which the bucket refills, and ok tells you whether the take was permitted.

## httplimit wraps a standard handler and sets four headers

For HTTP services, the httplimit package avoids the boilerplate of calling Take yourself on every route. After creating a store, you wrap Go's standard handler:

```golang
middleware, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc())
if err != nil {
  log.Fatal(err)
}

mux1 := http.NewServeMux()
mux1.Handle("/", middleware.Handle(doWork)) // doWork is your original handler
```

The middleware sets four response headers, which the documentation says conform to the latest RFCs. X-RateLimit-Limit carries the configured limit, described as constant. X-RateLimit-Remaining carries the number of tokens left in the current interval. X-RateLimit-Reset carries the UTC time when the limit resets. Retry-After carries the time at which to retry.

Two things are worth noticing. The IPKeyFunc is a choice rather than a hard coded behaviour, so you can substitute a function that keys on a tenant header or a session, which is the whole point of the arbitrary key design. And the middleware does not reject anything itself in the example, it sets the headers and hands the request to your handler, so the decision about what to return on a limited request stays in your code rather than being forced on you.

## Three stores, and the fastest one cannot be shared

The store interface in the repository root is what makes the package configurable, and three implementations ship with it. The memory store is described as the fastest, with the important limitation that it only works on a single container or virtual machine since there is no way to share the state. The Redis store uses Redis plus Lua as a shared pool, and the documentation is blunt about the trade: it comes at a performance cost. It lives in its own repository, sethvargo/go-redisstore. The noop store does no rate limiting at all but still implements the interface, which the documentation suggests is useful for testing and local development.

That third option is small and worth more attention than it gets. A noop store that satisfies the same interface means your test suite does not need a Redis container, and a developer running the service locally does not need a shared backend to reproduce a request path that depends on rate limiting.

The repository layout is small and easy to read: limiter.go, store.go, and directories for httplimit, memorystore, noopstore, internal, and a separate benchmarks directory. The go.mod file declares the module as github.com/sethvargo/go-limiter and requires Go 1.25.

## Running the benchmarks yourself instead of trusting a table

The README publishes sample benchmark output, and it is unusually open about how the numbers were produced. They come from benchmarks with 100,000 unique keys, and the README states you can run them yourself. The Makefile has a target for it:

```bash
$ make benchmarks
```

The published figures put the memory store at 81.7 ns/op and 16 B/op with 1 alloc per op in the serial case, and 151 ns/op with 3 allocs per op in the parallel case, against 176 ns/op for the throttled comparison package and 368 ns/op for the tollbooth package in the same serial memory scenario. The sweep variant of the memory store is quoted at 58.3 ns/op with zero allocations. These are the author's own numbers on his own machine with his own key distribution, so treat them as a shape rather than a promise, and reproduce them on your own hardware if the difference matters to you.

The Makefile also separates ordinary tests from the race detector run, with test and test-acc targets, and the latter passes -race with a ten minute timeout. The test target runs with -count=1, -shuffle=on, -short and -vet=all, which tells you the project treats vet and shuffled test ordering as part of the definition of passing.

## Choosing between the memory store and a shared Redis store

This is the real decision in the package, and the alternative is a different architecture rather than a different library. The memory store keeps bucket state inside the process, which is the fastest option and the simplest to operate, and it is correct only while one process holds all the traffic for a given key. The Redis store moves the bucket state into Redis, with Lua scripts doing the atomic work server side, and that is the only option in the set that behaves correctly when your service runs on several machines.

The cost is real and the documentation says so directly: the Redis store comes at a performance cost. You add a network round trip and a shared dependency to every rate-limited request, and you inherit the operational burden of running Redis.

The mistake to avoid is deploying the memory store across several replicas and believing the limit is global. Each replica would enforce its own bucket, so the effective limit multiplies by the replica count. The same trap exists in a smaller form with autoscaling, where a scale event silently changes your enforcement.

## Release history, license and upgrade cost

The version history is short and the pace is irregular, which is normal for a focused utility package. v1.0.0 was released on 2024-03-25, v1.1.0 on 2025-11-17, and v1.2.0 on 2026-07-17. The last push to the repository was on 2026-10-05, so work continued after that release. The v1.x line means there is no major version migration to plan for, and the API you would write against v1.2.0 is the API the current code exposes.

The license is Apache-2.0, which permits use in commercial and closed source products under its notice and patent conditions. That is a description of what the license grants rather than legal advice, and the LICENSE file in the repository is the text that governs your use.

Upgrade cost is low by design. The package has no dependencies to upgrade, only the standard library, so there is no transitive tree to audit when you bump the version. The one thing to check is the Go toolchain, since the go.mod file requires Go 1.25 and the go directive in a dependency is enforced by the toolchain, not by this package.

## Conclusion

Adopt go-limiter when you rate limit on a key of your own choosing, such as a tenant identifier or a device id, when you want HTTP middleware without pulling in third party dependencies, and when you can pick the store that matches your deployment. Do not adopt the in-memory store for a horizontally scaled service, because the documentation states it only works on a single container or virtual machine, and plan for the Redis store in that case even though it costs performance. Before you integrate, decide whether your Go toolchain is on 1.25 as the go.mod file requires, and read the store interface in store.go, since the memory, Redis and noop implementations all satisfy it.

## FAQ

### How do I rate limit by something other than an IP address with go-limiter?

Rate limiting happens on an arbitrary string key you pass to Take, so you can use a tenant id, an API key or a device id instead of an IP. For HTTP handlers, the httplimit package lets you supply your own key function rather than using httplimit.IPKeyFunc().

### Does go-limiter work across multiple servers?

Only with the Redis store, which uses Redis plus Lua as a shared pool and comes at a performance cost. The memory store is the fastest option but only works on a single container or virtual machine since there is no way to share the state.

### Which headers does the go-limiter HTTP middleware set?

The middleware sets X-RateLimit-Limit for the configured limit, X-RateLimit-Remaining for the tokens left in the current interval, X-RateLimit-Reset for the UTC reset time, and Retry-After for when to retry. The documentation says these conform to the latest RFCs.

### Does go-limiter have any external dependencies?

No. The README states that go-limit uses only the Go standard library, on the grounds that a rate limiter should not pull in external packages. The Redis store is a separate project, sethvargo/go-redisstore.

### What does the noop store in go-limiter do?

It does no rate limiting but still implements the store interface, which the documentation says is useful for testing and local development. It lets tests exercise rate-limited code paths without a shared backend.

## Sources

- [Issues](https://github.com/sethvargo/go-limiter/issues)
- [License: Apache-2.0](https://github.com/sethvargo/go-limiter/blob/main/LICENSE)
- [README](https://github.com/sethvargo/go-limiter/blob/main/README.md)
- [Releases](https://github.com/sethvargo/go-limiter/releases)
- [sethvargo/go-limiter on GitHub](https://github.com/sethvargo/go-limiter)

---

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