-
Notifications
You must be signed in to change notification settings - Fork 2
RateLimiter
Pair\Api\RateLimiter is the storage engine used by ThrottleMiddleware and any custom throttling logic.
It currently uses:
- Redis as the primary backend when available
- file-based storage as an automatic fallback
Its public API is intentionally small, and attempt() is the main method you should understand first.
Redis is used when:
-
REDIS_HOSTis configured - the
ext-redisextension is available - the connection succeeds during the request
Redis mode uses Lua scripts for atomic sliding-window checks and updates.
When Redis is unavailable, the limiter stores data under:
TEMP_PATH/rate_limits/- or the system temp directory if
TEMP_PATHis not defined
Current behavior:
- one logical file per rate-limit key
-
flock()for atomic file access - sliding-window hit lists instead of the old fixed
{count, expiresAt}structure - expired entries are cleaned up opportunistically
$limiter = new \Pair\Api\RateLimiter(60, 60);Parameters:
maxAttemptsdecaySeconds
Example:
// Creates a limiter for 60 requests every 60 seconds.
$limiter = new \Pair\Api\RateLimiter(60, 60);This is the main method of the class.
It:
- checks the current sliding window
- consumes the hit only when the request is still allowed
- returns a
RateLimitResultwith the current state
Example:
$result = $limiter->attempt('throttle:user:15');
// Sends the standard rate-limit headers.
$result->applyHeaders();
if (!$result->allowed) {
// Stops the request with a normalized API error.
\Pair\Api\ApiResponse::error('TOO_MANY_REQUESTS', [
'retryAfter' => $result->retryAfter,
'resetAt' => $result->resetAt,
]);
}Prefer attempt() over composing tooManyAttempts() and hit() manually, because attempt() is the atomic and safer primitive.
Read-only check for the current state. It does not consume a hit.
This is useful for diagnostics and read-only checks, but not ideal for enforcing limits under concurrency.
Consumes a hit and returns the remaining attempts.
Current implementation detail: hit() also emits the standard rate-limit headers for backward-compatible flows.
Deletes the current key from both Redis and file fallback storage.
Typical use cases:
- reset a login-throttle bucket after a successful challenge
- clear a temporary rate-limit bucket after a workflow is completed
attempt() returns a Pair\Api\RateLimitResult with:
allowedlimitremainingresetAtretryAfterdriver
Emits:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset-
Retry-Afterwhen blocked
The driver field tells you which backend was used, usually redis or file.
$limiter = new \Pair\Api\RateLimiter(60, 60);
// Builds one logical key for this traffic class.
$key = 'throttle:bearer:' . hash('sha256', $token);
// Atomically checks and consumes the hit.
$result = $limiter->attempt($key);
// Emits the response headers before the action continues.
$result->applyHeaders();
if (!$result->allowed) {
\Pair\Api\ApiResponse::error('TOO_MANY_REQUESTS', [
'retryAfter' => $result->retryAfter,
'resetAt' => $result->resetAt,
]);
}$key = 'throttle:login:' . $request->ip();
if ($loginSuccess) {
// Clears the throttle bucket after a successful login.
$limiter->clear($key);
}// Public endpoints.
$publicLimiter = new \Pair\Api\RateLimiter(120, 60);
// Sensitive endpoints such as OTP or login checks.
$strictLimiter = new \Pair\Api\RateLimiter(10, 60);$result = $limiter->attempt('throttle:user:42');
if ($result->driver === 'redis') {
// The limit is shared across workers or nodes.
}PAIR_API_RATE_LIMIT_REDIS_PREFIX="pair:rate_limit:"
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
REDIS_TIMEOUT=1If Redis is not configured or becomes unavailable during the request, RateLimiter transparently falls back to file storage.
The class intentionally exposes only a few public methods:
-
attempt()for atomic enforce-and-consume -
tooManyAttempts()for read-only checks -
hit()for manual counting -
clear()for resetting one bucket
That small surface is deliberate and keeps most integrations straightforward.
- Reusing the same logical key for unrelated traffic classes.
- Forgetting that file fallback is local to the current node.
- Assuming client IP is trustworthy behind proxies without proper
PAIR_TRUSTED_PROXIESconfiguration. - Writing your own non-atomic
tooManyAttempts()+hit()sequence whenattempt()already solves that problem.
See also: ThrottleMiddleware, Middleware, Request, API.