-
Notifications
You must be signed in to change notification settings - Fork 2
ThrottleMiddleware
Pair\Api\ThrottleMiddleware applies API rate limiting through RateLimiter.
It is the middleware you use when an endpoint must be protected from:
- burst traffic
- client retry storms
- abuse on public or semi-public routes
ApiController already registers one default throttle middleware when PAIR_API_RATE_LIMIT_ENABLED=true.
new ThrottleMiddleware(int $maxAttempts = 60, int $decaySeconds = 60)Example:
// Adds a custom limiter of 120 requests every 60 seconds.
$this->middleware(new \Pair\Api\ThrottleMiddleware(120, 60));This is the main method of the class.
Current flow:
- resolves the best available identity key
- calls
RateLimiter::attempt()atomically - applies
X-RateLimit-*headers - if blocked, returns
TOO_MANY_REQUESTSwith HTTP429 - otherwise forwards the request to the next middleware or action
Because the headers are applied before the decision is returned, clients receive rate-limit metadata on both successful and blocked requests.
The middleware builds the key from the most stable identity available:
-
sidquery parameter - bearer token
- authenticated user
- client IP (
Request::ip())
Sensitive credentials such as sid and bearer tokens are hashed before they are used in the storage key.
Examples of generated logical key shapes:
throttle:session:<sha256>throttle:bearer:<sha256>throttle:user:42throttle:ip:203.0.113.7
protected function _init(): void
{
parent::_init();
// The default throttle is already present after parent::_init().
$this->middleware(new \Pair\Api\CorsMiddleware());
}
public function usersAction(): void
{
$this->runMiddleware(function () {
// Runs only after the throttle check passes.
\Pair\Api\ApiResponse::respond(['users' => []]);
});
}protected function _init(): void
{
parent::_init();
// Adds a second, tighter limiter for sensitive actions.
$this->middleware(new \Pair\Api\ThrottleMiddleware(10, 60));
}This pattern keeps the default global throttle and adds a second stricter limit for the same request flow.
If you need CORS to run before throttling, override the default middleware registration order:
protected function registerDefaultMiddleware(): void
{
// Runs CORS first for this controller family.
$this->middleware(new \Pair\Api\CorsMiddleware());
// Runs the throttle after CORS.
$this->middleware(new \Pair\Api\ThrottleMiddleware(60, 60));
}HTTP/1.1 429 Too Many Requests
Retry-After: 15
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710842892
// Performs one request against a throttled endpoint.
const response = await fetch('/api/users?sid=SESSION_ID');
if (response.status === 429) {
// Reads the recommended retry delay in seconds.
const retryAfter = response.headers.get('Retry-After');
// Reads the reset timestamp for diagnostics.
const resetAt = response.headers.get('X-RateLimit-Reset');
}Most of the throttling behavior is delegated to RateLimiter, but these implementation details matter:
- the middleware resolves the key privately through
resolveKey(...) - successful requests still receive the rate-limit headers
- authenticated users use
user:<id>keys instead of hashed ids
- Redis is used automatically when configured.
- File storage remains the fallback for single-node or degraded mode.
-
Request::ip()is proxy-aware only when trusted proxies are configured correctly. - If you add another throttle manually, remember the default one may already be active.
See also: RateLimiter, Middleware, MiddlewarePipeline, Request, CorsMiddleware.