marko/rate-limiting
Cache-backed rate limiter with route middleware --- throttle requests by IP with configurable limits and automatic Retry-After headers. Rate limiting uses the cache layer to track request attempts per key (typically client IP). When limits are exceeded, the middleware returns a 429 response with a Retry-After header. Responses include X-RateLimit-Limit and X-RateLimit-Remaining headers so clients can self-throttle.
Installation
Section titled “Installation”composer require marko/rate-limitingRequires marko/cache for the storage backend and marko/routing for the middleware.
Route Middleware
Section titled “Route Middleware”Apply RateLimitMiddleware to routes that need throttling:
use Marko\Routing\Attributes\Get;use Marko\Routing\Attributes\Middleware;use Marko\Routing\Http\Response;use Marko\RateLimiting\Middleware\RateLimitMiddleware;
class ApiController{ #[Get('/api/data')] #[Middleware(RateLimitMiddleware::class)] public function index(): Response { return new Response('OK'); }}The middleware defaults to 60 requests per 60 seconds. It resolves the client IP from the X-Forwarded-For header, falling back to Remote-Addr. Configure limits via constructor injection:
use Marko\RateLimiting\Contracts\RateLimiterInterface;use Marko\RateLimiting\Middleware\RateLimitMiddleware;
$middleware = new RateLimitMiddleware( limiter: $rateLimiter, maxAttempts: 100, decaySeconds: 120,);Using the Rate Limiter Directly
Section titled “Using the Rate Limiter Directly”For custom throttling logic, inject RateLimiterInterface:
use Marko\RateLimiting\Contracts\RateLimiterInterface;
public function __construct( private readonly RateLimiterInterface $rateLimiter,) {}
public function processLogin( string $email,): void { $result = $this->rateLimiter->attempt( "login:$email", 5, 300, );
if (!$result->allowed()) { // Too many attempts, retry after $result->retryAfter() seconds }}Checking Without Incrementing
Section titled “Checking Without Incrementing”Check if a key is rate-limited without consuming an attempt:
if ($this->rateLimiter->tooManyAttempts('api:' . $ip, 60)) { // Already rate-limited}Clearing Rate Limits
Section titled “Clearing Rate Limits”Reset the counter for a key --- for example, after a successful login:
$this->rateLimiter->clear("login:$email");Customization
Section titled “Customization”Replace RateLimiter via Preferences to change the keying strategy or storage:
use Marko\Core\Attributes\Preference;use Marko\RateLimiting\RateLimiter;use Marko\RateLimiting\RateLimitResult;
#[Preference(replaces: RateLimiter::class)]class SlidingWindowRateLimiter extends RateLimiter{ public function attempt( string $key, int $maxAttempts, int $decaySeconds, ): RateLimitResult { // Custom sliding window logic }}API Reference
Section titled “API Reference”RateLimiterInterface
Section titled “RateLimiterInterface”public function attempt(string $key, int $maxAttempts, int $decaySeconds): RateLimitResult;public function tooManyAttempts(string $key, int $maxAttempts): bool;public function clear(string $key): void;RateLimitResult
Section titled “RateLimitResult”public function allowed(): bool;public function remaining(): int;public function retryAfter(): ?int;RateLimitMiddleware
Section titled “RateLimitMiddleware”public function handle(Request $request, callable $next): Response;Constructor parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
$limiter | RateLimiterInterface | --- | The rate limiter instance |
$maxAttempts | int | 60 | Maximum requests allowed in the window |
$decaySeconds | int | 60 | Time window in seconds before attempts reset |