Skip to content

ThrottleMiddleware

Viames Marino edited this page Mar 26, 2026 · 4 revisions

Pair framework: 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.

Constructor

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

Main method

handle(Request $request, callable $next): void

This is the main method of the class.

Current flow:

  1. resolves the best available identity key
  2. calls RateLimiter::attempt() atomically
  3. applies X-RateLimit-* headers
  4. if blocked, returns TOO_MANY_REQUESTS with HTTP 429
  5. 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.

Identity strategy

The middleware builds the key from the most stable identity available:

  1. sid query parameter
  2. bearer token
  3. authenticated user
  4. 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:42
  • throttle:ip:203.0.113.7

Integration example

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' => []]);
    });
}

Add a stricter limiter to a sensitive controller

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.

Custom ordering example

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));
}

Example 429 response

HTTP/1.1 429 Too Many Requests
Retry-After: 15
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710842892

Example client handling

// 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');
}

Secondary notes worth knowing

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

Notes and caveats

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

Clone this wiki locally