A small, plug-into-anything sliding-window rate limiter for Go.
import (
"github.com/PS-safe/ratelimit"
"github.com/PS-safe/ratelimit/memory"
"github.com/PS-safe/ratelimit/middleware"
)
limiter, _ := memory.New(ratelimit.Config{Limit: 100, Window: time.Minute})
mux := http.NewServeMux()
mux.HandleFunc("/api/things", thingsHandler)
http.ListenAndServe(":8080", middleware.Middleware(limiter)(mux))That's it — every request to anything behind the mux is now rate-limited per IP, with X-RateLimit-* headers and 429 + Retry-After on rejection.
v0 — Limiter interface frozen, memory backend complete and contract-tested, middleware complete, Redis backend shipped as an interface-correct skeleton (the Lua script is written; plug in your Redis client).
| Backend | Status | Use for |
|---|---|---|
memory |
✅ complete | single-instance services; tests; CLI tools |
redis |
🚧 skeleton | distributed services (multiple instances sharing state) |
type Limiter interface {
Allow(ctx context.Context, key string) (Result, error)
}
type Result struct {
Allowed bool
Limit int
Remaining int
RetryAfter time.Duration
ResetAt time.Time
}Allow checks the sliding window and atomically records the request if the key is under the limit.
middleware.Middleware(limiter,
middleware.WithKey(middleware.IPKey), // default
middleware.WithRejectedHandler(myCustomHandler), // optional
middleware.WithoutHeaders(), // optional: skip X-RateLimit-*
)IPKey honors X-Forwarded-For first then falls back to RemoteAddr. Trust XFF only when you're behind a proxy that strips inbound headers.
The Redis package ships the Lua script and the algorithm. To make it run, satisfy the Client interface with a tiny shim around your Redis driver of choice:
import "github.com/redis/go-redis/v9"
type goRedisAdapter struct{ rdb *redis.Client }
func (a goRedisAdapter) Eval(ctx context.Context, script string, keys []string, args ...any) ([]any, error) {
return a.rdb.Eval(ctx, script, keys, args...).Slice()
}Then:
client := goRedisAdapter{rdb: rdb}
limiter, _ := ratelimitredis.New(client, ratelimit.Config{Limit: 100, Window: time.Minute}, "myapp:")go run ./cmd/server
# in another shell:
for i in $(seq 1 8); do curl -i http://localhost:8080/ping; echo; done5 calls succeed; calls 6–8 return 429 with Retry-After.
go test ./...Contract tests live in ratelimit_test.go and are re-runnable against any new backend.
MIT