-
Notifications
You must be signed in to change notification settings - Fork 0
Limits and Precedence
Three levels, resolved once per route when the route table is built:
per-endpoint → per-router → global
The most specific one that exists wins. A route with no endpoint limit on a router with no router limit gets the global one; if there is no global one either, it is unlimited.
ratelimit.WithGlobalLimit(600, time.Minute)Applies to every route on every router.
ratelimit.WithRouterLimit("partner", 100, time.Minute)
ratelimit.WithRouterLimit("internal", 10_000, time.Minute)Useful because a router usually corresponds to an audience — a partner listener and the public one rarely deserve the same budget.
type LoginRoute struct{ rxroute.Route }
func (r *LoginRoute) RateLimit() ratelimit.LimitConfig {
return ratelimit.LimitConfig{Rate: 5, Window: time.Minute}
}
app.RegisterRoute(&LoginRoute{Route: rxroute.New("POST", "/login", login)})⚠ Pointer receiver → register as a pointer, or the assertion fails and the route falls through to the router or global limit.
This is the level that matters most: a login endpoint, a password reset, a verification-code check and an expensive search each deserve a much tighter budget than the API as a whole.
For tiered limits, where the budget depends on who is calling:
func (r *SearchRoute) RateLimitByContext(ctx context.Context) ratelimit.LimitConfig {
user, ok := security.GetPrincipalAs[*User](/* request */ nil)
if !ok {
return ratelimit.LimitConfig{Rate: 10, Window: time.Minute} // anonymous
}
switch user.Tier {
case "enterprise":
return ratelimit.LimitConfig{Rate: 10_000, Window: time.Minute}
case "pro":
return ratelimit.LimitConfig{Rate: 1_000, Window: time.Minute}
default:
return ratelimit.LimitConfig{Rate: 100, Window: time.Minute}
}
}The context carries whatever upstream middleware stored — including the authenticated principal, because rate limiting runs before authentication for the refusal decision but the provider is consulted with the request's context.
RateLimitByContext takes precedence over RateLimit when a route
implements both.
The same is available globally and per router:
ratelimit.WithGlobalLimitFunc(func(ctx context.Context) ratelimit.LimitConfig {
if underLoad() {
return ratelimit.LimitConfig{Rate: 100, Window: time.Minute}
}
return ratelimit.LimitConfig{Rate: 600, Window: time.Minute}
})A dynamic provider is called on every request — keep it cheap. Reading a value out of the context is fine; a database lookup is not.
type LimitProvider interface {
Limit(ctx context.Context) LimitConfig
}
ratelimit.FixedLimit(600, time.Minute)
ratelimit.DynamicLimit(func(ctx context.Context) ratelimit.LimitConfig { … })RateLimitFactory is a
PerRouteMiddleware
factory. The framework calls it once per route, with the route and the
router it is registered on — which is exactly what the precedence chain needs.
Resolution used to happen per request, from the live URL:
provider := resolveProvider(r.Method, r.URL.Path, routerName, cfg, index)against an index keyed at registration by the route's pattern. Those never match for a parameterized route, so per-endpoint limits silently never applied to any route with a path parameter — every such route fell through to the router or global limit.
The router name was the second half of the problem: with only the route available, the extension hardcoded the default router's name, so a per-router limit configured for any other router was never consulted.
The factory is handed both. There is no index and no lookup, and the pattern/URL mismatch cannot arise because neither is consulted.
RateLimitMiddleware remains for an application composing its own chain, and is
deprecated.
A sliding window counter: requests are counted across a moving window rather than reset at a fixed boundary. A fixed window lets a client send its whole budget in the last second of one window and again in the first second of the next — twice the intended rate across that boundary.
- Authentication endpoints: single digits per minute per client. The attacker's cost is what you are raising.
- Write endpoints: tens per minute. Legitimate clients rarely burst.
- Read endpoints: hundreds. Be generous; the cost is low and a limit that fires on normal use trains clients to retry harder.
- The global limit is a backstop, not the main control. Set it well above normal aggregate traffic.
Watch X-RateLimit-Remaining in access logs before tightening anything.
rextension-ratelimit — global, per-router and per-endpoint rate limiting for Rex · MIT · © 2026 Kryovyx
Ecosystem