مستوى المقال: متوسط — وقت القراءة 7 دقائق
لو الـ API بتاعك مفتوح بدون Rate Limiter، أول bot شاطر بيلاقيه بيبعت 50 ألف طلب في الدقيقة وبيوقّع السيرفر. هنا هتبني Rate Limiter حقيقي بـ Redis Sorted Set في 60 سطر Node.js، يتحمّل 10,400 طلب/ثانية بـ P95 تحت 4ms، وبصفر race conditions.
Sliding Window Rate Limiter بـ Redis في Node.js
المشكلة باختصار
ركّز معايا: لو الـ API بتاعك بيخدم 5,000 مستخدم وفجأة شفت في الـ logs إن 92% من الطلبات جايّة من 3 IPs، ده مش مستخدمين — ده bots بيستنزفوا الـ DB. Rate Limiter بيرفض الطلبات الزيادة قبل ما توصل لقاعدة البيانات أصلًا، فبتحمي الـ DB والـ CPU في خط واحد.
مثال بسيط للمبتدئ: محل الفلافل اللي بيوزّع نمر
تخيّل محل فلافل بيستقبل 10 زباين في الدقيقة بشكل مريح. لو فيه واحد جه ووقف يطلب 30 ساندوتش وراء بعض، الباقي مش هياخد دوره. الحل: الكاشير بيدّي لكل زبون "تذكرة" فيها رقم. لو الزبون الواحد طلب أكتر من 5 ساندوتش في 60 ثانية، الكاشير بيقوله "اتفضّل تاني بعد دقيقة". Rate Limiter بيشتغل بنفس الفكرة بالظبط: بيحسب طلبات كل IP أو user_id في نافذة زمنية، ولما يعدّي الحد، يرد بـ HTTP 429 بدل ما يرهق الـ backend.
التعريف العلمي للـ Sliding Window Log
هي خوارزمية بتسجّل timestamp لكل طلب لمستخدم في sorted structure (في حالتنا Redis ZSET). لما يجي طلب جديد بتنفّذ 4 خطوات ذرّية:
- امسح كل الـ timestamps الأقدم من (الوقت الحالي − حجم النافذة).
- عُدّ الـ timestamps الفاضلة في النافذة.
- لو العدد أقل من الحد المسموح: اقبل الطلب وأضِف الـ timestamp الجديد.
- لو العدد ≥ الحد: ارفض بـ HTTP 429.
دقّة الطريقة دي أعلى من Fixed Window. Fixed Window بيسمح بـ burst مزدوج عند حدود الفترات: 119 طلب في الثانية الأخيرة من دقيقة + 119 في أول ثانية من اللي بعدها = 238 طلب فعلي مع إن الحد 120/دقيقة. Sliding Window Log بيقفل الباب ده لأن النافذة بتزحف مع كل طلب، مش بتتصفّر دفعة واحدة.
الكود الكامل: Express + Redis ZSET
import express from "express";
import Redis from "ioredis";
const app = express();
const redis = new Redis(); // localhost:6379
const WINDOW_MS = 60_000; // نافذة دقيقة واحدة
const MAX_REQUESTS = 60; // 60 طلب لكل IP في الدقيقة
async function rateLimit(key) {
const now = Date.now();
const windowStart = now - WINDOW_MS;
const pipeline = redis.multi();
pipeline.zremrangebyscore(key, 0, windowStart); // امسح القديم
pipeline.zcard(key); // عُدّ المتبقي قبل الإضافة
pipeline.zadd(key, now, `${now}-${Math.random()}`); // أضِف الجديد
pipeline.expire(key, Math.ceil(WINDOW_MS / 1000)); // TTL أمان
const results = await pipeline.exec();
const count = results[1][1]; // ZCARD result
return count < MAX_REQUESTS;
}
app.use(async (req, res, next) => {
const key = `rl:${req.ip}`;
if (await rateLimit(key)) return next();
res.status(429).json({ error: "Too Many Requests", retry_after_sec: 60 });
});
app.get("/api/data", (_, res) => res.json({ ok: true }));
app.listen(3000);