Today : Jan 08, 2025
Technology
07 January 2025

Implementing Effective Rate Limiting With Yii And Redis

A practical guide to using sliding window rate limiting for API protection and fair resource usage.

Effective rate limiting plays a pivotal role in safeguarding application programming interfaces (APIs) by mitigating the risks of abuse and ensuring fair access to resources. This article will explore how to implement rate limiting using the Yii framework and Redis, particularly focusing on the sliding window rate limiting technique.

Rate limiting, fundamentally, restricts the number of requests clients can make to your server in a specified timeframe. Why does this matter? Without it, APIs can easily become overwhelmed, leading to poor performance and service failures during peak usage times.

There are various ways to enforce rate limiting, but primarily we can categorize them under two main methods: fixed window rate limiting and sliding window rate limiting. The fixed window model allocates requests within fixed timeframes, which can sometimes lead to surge issues if multiple requests are sent just before the window resets. A key advantage of the sliding window is its flexibility — only keeping track of the recent requests within the designated timeframe. If all requests are made swiftly, only the allowed number according to the current window will be processed.

How does the sliding window rate limiter function? It closely monitors requests over the last defined interval — for example, if the limit is set for 10 requests per minute, clients are only allowed 10 requests during the last 60 seconds. Therefore, if they exceed their limit, subsequent requests will be denied until the window resets. This creates both fairness and stability when handling clients' requests.

The implementation can be achieved conveniently using Redis, which provides efficient data storage and retrieval via its sorted sets functionality. With Redis, timestamps of requests can be managed easily, enabling seamless rate limit enforcement.

Here is how to implement the RateLimiter in PHP:

<?php declare(strict_types=1); namespace app\modules\api\filters; use common\helpers\App; use app\Cache\Adapter\PredisAdapter; use app\modules\api\providers\Interfaces\ClientIdProviderInterface; use app\modules\api\providers\Interfaces\ControllerInfoProviderInterface; use Yii; class RateLimiter extends \CFilter { const MICROSECONDS_FACTOR = 1000000; public $enableHeaders = true; public $limit; public $time; public $whitelist = []; private $redis; private $clientIdProvider; private $controllerInfoProvider; public function __construct() { $this->redis = Yii::app()->cache->getClient(); $this->clientIdProvider = Yii::app()->container->get(ClientIdProviderInterface::class); $this->controllerInfoProvider = Yii::app()->container->get(ControllerInfoProviderInterface::class); } public function preFilter($filterChain): bool { $clientId = $this->clientIdProvider->getCurrentClientId(); $action = $this->controllerInfoProvider->getActionId($filterChain); $key = $this->getRedisKey($clientId, $action); $microtime = microtime(); list($msec, $sec) = explode(' ', $microtime); $seconds = (int)$sec; $microseconds = (int)((float)$msec  self::MICROSECONDS_FACTOR); $totalMicroseconds = (int)(($seconds  self::MICROSECONDS_FACTOR) + $microseconds); $this->redis->zremrangebyscore($key, '-inf', $totalMicroseconds - $this->time  self::MICROSECONDS_FACTOR); if (in_array($clientId, $this->whitelist)) { $this->saveRequest($key, $totalMicroseconds); return true; } $requestCount = $this->redis->zcard($key); $allowance = $requestCount + 1; if ($allowance > $this->limit) { $this->saveRequest($key, $totalMicroseconds); $this->addHeaders($this->limit, 0, $this->time); throw new \CHttpException(429, 'Too Many Requests'); } $this->saveRequest($key, $totalMicroseconds); $this->addHeaders($this->limit, $this->limit - $allowance, (int)((($this->limit - $allowance + 1)  $this->time) / $this->limit)); return true; } private function saveRequest(string $key, int $timestamp): void { $this->redis->zadd($key, [$timestamp => $timestamp]); $this->redis->expire($key, $this->time); } public function getRedisKey(int $clientId, string $action): string { return "rate_limit:{$clientId}:{$action}"; } private function addHeaders(int $limit, int $remaining, int $reset): void { if ($this->enableHeaders && !App::isTest()) { header('X-Rate-Limit-Limit: ' . $limit); header('X-Rate-Limit-Remaining: ' . $remaining); header('X-Rate-Limit-Reset: ' . $reset); } } } </code>

This simple implementation of the Rate Limiter effectively checks whether the current user's requests exceed the limit and returns relevant HTTP headers to inform the user of their current status. The entire functionality is encapsulated within well-structured methods to maintain clean and readable code.

Next, it's important to connect this filter to our controller to operate correctly:

<?php declare(strict_types=1); namespace app\controllers; use yii\web\Controller; use app\modules\api\filters\RateLimiter; class ApiController extends Controller { public function filters() { return array_merge(parent::filters(), [ [ RateLimiter::class, 'limit' => 10, 'time' => 60, 'whitelist' => Yii::app()->params['api_rate_limit_whitelist'], ], ]); } public function actionIndex() { return ['message' => 'Welcome to the API']; } } </code>

This integration ensures incoming requests are filtered effectively against the defined limits, using the sliding window technique for optimal performance!

Some potential enhancements of this system can include setting dynamic limits based on user privilege levels, implementing global limits for the API as necessary, or utilizing request data analytics for abuse detection and monitoring.

Overall, the implemented RateLimiter is efficient, scalable, and flexible, providing solid protection for API resources. How do you manage rate limiting within your applications? Share your experiences and suggestions with us!