المستوى: متوسط. الشرح مبني على فرضية إنك عارف Node.js أساسي، وعندك Redis شغّال (محلي أو managed). لو لسه مبتدئ خالص، فيه مثال بسيط في الأول هيوصّلك الفكرة قبل الكود.
اعمل Rate Limiting لـ API بتاعك بـ Redis
لو مسار /login أو /send-otp عندك مفتوح من غير أي حد للطلبات، أي سكربت بسيط يقدر يضربه 5000 مرة في الدقيقة. النتيجة: فاتورة SMS بتولع، أو brute-force على الباسوردات، أو سيرفر بيقع تحت الضغط. اللي قدامك هنا بيقفل ده في حدود 40 سطر، من غير ما تغيّر الـ stack بتاعك.
المشكلة باختصار
الـ Rate Limiting معناه إنك تحط سقف لعدد الطلبات اللي عميل واحد يقدر يبعتها في فترة زمنية. مثلًا: 100 طلب في الدقيقة لكل IP. أي طلب فوق ده بيترفض بـ 429 Too Many Requests. ركز: ده مش رفاهية، ده خط الدفاع الأول قدام الإساءة وقدام الحمل المفاجئ.
قبل الكود، مثال بسيط للمبتدئ: تخيّل بوّاب في صالة أفراح بياخد سعتها 100 شخص. كل ما حد يدخل، البوّاب بيسجّل وقت دخوله في كشف. ولما حد جديد يقرب، البوّاب بيشطب من الكشف كل اللي بقالهم أكتر من ساعة جوّه، وبيعدّ الباقي. لو العدد لسه 100، بيقوله "استنى". الكشف ده، مع شطب القديم وعدّ الباقي، هو بالظبط فكرة الـ Sliding Window.
علميًا: إحنا بنخزّن طابع زمني (timestamp) لكل طلب في هيكل بيانات مرتّب. عند كل طلب جديد، بنمسح كل الطوابع الأقدم من النافذة (آخر 60 ثانية مثلًا)، بنعدّ الباقي، ولو العدد أقل من السقف بنسمح ونضيف الطابع الجديد. في Redis، الهيكل المناسب هو الـ Sorted Set، لأنه بيرتّب العناصر حسب الـ score (هنخلّيه الوقت بالملي ثانية)، وبيدّينا أوامر سريعة للمسح والعد على المدى.
ليه مش أي طريقة؟ مشكلة النافذة الثابتة
أبسط حل هو Fixed Window: عدّاد واحد لكل دقيقة، بيتصفّر مع بداية كل دقيقة جديدة. المشكلة إنه بيسمح بانفجار على الحدود. لو السقف 100/دقيقة، العميل يقدر يبعت 100 في الثانية 59، و100 كمان في الثانية 61 — يعني 200 طلب في ثانيتين، ضعف السقف. الطريقة دي بتفشل بالظبط في اللحظة اللي انت محتاج فيها الحماية.
الـ Sliding Window بيحل ده، لأنه بيحسب آخر 60 ثانية بشكل مستمر مهما كانت لحظة الطلب، فمفيش حدود يتم استغلالها.
الحل: نافذة منزلقة على Redis في 40 سطر
هنستخدم express و ioredis. السر إن العمليات الأربعة (مسح القديم، العد، الإضافة، ضبط TTL) لازم تتنفّذ كوحدة واحدة ذرّية (atomic)، عشان متحصلش race condition بين طلبين متوازيين. عشان كده بنحطها في سكربت Lua، وRedis بينفّذه كبلوك واحد.
// rateLimiter.js
const Redis = require("ioredis");
const redis = new Redis(process.env.REDIS_URL || "redis://127.0.0.1:6379");
// سكربت Lua ذرّي: يمسح القديم، يعدّ، ويضيف لو مسموح
const LUA = `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('PEXPIRE', key, window)
return limit - count - 1
end
return -1
`;
// 100 طلب كل 60 ثانية لكل مفتاح
function rateLimit({ limit = 100, windowMs = 60000 } = {}) {
return async (req, res, next) => {
const key = `rl:${req.ip}:${req.path}`;
const remaining = await redis.eval(LUA, 1, key, Date.now(), windowMs, limit);
res.set("X-RateLimit-Limit", limit);
res.set("X-RateLimit-Remaining", Math.max(remaining, 0));
if (remaining < 0) {
res.set("Retry-After", Math.ceil(windowMs / 1000));
return res.status(429).json({ error: "Too Many Requests" });
}
next();
};
}
module.exports = { rateLimit };