httprate - HTTP Rate Limiter
net/http
request rate limiter based on the Sliding Window Counter pattern inspired by
CloudFlare https://blog.cloudflare.com/counting-things-a-lot-of-different-things.
The sliding window counter pattern is accurate, smooths traffic and offers a simple counter
design to share a rate-limit among a cluster of servers. For example, if you'd like
to use redis to coordinate a rate-limit across a group of microservices you just need
to implement the httprate.LimitCounter
interface to support an atomic increment and get.
Backends
Example
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httprate"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(httprate.LimitByIP(100, time.Minute))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("."))
})
http.ListenAndServe(":3333", r)
}
Common use cases
Rate limit by IP and URL path (aka endpoint)
r.Use(httprate.Limit(
10,
10*time.Second,
httprate.WithKeyFuncs(httprate.KeyByIP, httprate.KeyByEndpoint),
))
Rate limit by arbitrary keys
r.Use(httprate.Limit(
100,
time.Minute,
httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
return r.Header.Get("X-Access-Token"), nil
}),
))
Rate limit by request payload
loginRateLimiter := httprate.NewRateLimiter(5, time.Minute)
r.Post("/login", func(w http.ResponseWriter, r *http.Request) {
var payload struct {
Username string `json:"username"`
Password string `json:"password"`
}
err := json.NewDecoder(r.Body).Decode(&payload)
if err != nil || payload.Username == "" || payload.Password == "" {
w.WriteHeader(400)
return
}
if loginRateLimiter.RespondOnLimit(w, r, payload.Username) {
return
}
w.Write([]byte("login at 5 req/min\n"))
})
Send specific response for rate-limited requests
The default response is HTTP 429
with Too Many Requests
body. You can override it with:
r.Use(httprate.Limit(
10,
time.Minute,
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, `{"error": "Rate-limited. Please, slow down."}`, http.StatusTooManyRequests)
}),
))
Send specific response on errors
An error can be returned by:
- A custom key function provided by
httprate.WithKeyFunc(customKeyFn)
- A custom backend provided by
httprateredis.WithRedisLimitCounter(customBackend)
- The default local in-memory counter is guaranteed not return any errors
- Backends that fall-back to the local in-memory counter (e.g. httprate-redis) can choose not to return any errors either
r.Use(httprate.Limit(
10,
time.Minute,
httprate.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, fmt.Sprintf(`{"error": %q}`, err), http.StatusPreconditionRequired)
}),
httprate.WithLimitCounter(customBackend),
))
r.Use(httprate.Limit(
1000,
time.Minute,
httprate.WithResponseHeaders(httprate.ResponseHeaders{
Limit: "X-RateLimit-Limit",
Remaining: "X-RateLimit-Remaining",
Reset: "X-RateLimit-Reset",
RetryAfter: "Retry-After",
Increment: "",
}),
))
r.Use(httprate.Limit(
1000,
time.Minute,
httprate.WithResponseHeaders(httprate.ResponseHeaders{}),
))
LICENSE
MIT