-
Notifications
You must be signed in to change notification settings - Fork 0
Client Identity
A bucket is keyed by scope + client identity. The scope comes from the
precedence chain; the identity comes from a KeyFunc.
type KeyFunc func(r *http.Request) stringThe peer address from RemoteAddr, ignoring forwarding headers.
Correct only when nothing sits in front of the application. Behind any
ingress — a load balancer, a reverse proxy, a CDN — RemoteAddr is the
proxy's address, identical for every client, so this turns a per-client
limiter into a single global one.
That failure is silent and looks like working code: the limiter fires, just for everyone at once.
Resolves the client address through X-Forwarded-For, trusting only the given
networks. Selected automatically by WithTrustedProxies. See
Behind a Proxy — the direction of the walk matters, and it is
not the obvious one.
A KeyFunc returns the string a bucket is keyed by. Anything derived from the
request works:
// Per API key, falling back to address.
ratelimit.WithKeyFunc(func(r *http.Request) string {
if key := r.Header.Get("X-API-Key"); key != "" {
return "key:" + hash(key)
}
return remoteIP(r)
})// Per authenticated user, falling back to address for anonymous callers.
ratelimit.WithKeyFunc(func(r *http.Request) string {
if u, ok := security.GetPrincipalAs[*User](r); ok {
return "user:" + u.ID
}
return remoteIP(r)
})Note the second one only works if authentication has already run — and rate limiting is at priority 300, before auth at 400. So the principal is not in the context yet. If you want per-user limits, use a dynamic route limit for the budget and keep the key address-based, or move the key derivation to something present on the raw request, like an API key header.
Hash anything secret. The key is held in memory in the bucket store; a raw API key or session identifier does not belong there.
func hash(s string) string {
sum := sha256.Sum256([]byte(s))
return hex.EncodeToString(sum[:8])
}Prefix by kind. "key:abc" and "ip:1.2.3.4" cannot collide; "abc" and
"1.2.3.4" in the same namespace can, in principle, and the collision is
attacker-selectable if either value is.
Keep the cardinality bounded — or accept the cap. The store is keyed partly
by whatever you return, so a key derived from something a client controls is
bounded by the client. The MaxBuckets cap exists exactly because
this cannot be guaranteed, but a key function with a bounded range is better
than relying on the cap.
- A raw path or full URL. Unbounded, client-controlled, and it makes the limiter per-URL rather than per-client.
- A user agent. Trivially rotated.
- Nothing at all (a constant). That is a global concurrency limit, not a rate limit, and there are better ways to build one.
rextension-ratelimit — global, per-router and per-endpoint rate limiting for Rex · MIT · © 2026 Kryovyx
Ecosystem