# mennanov/limiters: five rate limiting algorithms across seven backends

> The limiters library gives Go services a consistent interface over token bucket, leaky bucket, fixed window, sliding window and concurrent buffer limiters, with in-memory, Redis, Memcached, etcd, DynamoDB, Cosmos DB and other backends, plus distributed locks so the counters agree across instances. The design point is that any type of back-end and lock can be used as long as it implements a minimal interface, so the algorithm choice and the infrastructure choice are separate.

**mennanov/limiters** — Golang rate limiters for distributed applications

- Repository: https://github.com/mennanov/limiters
- Website: https://godoc.org/github.com/mennanov/limiters
- Stars: 652 · Forks: 63
- Language: Go
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/mennanov-limiters

## The same interface, five algorithms, seven backends

The library is described as rate limiters for distributed applications in Go with configurable back-ends and distributed locks, and the design rule underneath is that any types of back-ends and locks can be used that implement certain minimalistic interfaces, with the most common implementations already provided.

That sentence is the whole architecture. The algorithm and the storage are separate axes, and a project can pick a token bucket backed by Redis without the library knowing anything about Redis, or a fixed window backed by DynamoDB, and the calling code looks the same either way.

The five algorithms are described individually, and the differences between them are the differences that matter when you are choosing.

The token bucket allows requests at a certain input rate with possible bursts configured by the capacity parameter, the output rate equals the input rate, and it is precise with no over or under-limiting, but it requires a lock, which is provided.

The leaky bucket puts requests in a FIFO queue to be processed at a constant rate, with no restrictions on the input rate except the capacity of the queue, and it also requires a lock.

The fixed window counter is the simple and resource efficient algorithm that does not need a lock, where precision may be adjusted by the size of the window, and which may be lenient when there are many requests around the boundary between two adjacent windows.

The sliding window counter smoothes out those boundary bursts but needs twice the memory of the fixed window, since it keeps two windows instead of one, and it will disallow all the requests when a client floods the service, which makes handling a disallowed request the client's responsibility.

The concurrent buffer is the odd one out: it allows concurrent requests up to a given capacity and requires a lock.

The licence is MIT, the primary language is Go, the last push was on 2026-09-02, and the recent releases are v1.13.11 from 2026-06-03, v1.13.10 from 2026-03-20 and v1.13.9 from 2025-12-05.

## The backend matrix, and which algorithms work where

The backend list is where the practical constraints show up, and it is not uniform across algorithms.

Token bucket and leaky bucket both support the full set: in-memory local, Redis, Memcached, etcd, DynamoDB and Cosmos DB.

Fixed window and sliding window support in-memory, Redis, Memcached, DynamoDB and Cosmos DB, but not etcd. The repository tree shows the implementation files that match this: fixedwindow.go, slidingwindow.go, tokenbucket.go, leakybucket.go and concurrent_buffer.go at the top level, with backend-specific files for dynamodb.go and cosmosdb.go.

The concurrent buffer has the narrowest set: in-memory, Redis and Memcached only.

The reason is in the algorithm semantics rather than an omission. A windowed counter needs a place to keep a counter keyed by time window, and etcd is a key-value store with compare-and-swap semantics that suits locks and leases more naturally than it suits a bucketed counter. The concurrent buffer needs to know how many requests are in flight right now, which is a live gauge rather than a windowed tally.

One convenience is worth noting for anyone moving between vocabularies. The Android-style ABI names are irrelevant here, but the point that the library accepts aliases for architectures is not; here the relevant convenience is the same idea in a different form, with the library accepting the backend names you would use in configuration, so you do not have to translate a Redis or DynamoDB endpoint name between the library and the client SDK.

The dependencies in go.mod line up with the backends: the Redis client, the AWS SDK v2 for DynamoDB, the Azure SDK for Cosmos DB, the etcd client, the Consul API, the ZooKeeper client, the PostgreSQL driver, a memcached client and a memcached lock implementation.

## Distributed locks, and when you do not need one

The lock section explains why some of the algorithms have that requirement. Some algorithms require a distributed lock to guarantee consistency during concurrent requests, and the exception is spelled out: in case there is only one running application instance then no distributed lock is needed as all the algorithms are thread-safe, so you use LockNoop.

That is a genuinely useful distinction for adoption. A single-instance service, which is a lot of services in practice, can use the whole library with no coordination infrastructure at all. The moment you run more than one instance, the algorithms that need precise accounting need a lock.

