php
48 lines · 8 steps
Sliding-window rate limiting with Redis sorted sets
Use a Redis sorted set of timestamps to count requests in a rolling window and enforce a per-user limit.
Explained by
highlit
1<?php
2
3namespace App\RateLimiting;
4
5use Predis\Client;
6
7final class SlidingWindowRateLimiter
8{
9 private const WINDOW_SECONDS = 3600;
10
11 public function __construct(
12 private readonly Client $redis,
13 private readonly int $maxRequests = 1000,
14 ) {
15 }
16
17 public function attempt(int $userId): RateLimitResult
18 {
19 $key = sprintf('ratelimit:user:%d', $userId);
20 $now = microtime(true);
21 $windowStart = $now - self::WINDOW_SECONDS;
22 $member = sprintf('%.6f:%s', $now, bin2hex(random_bytes(6)));
23
24 $responses = $this->redis->transaction(function ($tx) use ($key, $windowStart, $member, $now) {
25 $tx->zremrangebyscore($key, '-inf', $windowStart);
26 $tx->zadd($key, [$member => $now]);
27 $tx->zcard($key);
28 $tx->expire($key, self::WINDOW_SECONDS);
29 });
30
31 $count = (int) $responses[2];
32
33 if ($count > $this->maxRequests) {
34 $this->redis->zrem($key, $member);
35 $oldest = $this->redis->zrange($key, 0, 0, ['withscores' => true]);
36 $retryAfter = self::WINDOW_SECONDS;
37
38 if (!empty($oldest)) {
39 $oldestScore = (float) reset($oldest);
40 $retryAfter = (int) ceil($oldestScore + self::WINDOW_SECONDS - $now);
41 }
42
43 return new RateLimitResult(false, 0, max($retryAfter, 1));
44 }
45
46 return new RateLimitResult(true, $this->maxRequests - $count, 0);
47 }
48}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A sorted set scored by timestamp turns rate limiting into a range-prune-then-count operation.
- 2Wrapping prune, add, count, and expire in one transaction keeps the count consistent under concurrency.
- 3Computing retry-after from the oldest surviving entry tells clients exactly when the window frees up.
Related explainers
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
php
<?php final class RotatingFileLogger {
How a rotating file logger works in PHP
logging
file-rotation
io
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/sliding-window-rate-limiting-with-redis-sorted-sets-explained-php-e6d0/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.