Skip to content

PS-safe/ratelimit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ratelimit

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.

Status

v0Limiter 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).

Backends

Backend Status Use for
memory ✅ complete single-instance services; tests; CLI tools
redis 🚧 skeleton distributed services (multiple instances sharing state)

API

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.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.

Wiring up Redis

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:")

Demo server

go run ./cmd/server
# in another shell:
for i in $(seq 1 8); do curl -i http://localhost:8080/ping; echo; done

5 calls succeed; calls 6–8 return 429 with Retry-After.

Testing

go test ./...

Contract tests live in ratelimit_test.go and are re-runnable against any new backend.

License

MIT

About

Sliding-window rate limiter for Go — memory + Redis backends, net/http middleware

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages