السلسلة: هذا الدرس جزء من سلسلة NestJS 11. اقرأ المقال الرئيسي.
Rate-limiting الحاجز الذي يمنع مهاجماً أو عميلاً سيئ الكود من إشباع API. NestJS يكشف @nestjs/throttler، module بسيط الإعداد، لكن افتراضياً يخزّن عدّاداته في الذاكرة — يجعله عديم الفائدة بمجرّد نشر نسختين خلف load balancer. هذا الدرس يبني rate-limiting موزَّعاً يُشارك الحالة عبر Redis، يميّز العدّادات حسب IP والمستخدم المُصادَق، يكشف headers RateLimit المعيارية (IETF draft)، ويُدير الشبكات خلف proxy.
المتطلبات
- API NestJS 11 مع مصادقة JWT
- نسخة Redis 7 أو 8
- فهم HTTP 429 وheader
Retry-After - 60 دقيقة
الخطوة 1 — إطلاق Redis محلياً
# docker-compose.dev.yml
services:
redis:
image: redis:8-alpine
ports: ["6379:6379"]
command: ["redis-server", "--appendonly", "yes"]
--appendonly yes يُفعّل persistance AOF. للـ rate-limiter فقط، الـ persistance غير حرجة، لكن حين يخدم Redis نفسه BullMQ، AOF يصير لا غنى عنه.
الخطوة 2 — تثبيت @nestjs/throttler وstorage Redis
cd apps/api
pnpm add @nestjs/throttler @nest-lab/throttler-storage-redis ioredis
client ioredis مفضّل على node-redis لأنه يدعم clusters Redis أفضل ويُعيد الاتصال آلياً.
الخطوة 3 — ضبط ThrottlerModule مع Redis
// app.module.ts
ThrottlerModule.forRootAsync({
useFactory: () => ({
throttlers: [
{ name: 'short', ttl: 1000, limit: 10 },
{ name: 'medium', ttl: 60_000, limit: 100 },
{ name: 'auth', ttl: 60_000, limit: 5 },
],
storage: new ThrottlerStorageRedisService(
new Redis({ host: 'localhost', port: 6379 })
),
}),
})
ثلاثة throttlers يغطون غالبية الحالات. short يحجب rafales قصيرة، medium يفرض معدّلاً متوسطاً، auth يحمي endpoints المصادقة.
الخطوة 4 — تفعيل الـ guard العام والاستثناءات
// app.module.ts
providers: [
{ provide: APP_GUARD, useClass: ThrottlerGuard },
],
// health/health.controller.ts
@Controller('health')
@SkipThrottle()
export class HealthController { /* ... */ }
endpoints /health/live و/health/ready يجب أن تبقى سريعة. sondes Kubernetes أو Docker قد تستجوبها كل 5 ثوان من عدة IP.
الخطوة 5 — تمييز العدّادات حسب المستخدم المُصادَق
// throttler/auth-throttler.guard.ts
@Injectable()
export class AuthThrottlerGuard extends ThrottlerGuard {
protected async getTracker(req: Record<string, any>): Promise<string> {
return req.user?.id ?? req.ip;
}
}
السلوك الافتراضي يحسب الطلبات حسب IP، صحيح لـ endpoints عامة لكن مجحف على endpoints مُصادَقة: مستخدم خلف NAT شركة يُشارك حصته مع زملائه. الـ guard المُخصَّص يحلّ 90% من الإحباطات.
الخطوة 6 — Headers RateLimit معيارية
// طلب محجوب — رد HTTP 429
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 5
RateLimit-Remaining: 0
RateLimit-Reset: 42
Retry-After: 42
Content-Type: application/json
{"statusCode":429,"message":"ThrottlerException: Too Many Requests"}
SPA جيدة الصنع تعترض 429 في middleware Axios أو Apollo Link، تقرأ Retry-After، وتعرض toast «أعد المحاولة بعد 42 ثانية».
الخطوة 7 — إدارة proxies والـ IPs الحقيقية
// main.ts
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.set('trust proxy', 1); // مستوى proxy واحد
القيمة العددية تدلّ على عدد proxies أمام API. مع Coolify وTraefik، 1. مع Cloudflare أمام Coolify، 2. وضع true يقبل أي header X-Forwarded-For يرسله العميل، ما يسمح لمهاجم بانتحال أي IP.
الخطوة 8 — اختبار تحت حمل بـ autocannon
npx autocannon -c 50 -d 10 -m POST \
-H "Content-Type=application/json" \
-b '{"email":"x@x","password":"y"}' \
http://localhost:3000/auth/login
التقرير يجب أن يُظهر أن الأغلبية الكبرى من الطلبات تُرجع 429 بعد الثواني الأولى. إن استجاب الخادم 401 أو 500 طوال المدة، rate-limiter لا يشتغل.
الخطوة 9 — استراتيجيات متقدّمة: sliding window وtoken bucket
throttler الافتراضي يستخدم fixed window: العدّاد يعود إلى الصفر عند كل فترة. هذه الاستراتيجية تعاني من burst de bord، حيث يستطيع عميل إرسال 5 طلبات في الثانية 59 و5 أخرى في الثانية 1 من الفترة التالية، أي 10 طلبات في ثانيتين.
// throttler/sliding-window.guard.ts (مقتطف منطق)
const key = 'rl:' + tracker + ':' + route;
const now = Date.now();
await redis.zremrangebyscore(key, 0, now - windowMs);
const count = await redis.zcard(key);
if (count >= limit) throw new ThrottlerException();
await redis.zadd(key, now, now + '-' + randomId());
await redis.expire(key, Math.ceil(windowMs / 1000));
sliding window يُسجّل كل طلب في sorted set Redis مع timestamp كـ score، يحذف الإدخالات الأقدم من النافذة، ثم يفحص cardinal. الدقة بمستوى المللي ثانية وburst de bord يختفي.
أخطاء شائعة
| الخطأ | السبب | الحل |
|---|---|---|
| Limit غير مُشترك بين instances | Storage افتراضي ذاكرة | ThrottlerStorageRedisService |
| كل IPs تشترك في حصة | trust proxy غير مُعد |
app.set('trust proxy', N) |
| Health checks في 429 | Endpoint غير مستثنى | @SkipThrottle على HealthController |
| عدّاد مكسور بعد reboot | Redis AOF معطّل على queue مشتركة | --appendonly yes |
| WebSocket غير محمي | Throttler افتراضي HTTP فقط | WsThrottlerGuard مخصّص لـ Socket.IO |
التعايش مع cache تطبيقي
نفس نسخة Redis تستطيع استضافة cache تطبيقي. لتجنّب تلوث البادئات، خصّص قاعدة مختلفة لكل استخدام: قاعدة 0 للـ rate-limiter، 1 للـ cache، 2 لاحقاً لـ BullMQ. هذا الانضباط يتجنّب أن يمحو FLUSHDB debug عدّادات rate-limit في الإنتاج.
أسئلة شائعة
Redis أم Memcached؟ Redis يكسب على كل المحاور في 2026: يدعم أنواع بيانات معقّدة، يخدم BullMQ والـ cache، ويوفّر persistance اختيارية.
أي حدّ لـ /auth/login؟ 5 محاولات في الدقيقة لكل IP/مستخدم تسوية جيدة. صارم جداً (1 في الدقيقة) يُحبط المستخدمين، متساهل جداً (50 في الدقيقة) يدع هجمات قاموس تمرّ.
كيف نرصد الحجب؟ أصدر log مهيكل عند كل 429 وادفعه لـ Loki. تنبيه Grafana عند 10%+ من 429 على 5 دقائق — غالباً إشارة هجوم أو bug عميل.
هل rate-limiting يستبدل WAF؟ لا. rate-limiter يحدّ التردد؛ WAF (Cloudflare، AWS WAF) يفحص المحتوى ويحجب أنماط SQL injection أو XSS. الاثنان متكاملان.