Accepted
2026-05-15
The API gateway previously supported only a single global rate limit applied to all proxied requests. As the gateway evolved to serve multiple independent consumers with different rate requirements, a single global limit became insufficient. Some routes needed stricter limits to protect backend services, while others needed higher limits for trusted clients.
We introduced per-route rate limiting that supplements the existing global rate limit. The implementation uses the existing IPRateLimiter token bucket with a per-route variant: GetRouteLimiter(routeID, key, rate, burst).
Implementation:
-
Per-route limiter lookup —
Proxy()checks ifroute.RateLimit > 0and callsh.rateLimiter.GetRouteLimiter(route.ID, key, rate.Limit(route.RateLimit), burst)whereburst = route.RateLimit * 2. -
Two-phase handler initialization —
GatewayHandleris set tonilinInitHandlersand re-initialized inSetupRouterafter the global limiter is created, allowing the handler to access the limiter for per-route operations. -
Key selection — Same as global: API Key header (masked to first 5 chars) or client IP, enabling per-client tracking within each route.
-
Burst sizing — Burst is set to
rateLimit * 2to allow brief bursting while maintaining the sustained rate floor.
- Routes can have independent rate limits without affecting global gateway throughput
- Per-client tracking within routes enables fair sharing among consumers of the same route
- Existing global rate limit continues to protect gateway infrastructure
- Increased memory per route (a
map[uuid.UUID]map[string]*rate.Limiterinstead of a flat map) - Two-phase init required because
IPRateLimiteris created inSetupRouterafter handlers are initialized - Deterministic testing of rate-limit-exceeded paths requires a mock-able
RateLimiterinterface (not yet introduced)
- Rate limit applies only to proxied requests, not management API calls (create/delete routes)
- Burst calculation (
rateLimit * 2) is heuristic; production tuning may be needed
Why rejected: Requires managing lifecycle of many independent limiters. The IPRateLimiter already supports route-scoped entries via GetRouteLimiter.
Why rejected: Introduces new dependency; the existing golang.org/x/time/rate meets requirements with minimal added code.
Why rejected: Complex to reason about; one route's burst could starve another. Independent limiters per route provide cleaner semantics.