nginx -t قبل كده وعارف إن الـ config بيتحط في /etc/nginx/conf.d/، انت في المكان الصح. لو لسه في الأول، فيه مثال البقالة في الجزء التاني هيخلي كل حاجة واضحة.
لو الـ API بتاعك بيرجّع 502 Bad Gateway لمدة 15 دقيقة كل يوم الساعة 9 صباحاً، المشكلة مش دايماً في الـ backend. الموجة بتدخل NGINX، وNGINX بيمررها كلها للتطبيق بدون أي حد، والـ workers بتمتلئ، والمستخدمين الحقيقيين بيشوفوا 502. الحل في 10 سطور config على طبقة NGINX قبل ما الـ request يلمس تطبيقك أصلاً.
NGINX Rate Limiting: حماية الـ API بطبقة واحدة قبل التطبيق
المشكلة باختصار
NGINX بشكل افتراضي بيمرر كل request للـ backend بدون أي حد. لو واحد كتب bot ضرب على /api/login بـ 10K request/ثانية، الـ Node.js workers بتاعتك أو الـ PHP-FPM pool هيتشغّلوا في الـ requests الخبيثة دي، والمستخدمين الحقيقيين هيشوفوا 502. NGINX بيرفض الـ request في 0.3ms بدل ما تطبيقك ياخد 200ms + database connection لكل واحد.
مثال للمبتدئ: محل البقالة فيه كاشير واحد
تخيّل محل بقالة فيه كاشير واحد بيخدم 5 عملاء في الدقيقة. لو دخل المحل 50 عميل في نفس الدقيقة، الكاشير هيبطّأ، الطابور هيتخنق، والمحل هيقفل لأنه امتلأ. الحل: حارس على الباب بيقول "ادخلوا 5 في الدقيقة بس، الباقي يستنوا في طابور احتياطي بيسع 10 ناس فقط، اللي يجي بعدهم نقوله ارجع بعدين".
NGINX limit_req هو الحارس ده بالظبط:
- rate = الكاشير (مثلاً 5 طلبات/ثانية).
- burst = الطابور الاحتياطي (مثلاً 10 طلبات إضافية مؤقتة).
- الـ request اللي بعدهم بيتقطع بـ 503 أو 429.
التعريف الدقيق: خوارزمية Leaky Bucket
limit_req مبني على Leaky Bucket. كل client (مُعرَّف بـ IP افتراضياً) عنده "دلو" بحجم محدد، والـ requests بتدخل فيه. الدلو بيتسرّب بمعدل ثابت (مثلاً 10 req/sec). لو الـ request دخلت والدلو ممتلي، الـ request بترفض. الفرق بين limit_req و limit_conn: الأول بيحدّ معدل الطلبات في الزمن، التاني بيحدّ عدد الاتصالات المتزامنة. الاتنين مفيدين، لكن limit_req أهم لمنع الـ floods.
الـ config الكامل (قابل للنسخ)
# /etc/nginx/conf.d/limit.conf
http {
# دلو 10MB يحفظ ~160K IP فريد
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
# دلو منفصل للـ login (أصغر، أبطأ)
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=2r/s;
server {
listen 443 ssl http2;
server_name api.example.com;
# endpoint عادي: 10 req/sec + burst 20
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
# endpoint حساس: 2 req/sec + burst 5
location /api/login {
limit_req zone=login_limit burst=5 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}
}
إيش يعني كل سطر بالظبط؟
$binary_remote_addr: مفتاح التحديد.binary_بيوفر ذاكرة (4 bytes لـ IPv4 بدل 7).zone=api_limit:10m: اسم الدلو + حجمه. 10MB ≈ 160K IP في نفس اللحظة.rate=10r/s: المعدل المسموح به (10 طلبات/ثانية لكل IP).burst=20: الطلبات الإضافية المسموح تخزينها مؤقتاً وتنفيذها بعد كده.nodelay: ينفّذ الـ burst فوراً بدلاً من توزيعه على الزمن. بدونها NGINX هيبطّأ كل طلب في الـ burst.limit_req_status 429: يرجع 429 Too Many Requests بدل 503 الافتراضي. الـ 429 معياري ومحترم من المتصفحات والـ SDKs.
أرقام حقيقية: قبل وبعد
قياس على API Node.js Express يخدم 50K مستخدم/يوم على VM واحدة (4 vCPU, 8GB RAM):
- قبل: وقت الذروة 5000 req/sec. الـ p95 latency = 1800ms. نسبة 502 = 12%. الـ event loop lag > 400ms.
- بعد: NGINX يرفض ~60% من الطلبات بـ 429 (الـ bots بشكل أساسي). الـ p95 = 180ms. نسبة 502 = 0.3%.
- التكلفة على CPU: NGINX worker بيستخدم 8% CPU إضافية وقت الذروة. ذاكرة الزون 10MB ثابتة بغض النظر عن عدد الـ workers.
الافتراض هنا: rate limit بـ 10r/s لكل IP مناسب لـ API عام. لو تطبيقك بيخدم native mobile app بـ polling كل ثانية، 10r/s ممكن تكون قاسية. عدّل حسب الـ traffic profile الفعلي.
Trade-offs لازم تعرفها قبل ما تفعّلها
- الـ rate limit بـ IP بيظلم المستخدمين خلف NAT. المكتب اللي 200 موظف خلف IP واحد هيوصلوا للحد بسرعة. البديل: استخدم header زي
X-Forwarded-Forأو user_id من الـ session ك key. - nodelay vs delay.
nodelayبيخلي الـ burst يتنفذ فوراً (UX أحسن للمستخدمين الحقيقيين).delay=Nبيوزّع الـ burst (يحمي backend ضعيف من spikes داخلية). - الـ zone مشتركة بين كل الـ workers. 10MB في NGINX = 10MB في الذاكرة فعلياً، مش لكل worker. مش هتدفع الذاكرة مرتين.
- distributed NGINX مش مدعوم native. لو عندك 3 NGINX nodes خلف load balancer، كل واحد عنده zone منفصلة. النتيجة: المعدل الحقيقي = rate × عدد الـ nodes. للـ rate limit موزّع فعلياً، استخدم Redis token bucket في طبقة التطبيق.
اختبار الـ config قبل النشر على إنتاج
# 1. تأكد إن الـ syntax صحيح
sudo nginx -t
# 2. أعد التحميل بدون downtime
sudo nginx -s reload
# 3. اختبر بـ wrk (1000 req/sec لمدة 30 ثانية)
wrk -t4 -c100 -d30s -R1000 https://api.example.com/api/test
# 4. راقب الـ access log للـ 429 في الوقت الحقيقي
tail -f /var/log/nginx/access.log | grep ' 429 '
# 5. عدّ كم 429 خرج في آخر 100 طلب
tail -n 100 /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c
متى لا تستخدم limit_req
- الـ API بيخدم بشكل أساسي webhook callbacks من Stripe/GitHub. الـ IPs محدودة وموثوقة — اعمل allow list بدل rate limit.
- محتاج rate limit موزّع across nodes بدقة — استخدم Redis-based limiter في التطبيق (مكتبات زي
express-rate-limit+rate-limit-redis). - الحدود بتختلف حسب الـ subscription tier (free vs paid). ده منطق business، نفّذه في التطبيق مش في NGINX.
- في DDoS attack حقيقي بـ 100K IP فريد. limit_req هيستهلك الذاكرة في الزون نفسه. ضع Cloudflare أو AWS Shield قبل NGINX.
الخطوة التالية
افتح NGINX config بتاعك دلوقتي. أضف limit_req_zone واحد في الـ http block، وlimit_req في أهم endpoint عندك (غالباً /api/login أو /api/register). شغّل nginx -t ثم nginx -s reload. راقب الـ access log لساعة. لو 429 ظهرت بكثرة لمستخدمين حقيقيين، ارفع الـ rate. لو ما ظهرتش لـ bots اللي شفتهم في اللوج قبل كده، نزّل الـ rate. أعد المعايرة كل أسبوع لحد ما توصل لرقم مستقر.
المصادر
- NGINX Official Docs — ngx_http_limit_req_module: nginx.org/en/docs/http/ngx_http_limit_req_module.html
- NGINX Blog — Rate Limiting with NGINX and NGINX Plus: nginx.com/blog/rate-limiting-nginx
- RFC 6585 — Additional HTTP Status Codes (429 Too Many Requests): datatracker.ietf.org/doc/html/rfc6585
- Leaky Bucket Algorithm — Wikipedia: en.wikipedia.org/wiki/Leaky_bucket
- Cloudflare Learning — What is rate limiting?: cloudflare.com/learning/bots/what-is-rate-limiting