Repository navigation
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:
/**
* Replace the default throttle with one controller-specific policy.
*/
protected function registerDefaultMiddleware(): void {
$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 an explicit
ApiErrorResponsewithTOO_MANY_REQUESTSand 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 orX-Pair-Sessionheader - 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());
}
/**
* Return users after the runtime performs the throttle check once.
*/
public function usersAction(): \Pair\Http\JsonResponse {
return $this->jsonResponse(['users' => []]);
}/**
* Register one stricter throttle for this controller.
*/
protected function registerDefaultMiddleware(): void {
$this->middleware(new \Pair\Api\ThrottleMiddleware(10, 60));
}This replaces the default limiter. Built-in throttle instances share identity keys, so appending a second one consumes multiple hits from the same bucket. Independent global and route limits require custom middleware with distinct key scopes.
Pair's runtime already executes middleware before invoking the action. Do not call runMiddleware() again inside the action.
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', {
headers: { 'X-Pair-Session': sessionId }
});
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 - API sessions can be keyed from either the legacy
sidquery parameter or the saferX-Pair-Sessionheader
- 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. - Replace the default throttle when changing its policy; additional built-in instances share its storage key and count hits again.
See also: RateLimiter, AuthAttemptLimiter, Middleware, MiddlewarePipeline, Request, CorsMiddleware.
Maintained by Viames Marino — VM Ingegneria · Professional enquiries
Home · Setup · Configuration
Build with Pair 4
- Generator
- Controllers and responses
- Read models
- ActiveRecord and queries
- Forms and validation
- PairUI.js and HTML fragments
APIs and authentication
- API guide and CRUD
- Middleware and rate limits
- Users, sessions, and ACL
- Passkeys and social login
- Native mobile setup
Integrate and operate
- Integrations
- Runtime extensions and adapter keys
- Cache and cache stores
- Logs and observability
- PWA and push
- Packages and distribution
- Upgrade to v4