The Sliding Window Log algorithm maintains a log of timestamps for all requests within the time window. It provides the most accurate rate limiting by tracking exact request times, eliminating boundary issues completely.
- Maintain a log (queue) of all request timestamps
- For each new request, remove timestamps older than the window
- If log size < limit, allow the request and add timestamp
- Otherwise, reject the request
Window: 60 seconds
Current time: 100s
Request Log:
[45s, 52s, 61s, 75s, 83s, 92s, 98s]
↑
Remove (older than 40s)
Final Log:
[52s, 61s, 75s, 83s, 92s, 98s] ← 6 requests in window
class SlidingWindowLog:
window_size: int
limit: int
requests: List[timestamp]
function allow_request():
now = current_time()
cutoff = now - window_size
# Remove old requests
while requests.length > 0 AND requests[0] <= cutoff:
requests.remove_first()
if requests.length < limit:
requests.append(now)
return True
else:
return False
export class SlidingWindowLogLimiter {
private windowSize: number;
private limit: number;
private requests: number[];
constructor(windowSize: number, limit: number) {
this.windowSize = windowSize * 1000;
this.limit = limit;
this.requests = [];
}
private removeOldRequests(): void {
const now = Date.now();
const cutoff = now - this.windowSize;
while (this.requests.length > 0 && this.requests[0] <= cutoff) {
this.requests.shift();
}
}
allowRequest(): RateLimitResult {
this.removeOldRequests();
const now = Date.now();
if (this.requests.length < this.limit) {
this.requests.push(now);
return {
allowed: true,
limit: this.limit,
remaining: this.limit - this.requests.length,
resetAt: this.requests[0] + this.windowSize
};
}
const oldestRequest = this.requests[0];
const retryAfter = (oldestRequest + this.windowSize - now) / 1000;
return {
allowed: false,
limit: this.limit,
remaining: 0,
resetAt: oldestRequest + this.windowSize,
retryAfter: Math.max(0, retryAfter)
};
}
}async function slidingWindowLogRedis(
redis: RedisClient,
key: string,
limit: number,
windowSize: number
): Promise<boolean> {
const now = Date.now();
const cutoff = now - (windowSize * 1000);
const pipe = redis.pipeline();
// Remove old entries
pipe.zremrangebyscore(key, 0, cutoff);
// Count current entries
pipe.zcard(key);
const results = await pipe.exec();
const count = results[1][1];
if (count < limit) {
// Add new entry
await redis.zadd(key, now, `${now}:${Math.random()}`);
await redis.expire(key, windowSize * 2);
return true;
}
return false;
}- 100% accurate
- No boundary issues
- True sliding window
- Fair to all requests
- High memory usage (O(N) per identifier)
- Slower than counter-based algorithms
- Not suitable for high traffic
10,000 req/min × 8 bytes = 80 KB per user
1M users = 80 GB!
Best for:
- Financial systems
- Compliance-heavy applications
- Low-medium traffic APIs
- When perfect accuracy required
- Time: O(N) for cleanup
- Space: O(limit) per identifier
- Practical limit: ~1000 req/window per user