redis_rate: GCRA rate limiting for Go, backed by a Redis Lua script
Rate limiting for go-redis
At a glance
- What is it?
- redis_rate implements the generic cell rate algorithm, also called a leaky bucket, as a Lua script that runs inside Redis, so a rate limit decision is atomic and shared across every instance of a Go service. The v10 line is a separate module path, and Redis 3.2 is the floor.
- Who is it for?
- redis_rate fits a Go service that needs a rate limit shared across horizontally scaled instances, where an in-process limiter would multiply the effective limit by the number of replicas. It is a poor fit if you need per-tenant quotas with weights that differ per key, since the algorithm here is a simple request rate, and it is a poor fit if you cannot run Lua scripts on your Redis, since the whole implementation depends on that.
- Can I use it commercially?
- Yes. BSD-2-Clause 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 18 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 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why a Lua script and not a counter with expiry
redis_rate is a rate limiting library for go-redis, and it is based on rwz/redis-gcra. The algorithm it implements is GCRA, the generic cell rate algorithm, also known as a leaky bucket.
The implementation detail that matters is where the decision happens. The package ships a lua.go file, and the rate limiting logic runs as a script inside Redis rather than as a sequence of commands from the client. That is what makes the limit correct when several instances of a service are running.
Consider what a naive implementation looks like. A client reads a counter, compares it to the limit, increments it, and writes it back. With two instances of a service behind a load balancer, that read-compare-write is three separate operations, and two requests can both read the same count and both be allowed. The limit is then not a limit at all, it is a suggestion, and the number of allowed requests scales with the number of replicas.
Running the whole decision inside Redis removes the window. The script reads the state and writes the new state as one atomic step, so every instance of every service shares one counter and the configured rate is the rate you actually get.
There is a second reason for the script, which is about replication. The package requires Redis version 3.2 or newer, and the stated reason is that it relies on the replicate_commands feature of script evaluation. In older Redis, a script that wrote to a key was not replicated to replicas as a write, so a failover to a replica could lose the rate limit state or replay it differently. With replicate_commands, the effects are propagated the way normal writes are, which keeps the state consistent across a failover.
The v10 import path is the first thing that trips people up
Installing the package is a two-step affair, and the second step has a detail that the documentation calls out specifically.
The library supports the last two Go versions and requires a Go version with modules support, so the project has to be a module first:
go mod init github.com/my/repoThen the package is added:
go get github.com/go-redis/redis_rate/v10The note attached to that command is that the v10 in the import path is required, and omitting it is a popular mistake. This is a consequence of Go's semantic import versioning, where a module with a v2 or later major version carries that version in its module path. The go.mod in the repository confirms it: the module is declared as github.com/go-redis/redis_rate/v10, and the default branch of the repository is named v10 rather than master or main, which is the convention that keeps the major version branch in sync with the module path.
The dependency set is small. The module requires go 1.19, and the direct dependencies are github.com/redis/go-redis/v9 at v9.0.2 and stretchr/testify at v1.8.1 for the tests. The indirect ones are the usual transitive set for go-redis, with cespare/xxhash for hashing and go-rendezvous for cluster support.
The pairing of v10 with go-redis v9 is worth noting because it looks like a version mismatch and is not. The major version of the rate limiter and the major version of the client library are independent, so redis_rate v10 depends on go-redis v9.
The API is one constructor and one call
The usage example is short enough to read in full, which is a good sign for a library with one job.
func ExampleNewLimiter() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
_ = rdb.FlushDB(ctx).Err()
limiter := redis_rate.NewLimiter(rdb)
res, err := limiter.Allow(ctx, "project:123", redis_rate.PerSecond(10))
if err != nil {
panic(err)
}
fmt.Println("allowed", res.Allowed, "remaining", res.Remaining)
// Output: allowed 1 remaining 9
}Three things are visible here. The limiter is constructed with a go-redis client, and nothing else, so it uses the same connection, timeouts and cluster settings you already have. The Allow method takes a context, a key, and a rate, and the key is just a string, so per-project, per-user or per-tenant limits are all the same call with a different key. And the rate is expressed with a helper, PerSecond(10), rather than as a raw number of requests per interval.
The result is a struct with at least two fields, Allowed and Remaining, and the example output shows the accounting: after one request against a limit of ten per second, one is allowed and nine remain. Having Remaining is what lets a caller reject a request early with a 429 and a Retry-After style hint, rather than discovering the limit was already exceeded.
The Allow method returns an error as well, and the example treats it as fatal. The error path matters more than it looks, because an error from Redis means the limiter could not make a decision. Whether a failed rate limit check should allow or deny the request is a policy decision the caller has to make, and the library leaves it open.
What the package does not give you
The scope is narrow, and the narrowness is the main thing to check before adopting.
The rate is a request rate. There is no documented notion of a cost, a weight, or a quota, where a heavy request consumes more of the budget than a light one. If your API has a mix of endpoints where one call is far more expensive than another, a flat request rate treats them the same, and you will have to either split the limit across several keys or normalise the cost yourself before calling the limiter.
The key is a plain string with no built-in namespacing or hierarchy, so a per-tenant and a per-user limit for the same tenant are two unrelated keys, and there is no documented mechanism to check a hierarchy of limits in one call. If you need a request to be allowed only when it passes both a per-user and a per-tenant budget, that is two Allow calls and your own logic for combining them.
The state lives in Redis and only in Redis. A flush resets the counters, which is exactly what the example does at the start with FlushDB, and a failover to a replica can lose the most recent writes.
The library also does not give you the storage for the counters, the metrics, or the policy. It gives you an atomic decision and a remaining count. Everything around that, the key naming, the response headers, the monitoring, the exemption lists, is application code.
On maintenance, the repository is not archived and the last push was on 2026-10-05. The licence is BSD-2-Clause, with a LICENSE file at the root, and the topics are gcra, leaky-bucket, rate-limiting and redis. There are no published GitHub releases on GitHub, so the version comes from the module proxy.
The test and lint targets the maintainer runs
The Makefile shows what a contributor has to satisfy, and for a small library the list is short and reveals the priorities.
all:
go test ./...
go test ./... -short -race
go test ./... -run=NONE -bench=. -benchmem
env GOOS=linux GOARCH=386 go test ./...
golangci-lint runFour test runs and a lint. The first is the plain suite. The second is the same suite in short mode with the race detector, which matters here for a specific reason: this library moves state between a Go process and a Redis server, and the race detector is what catches a client that shares a connection across goroutines without care. The third runs the benchmarks with no tests, which is the standard way to keep benchmark code compiling as part of the default target.
The fourth is the one that catches portability problems, and it is worth understanding what it is doing. It sets GOOS=linux and GOARCH=386 and runs the test suite, which is a 32-bit build. A rate limiter that indexes a timestamp or computes a time delta is exactly the kind of code where a 32-bit integer overflow or a truncation can hide, and running the tests on 386 catches that in CI rather than on a 32-bit machine years later.
The lint step is golangci-lint, and the repository carries a renovate.json, so the dependencies are updated by automation rather than by hand.
The source tree is correspondingly small: rate.go for the limiter and the result type, lua.go for the script that runs inside Redis, rate_test.go and example_test.go for the tests and the documented example, plus the go.mod, go.sum, Makefile and a README. There is no separate documentation site in this repository, the guide lives at redis.uptrace.dev, and the package is maintained under the go-redis organisation.
Editorial conclusion
redis_rate fits a Go service that needs a rate limit shared across horizontally scaled instances, where an in-process limiter would multiply the effective limit by the number of replicas. It is a poor fit if you need per-tenant quotas with weights that differ per key, since the algorithm here is a simple request rate, and it is a poor fit if you cannot run Lua scripts on your Redis, since the whole implementation depends on that. Before adopting, confirm your Redis is 3.2 or newer, get the import path right with the v10 suffix, and remember that the algorithm is a leaky bucket whose state lives in a single Redis instance.
Frequently asked questions
How do I install redis_rate in a Go project?
Initialize a Go module with go mod init github.com/my/repo, then run go get github.com/go-redis/redis_rate/v10. The v10 suffix in the import path is required and omitting it is a common mistake, because Go's semantic import versioning puts the major version in the module path.
Which rate limiting algorithm does redis_rate use?
It implements GCRA, the generic cell rate algorithm, also known as a leaky bucket, based on rwz/redis-gcra. The decision runs as a Lua script inside Redis, so the check and the state update are atomic and shared across service instances.
What Redis version does redis_rate need?
Redis 3.2 or newer, because the implementation relies on the replicate_commands feature of script evaluation so that rate limit state is replicated to replicas the way normal writes are.
How do I use the redis_rate limiter?
Create a limiter with redis_rate.NewLimiter(rdb) using your go-redis client, then call limiter.Allow(ctx, key, redis_rate.PerSecond(10)). The result reports whether the request was allowed and how much of the allowance remains, and the key is a plain string so you can limit per project, user or tenant.
Official sources
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.
[](https://hysenlabs.com/projects/go-redis-redis-rate)