The supported lock backends are etcd, Consul, Zookeeper, Redis, Memcached and PostgreSQL. The dependency list shows redsync for the Redis-based lock, a gomemcached-lock package, the etcd client, the Consul API, the ZooKeeper client and the PostgreSQL driver, which lines up one to one with the list.

And then there is the warning about Memcached, which is the most useful piece of documentation in the README. It is important to understand that memcached is not ideal for implementing reliable locks or data persistence due to its inherent limitations, for two reasons. There is no guaranteed data retention, because Memcached can evict data at any point due to memory pressure, even if it appears to have space available, which can lead to unexpected lock releases or data loss. And there is a lack of distributed locking features, because Memcached does not offer the functionalities like distributed coordination required for consistent locking across multiple servers.

The guidance that follows is a decision rule rather than a prohibition. If memcached exists already and it is okay to handle burst traffic caused by unexpected evicted data, Memcached-based implementations are convenient; otherwise Redis-based implementations are better choices.

## A gRPC interceptor, which is the shape most people want

The gRPC example is the most useful thing in the README for anyone deciding whether to adopt this, because it shows the whole library in about twenty lines and in the shape most services want: an interceptor.

```go
// examples/example_grpc_simple_limiter_test.go
rate := time.Second * 3
limiter := limiters.NewTokenBucket(
    2,
    rate,
    limiters.NewLockerEtcd(etcdClient, "/ratelimiter_lock/simple/", limiters.NewStdLogger()),
    limiters.NewTokenBucketRedis(
        redisClient,
        "ratelimiter/simple",
        rate, false),
    limiters.NewSystemClock(), limiters.NewStdLogger(),
)

// Add a unary interceptor middleware to rate limit all requests.
s := grpc.NewServer(grpc.UnaryInterceptor(
    func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
        w, err := limiter.Limit(ctx)
        if err == limiters.ErrLimitExhausted {
            return nil, status.Errorf(codes.ResourceExhausted, "try again later in %s", w)
        } else if err != nil {
            // The limiter failed. This error should be logged and examined.
            log.Println(err)
            return nil, status.Error(codes.Internal, "internal error")
        }
        return handler(ctx, req)
    }))
```

The constructor arguments are the whole configuration surface. The capacity is 2, the rate is three seconds, the lock is etcd-backed with a path prefix, the counter is Redis-backed with a key, and the clock and logger are injected rather than assumed. Injecting the clock is the detail that makes the library testable, since a rate limiter without a controllable clock is a rate limiter you can only test by waiting.

The error handling in the interceptor is worth copying. The exhausted case returns a ResourceExhausted status with the wait time, and any other error is logged and turned into an internal error rather than being silently treated as a pass or a rejection. The comment in the code says the limiter failure should be logged and examined, which is the right call: a limiter that fails open without telling you is a limiter that quietly stopped limiting.

The repository also points at a closer to real world example, the IP address based gRPC global rate limiter in the examples directory, which is the shape you would want for per-client rather than global limits.

## DynamoDB and Cosmos DB both need schema setup before you start

Two backends need preparation, and their requirements differ in a way that matters for provisioning.

For DynamoDB, the use requires the creation of a DynamoDB Table prior to use, and an existing table can be used or a new one can be created. The key requirements depend on the limiter backend. The partition key is a string and is required for all backends. The sort key is a string and is needed for the fixed window and sliding window backends. The TTL is a number and is needed for the fixed window, sliding window, leaky bucket and token bucket backends.

So the table schema is not one size fits all, and the TTL in particular is optional per algorithm. A token bucket on DynamoDB needs a partition key and a TTL, but no sort key, while a sliding window needs all three. That is provisioning work, and getting it wrong shows up as expired counters rather than as a startup error.

There is also a helper for the configuration. All DynamoDB backends accept a DynamoDBTableProperties struct as a parameter, which can be manually created or use LoadDynamoDBTableProperties with the table name. When using the loader, the table description is fetched from AWS and verified that the table can be used for the limiter backends, and the results of the load are cached.

For Azure Cosmos DB for NoSQL, the use requires the creation of a database and container prior to use, and there is one requirement that is easy to miss: the container must have a default TTL set, otherwise TTL will not take effect. The partition key should be /partitionKey.

The dependency versions reflect active maintenance of these two integrations. The AWS SDK v2 modules and the Azure SDK modules are recent, and the repository's own test environment pins the Cosmos emulator to a digest by hand, with a comment explaining that newer emulator builds stop honouring short document TTLs and break the TTL expiration tests with a 409 conflict.

## Testing it means running seven services, and that is the real cost

The testing section is short in the README and reveals the operational weight of the library.

