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 namespace App\Http\Controllers;
Email confirmation with signed URLs in Laravel
signed-urls
email-verification
middleware
Intermediate
6 steps
php
<?php namespace App\Models\Concerns;
How an Auditable trait logs Eloquent changes in Laravel
traits
model-events
polymorphic-relations
Intermediate
8 steps
php
<?php namespace App\Http\Controllers;
Server-Sent Events in Laravel
server-sent-events
streaming
long-polling
Advanced
9 steps
php
<?php namespace App\Actions\Imports;
Streaming CSV imports with Laravel batches
lazy-collections
job-batching
streaming
Advanced
9 steps
java
@Configuration @EnableRedisHttpSession(namespace = "myapp:sessions", maxInactiveIntervalInSeconds = 1800, flushMode = FlushMode.IMMEDIATE) public class SessionConfig {
Backing HTTP sessions with Redis in Spring
session-management
redis
distributed-state
Intermediate
7 steps
php
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Log;
Catching N+1 queries with Eloquent strict mode in Laravel
n+1 queries
strict mode
eager loading
Intermediate
7 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.