Skip to content

Client Identity

wiki edited this page Sep 4, 2026 · 1 revision

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) string

The two built-ins

RemoteIPKeyFunc() — the default

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

TrustedProxyKeyFunc(prefixes)

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.

Writing your own

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.

Three rules for a key function

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.

What a key must not be

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

Clone this wiki locally