```bash
make test
```

runs the tests, `make benchmark` runs the benchmarks, and `make` runs both. But neither target is self-contained, because the test target depends on docker-compose-up, which starts the backing services. The compose file brings up etcd on 2379, Redis on 6379, a Redis cluster on 11000 to 11005, Memcached on 11211, Consul on 8500, ZooKeeper on 2181, DynamoDB Local on 8000 in memory mode, PostgreSQL on 5432 and the Cosmos emulator on 8081.

The Makefile passes the endpoints to the test binary through environment variables, with the Consul address, the etcd endpoints, the Redis address, the Redis node list, the ZooKeeper endpoints, the AWS address, the Memcached address, the PostgreSQL URL and the Cosmos address. The test run itself uses the race detector and fails fast, and the benchmark target runs with a non-existent test filter so only the benchmarks execute.

The Makefile also does the formatting and linting that a Go project is expected to do, using gofumpt, goimports and golangci-lint, with the linter version pinned and installed on demand if it is missing.

For an adopter, the honest summary is that using this library means owning a set of backing services, or at least the ones your chosen algorithm needs. A team that only wants a fixed window counter on Redis can run one container. A team using the token bucket across instances needs etcd or Redis for the lock as well as the counter, and a team that wants to be able to test all backends locally needs the full set.

The go.mod tells you about the Go version floor and the pinning discipline. The module requires Go 1.25.9, and the Consul API is pinned to v1.34.1 with a comment explaining that v1.34.2 raises its own go directive to 1.26 and would force this library's minimum Go version up, with a note to unpin when the go-version block moves to 1.26. The comments are marked up as blocks that affect the CI workflow files and the dependabot configuration, so the pinning is enforced by tooling rather than by convention.

## Conclusion

Adopt mennanov/limiters if you run more than one instance of a Go service and need a rate limiter whose algorithm and storage you can choose independently, since the fixed and sliding window counters avoid a lock entirely and the token bucket gives exact accounting when you accept the coordination cost. Do not adopt it for a single-instance service that could use an in-process limiter, because the library's value is the distributed backends, and do not put it on Memcached for anything you care about, since the README is explicit that evicted entries cause unexpected lock releases. Before you commit, pick the algorithm from the burst behaviour you need rather than the one that sounds clever, provision the DynamoDB table or the Cosmos container with the keys and TTL the chosen backend requires, and read the Memcached caveat before you pick a lock backend.

## FAQ

### Which rate limiting algorithms does the mennanov/limiters library provide?

Five: token bucket, leaky bucket, fixed window counter, sliding window counter and a concurrent buffer. Token bucket and leaky bucket require a distributed lock, the fixed window counter does not, and the sliding window counter needs twice the memory of the fixed window and rejects all requests when a client floods the service.

### Which backends can the limiters library use?

In-memory local, Redis, Memcached, etcd, DynamoDB and Cosmos DB, depending on the algorithm. Token bucket and leaky bucket support the full set, the fixed and sliding window counters support in-memory, Redis, Memcached, DynamoDB and Cosmos DB, and the concurrent buffer supports in-memory, Redis and Memcached.

### Which distributed lock backends does limiters support?

etcd, Consul, Zookeeper, Redis, Memcached and PostgreSQL. If you run only one application instance you need no distributed lock at all, because the algorithms are thread-safe and you can use LockNoop. The README warns that Memcached is not ideal for reliable locks since it can evict data under memory pressure and lacks the coordination features needed across servers.

### How do I use the limiters library in a gRPC service?

Construct the limiter with a capacity, a rate, a lock, a counter backend, an injected clock and a logger, then wrap your handler in a grpc.UnaryInterceptor that calls limiter.Limit, returns a ResourceExhausted status with the wait time when the limit is exhausted, and logs and converts any other limiter error into an internal error.

### What does the DynamoDB backend require before use?

A DynamoDB table must exist. The partition key is a string required for all backends, the sort key is a string needed for the fixed window and sliding window backends, and the TTL is a number needed for the fixed window, sliding window, leaky bucket and token bucket backends. You can configure it manually or load it with LoadDynamoDBTableProperties, which fetches and verifies the table description and caches the result.

## Sources

- [License: MIT](https://github.com/mennanov/limiters/blob/master/LICENSE)
- [mennanov/limiters on GitHub](https://github.com/mennanov/limiters)
- [Project website](https://godoc.org/github.com/mennanov/limiters)
- [README](https://github.com/mennanov/limiters/blob/master/README.md)
- [Releases](https://github.com/mennanov/limiters/releases)

---

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