
GoRL is a high-performance, extensible rate limiter library for Go. It supports multiple algorithms, pluggable storage backends, a metrics collector abstraction, and minimal dependencies for both single-instance deployments and Redis-backed shared-state deployments.
Storage interface)net/http, Fiber, Gin, and Echogo get github.com/AliRizaAynaci/gorl/v2
import (
"context"
"fmt"
"time"
"github.com/AliRizaAynaci/gorl/v2"
"github.com/AliRizaAynaci/gorl/v2/core"
)
func main() {
limiter, err := gorl.New(core.Config{
Strategy: core.SlidingWindow,
Limit: 5,
Window: 1 * time.Minute,
})
if err != nil {
panic(err)
}
defer limiter.Close()
ctx := context.Background()
for i := 1; i <= 10; i++ {
res, _ := limiter.Allow(ctx, "user-123")
fmt.Printf("Request #%d: allowed=%v, remaining=%d\n", i, res.Allowed, res.Remaining)
}
}
Existing v2 usage stays exactly the same. If you want per-resource policies,
you can opt into the additive resource-scoped API:
resourceLimiter, err := gorl.NewResourceLimiter(core.ResourceConfig{
Strategy: core.SlidingWindow,
DefaultPolicy: core.ResourcePolicy{
Limit: 100,
Window: time.Minute,
},
Resources: map[string]core.ResourcePolicy{
"login": {
Limit: 5,
Window: time.Minute,
},
"search": {
Limit: 50,
Window: time.Second,
},
},
})
if err != nil {
panic(err)
}
defer resourceLimiter.Close()
res, err := resourceLimiter.AllowResource(context.Background(), "login", "user-123")
if err != nil {
panic(err)
}
fmt.Println(res.Allowed, res.Remaining)
Unknown resources use the configured DefaultPolicy, so named overrides are
optional rather than required.
Example limits.yaml:
gorl:
strategy: sliding_window
redis_url: redis://localhost:6379/0
fail_open: false
default:
limit: 100
window: 1m
resources:
login:
limit: 5
window: 1m
search:
limit: 50
window: 1s
import (
"github.com/AliRizaAynaci/gorl/v2"
"github.com/AliRizaAynaci/gorl/v2/config"
)
cfg, err := config.LoadResourceConfig("limits.yaml")
if err != nil {
panic(err)
}
resourceLimiter, err := gorl.NewResourceLimiter(cfg)
if err != nil {
panic(err)
}
defer resourceLimiter.Close()
Additional library documentation is available under docs/README.md.
Recommended entry points:
GoRL ships with a ready-to-use net/http middleware under middleware/http.
Basic Usage (handler wrapping):
import (
"net/http"
"github.com/AliRizaAynaci/gorl/v2"
"github.com/AliRizaAynaci/gorl/v2/core"
mw "github.com/AliRizaAynaci/gorl/v2/middleware/http"
)
limiter, _ := gorl.New(core.Config{
Strategy: core.SlidingWindow,
Limit: 10,
Window: 1 * time.Minute,
})
mux := http.NewServeMux()
mux.Handle("/api/", mw.RateLimit(limiter, mw.Options{
KeyFunc: mw.KeyByIP(),
}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})))
http.ListenAndServe(":8080", mux)
Middleware Chaining:
rl := mw.NewMiddleware(limiter, mw.Options{
KeyFunc: mw.KeyByHeader("X-API-Key"),
})
mux.Handle("/api/", rl(myHandler))
The middleware always sets RateLimit-Limit and RateLimit-Remaining, and
adds RateLimit-Reset and Retry-After when the limiter returns a reliable
duration.
Available Key Extractors:
- mw.KeyByIP() — client IP (supports X-Forwarded-For, X-Real-Ip)
- mw.KeyByHeader("X-API-Key") — any request header
- mw.KeyByPath() — IP + request path (per-endpoint limiting)
resourceLimiter, _ := gorl.NewResourceLimiter(core.ResourceConfig{
Strategy: core.SlidingWindow,
DefaultPolicy: core.ResourcePolicy{
Limit: 100,
Window: time.Minute,
},
Resources: map[string]core.ResourcePolicy{
"/login": {Limit: 5, Window: time.Minute},
"/search": {Limit: 50, Window: time.Second},
},
})
mux.Handle("/", mw.RateLimitByResource(resourceLimiter, mw.Options{
KeyFunc: mw.KeyByIP(),
ResourceFunc: mw.ResourceByPath(),
}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})))
import (
"github.com/gofiber/fiber/v2"
"github.com/AliRizaAynaci/gorl/v2"
"github.com/AliRizaAynaci/gorl/v2/core"
fibermw "github.com/AliRizaAynaci/gorl/v2/middleware/fiber"
)
limiter, _ := gorl.New(core.Config{
Strategy: core.FixedWindow, Limit: 100, Window: time.Minute,
})
app := fiber.New()
app.Use(fibermw.RateLimit(limiter)) // key defaults to c.IP()
app.Listen(":3000")
import (
"github.com/gin-gonic/gin"
"github.com/AliRizaAynaci/gorl/v2"
"github.com/AliRizaAynaci/gorl/v2/core"
ginmw "github.com/AliRizaAynaci/gorl/v2/middleware/gin"
)
limiter, _ := gorl.New(core.Config{
Strategy: core.SlidingWindow, Limit: 100, Window: time.Minute,
})
r := gin.Default()
r.Use(ginmw.RateLimit(limiter)) // key defaults to c.ClientIP()
r.Run(":8080")
import (
"github.com/labstack/echo/v4"
"github.com/AliRizaAynaci/gorl/v2"
"github.com/AliRizaAynaci/gorl/v2/core"
echomw "github.com/AliRizaAynaci/gorl/v2/middleware/echo"
)
limiter, _ := gorl.New(core.Config{
Strategy: core.TokenBucket, Limit: 100, Window: time.Minute,
})
e := echo.New()
e.Use(echomw.RateLimit(limiter)) // key defaults to c.RealIP()
e.Start(":8080")
All framework middlewares set
RateLimit-LimitandRateLimit-Remaining, and add duration-based headers when reliable timing data is available. Pass a customConfig{KeyFunc: ..., ResourceFunc: ...}to override the default key or resource extraction behavior.
docker run --name redis-limiter -p 6379:6379 -d redis
limiter, err := gorl.New(core.Config{
Strategy: core.TokenBucket,
Limit: 100,
Window: 1 * time.Minute,
RedisURL: "redis://localhost:6379/0",
})
if err != nil {
panic(err)
}
GoRL provides an optional metrics collector abstraction. Below is an example integrating Prometheus:
import (
"log"
"net/http"
"time"
"github.com/AliRizaAynaci/gorl/v2"
"github.com/AliRizaAynaci/gorl/v2/core"
"github.com/AliRizaAynaci/gorl/v2/metrics"
mw "github.com/AliRizaAynaci/gorl/v2/middleware/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
// Create and register Prometheus collector
pm := metrics.NewPrometheusCollector("gorl", "sliding_window")
metrics.RegisterPrometheusCollectors(pm)
// Initialize limiter with metrics enabled
limiter, err := gorl.New(core.Config{
Strategy: core.SlidingWindow,
Limit: 5,
Window: 1 * time.Minute,
RedisURL: "redis://localhost:6379/0",
Metrics: pm,
})
if err != nil {
log.Fatal(err)
}
defer limiter.Close()
// Expose Prometheus metrics endpoint
http.Handle("/metrics", promhttp.Handler())
// Application handler with rate limiting middleware
http.Handle("/api", mw.RateLimitFunc(limiter, mw.Options{
KeyFunc: mw.KeyByHeader("X-API-Key"),
}, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}))
log.Println("Listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Benchmarks below are averages of 3 runs on Apple M4 using:
go test ./internal/algorithms -run=^$ -bench=. -benchmem -benchtime=1s -count=3
Redis results were measured against a local redis:7-alpine container and
reflect the Lua-backed atomic execution path.
| Algorithm | Single Key (ns/op, B/op, allocs) | Multi Key (ns/op, B/op, allocs) |
|---|---|---|
| Fixed Window | 217.2 ns/op, 64 B/op, 4 allocs/op | 268.8 ns/op, 86 B/op, 5 allocs/op |
| Sliding Window | 394.8 ns/op, 168 B/op, 9 allocs/op | 504.2 ns/op, 182 B/op, 10 allocs/op |
| Token Bucket | 467.6 ns/op, 272 B/op, 12 allocs/op | 546.0 ns/op, 300 B/op, 13 allocs/op |
| Leaky Bucket | 474.7 ns/op, 272 B/op, 12 allocs/op | 570.0 ns/op, 286 B/op, 13 allocs/op |
| Algorithm | Single Key (ns/op, B/op, allocs) | Multi Key (ns/op, B/op, allocs) |
|---|---|---|
| Fixed Window | 100797.0 ns/op, 416 B/op, 17 allocs/op | 103031.7 ns/op, 452 B/op, 17 allocs/op |
| Sliding Window | 106871.3 ns/op, 912 B/op, 32 allocs/op | 118876.3 ns/op, 970.3 B/op, 33 allocs/op |
| Token Bucket | 107571.0 ns/op, 800 B/op, 28 allocs/op | 108520.0 ns/op, 861 B/op, 29 allocs/op |
| Leaky Bucket | 103766.0 ns/op, 800 B/op, 28 allocs/op | 111682.3 ns/op, 859 B/op, 29 allocs/op |
| Algorithm | Single Key | Multi Key |
|---|---|---|
| Fixed Window | 178022.0 -> 100797.0 ns/op 43.4% faster |
186592.7 -> 103031.7 ns/op 44.8% faster |
| Sliding Window | 457973.7 -> 106871.3 ns/op 76.7% faster |
462304.7 -> 118876.3 ns/op 74.3% faster |
| Token Bucket | 374951.7 -> 107571.0 ns/op 71.3% faster |
399340.7 -> 108520.0 ns/op 72.8% faster |
| Leaky Bucket | 386675.0 -> 103766.0 ns/op 73.2% faster |
470074.0 -> 111682.3 ns/op 76.2% faster |
These comparisons use the same benchmark command, the same local Redis container setup, and 3-run averages before and after the Lua migration.
GoRL's storage layer uses a minimal key-value interface.
package storage
import (
"context"
"time"
)
type Storage interface {
// Incr atomically increments the value at key by 1, initializing to 1 if missing or expired.
Incr(ctx context.Context, key string, ttl time.Duration) (float64, error)
// Get retrieves the numeric value at key, returning 0 if missing or expired.
Get(ctx context.Context, key string) (float64, error)
// Set stores the numeric value at key with the specified TTL.
Set(ctx context.Context, key string, val float64, ttl time.Duration) error
// Close releases any resources held by the storage backend.
Close() error
}
Lock-free implementation using sync.Map and sync/atomic:
store := inmem.NewInMemoryStore()
Scalable store leveraging Redis commands:
store := redis.NewRedisStore("redis://localhost:6379/0")
Current distributed guarantees depend on the selected algorithm.
| Backend + Strategy | Multi-instance status |
|---|---|
| In-memory + any strategy | single-process only |
| Redis + Fixed Window | supported atomic shared-state path |
| Redis + Sliding Window | supported atomic shared-state path |
| Redis + Token Bucket | supported atomic shared-state path |
| Redis + Leaky Bucket | supported atomic shared-state path |
See docs/architecture/distributed-semantics.md for the current support matrix and planned direction.
By default, gorl.New(cfg core.Config) wires up:
cfg.RedisURL is set)To add any other storage backend (JetStream, DynamoDB, etc.) without forking the repo, follow these steps:
github.com/AliRizaAynaci/gorl/v2/storage/yourmodule and implement the storage.Storage interface:```go // github.com/AliRizaAynaci/gorl/v2/storage/yourmodule/store.go package yourmodule
import ( "context" "time" "github.com/AliRizaAynaci/gorl/v2/storage" )
// YourModuleStore holds your connection fields. type Y