Rate Limiting لواجهات API: تصميم الحدود التي تحمي ولا تخنق

Rate limiting يحدّ عدد الطلبات لكل عميل في نافذة زمنية، فيحمي سيرفرك من الإساءة والتكاليف دون خنق المستخدم الشرعي. ابدأ بـ60 طلباً في الدقيقة للعام و600 للموثق، وأعد ترويسة 429 مع Retry-After.
Rate limiting هو تحديد عدد الطلبات التي يقبلها API من كل عميل خلال نافذة زمنية، وهدفه حماية سيرفرك من الإساءة والهجمات وارتفاع التكاليف دون خنق المستخدم الشرعي. القاعدة العملية للبداية: 60 طلباً في الدقيقة للطلبات العامة (غير الموثقة) و600 طلباً في الدقيقة للمستخدم الموثق، مع رفع الحد لعمليات ثقيلة معيّنة أو خفضه للنقاط الحساسة مثل تسجيل الدخول. عند تجاوز الحد يرد السيرفر برمز 429 Too Many Requests وترويسة Retry-After تخبر العميل بعدد الثواني قبل المحاولة التالية.
الخوارزميتان الأشهر: Fixed Window (نافذة ثابتة) تعدّ الطلبات في فترة محددة ثم تصفّر العدّاد، بسيطة لكنها تسمح بدفعة مضاعفة عند حافة النافذتين. وToken Bucket (دلو الرموز) تعطي العميل رصيداً من الرموز يُستهلك مع كل طلب ويتجدد بمعدل ثابت، فتسمح بدفعات قصيرة مع ضبط المعدل الوسطي، وهي الأعدل لأغلب الحالات. التنفيذ ممكن على طبقتين: Nginx كخط دفاع أول قبل وصول الطلب لتطبيقك، وthrottle في Laravel لحدود دقيقة لكل مستخدم ومسار. الأهم أن الحد يحمي تكلفة سيرفرك: كل طلب يستهلك معالجاً وذاكرة، والحد يمنع عميلاً واحداً من ابتلاع موارد الجميع. نفصّل الخوارزميات والأرقام والتنفيذ.
Fixed Window مقابل Token Bucket
فهم الفرق يحدد جودة حمايتك. لنقارن الخوارزميتين على حد 60 طلباً في الدقيقة:
| المعيار | Fixed Window | Token Bucket |
|---|---|---|
| الفكرة | عدّاد يصفّر كل دقيقة | رصيد رموز يتجدد بمعدل ثابت |
| التعامل مع الدفعات | يمنعها فجأة عند السقف | يسمح بدفعة قصيرة ضمن الرصيد |
| ثغرة الحافة | يسمح بـ120 طلباً حول لحظة التصفير | لا توجد، المعدل وسطي ثابت |
| البساطة | أبسط تنفيذاً | أعقد قليلاً |
| الأنسب لـ | حماية عامة بسيطة | API حقيقي يحتاج عدالة |
ثغرة الحافة في Fixed Window مهمة: لو أرسل العميل 60 طلباً في آخر ثانية من الدقيقة، ثم 60 في أول ثانية من التالية، مرّر 120 طلباً في ثانيتين رغم أن الحد 60 في الدقيقة. Token Bucket يغلق هذه الثغرة لأن الرصيد يتجدد تدريجياً لا دفعة واحدة، فيبقى المعدل الوسطي محكوماً. لأغلب واجهات API الجادّة، Token Bucket أو أحد اشتقاقاته (Sliding Window) هو الخيار الأعدل.
أرقام بداية عملية
الحدود ليست رقماً واحداً، بل طبقات حسب نوع الطلب وحساسيته:
- طلبات عامة غير موثقة: 60 في الدقيقة لكل عنوان IP. كافٍ للتصفّح الطبيعي، مانع للزحف العدواني.
- مستخدم موثق: 600 في الدقيقة لكل حساب. المستخدم الحقيقي نادراً ما يقترب من هذا، لكنه يمنع حساباً مخترقاً من إغراق السيرفر.
- نقاط حساسة (تسجيل دخول، استعادة كلمة مرور، OTP): 5 محاولات في الدقيقة لكل IP وحساب. هذا يوقف هجمات تخمين كلمات المرور.
- عمليات ثقيلة (تصدير، تقارير، رفع كبير): حد أقل مثل 10 في الدقيقة لأنها تستهلك موارد أكثر بكثير.
راقب أنماط عملائك الحقيقيين أسبوعاً، ثم اضبط. الحد الجيد يمرّ منه 99% من الاستخدام الشرعي بلا احتكاك، ويصطدم به المسيء فقط. ابدأ بهذه الأرقام واصعد أو انزل بناءً على البيانات لا التخمين. انتبه أيضاً إلى أن الحد الواحد الصارم على كل شيء يخنق تطبيقات شرعية تحتاج دفعات، مثل تطبيق جوال يزامن بيانات كثيرة عند فتحه، فالأفضل حد عام معقول مع رفعه على النقاط التي تتطلب دفعات مشروعة، وخفضه فقط حيث تكمن الخطورة الحقيقية كتسجيل الدخول والعمليات المكلفة.
ترويسات 429 وRetry-After
رفض الطلب ليس كافياً، لابد أن تخبر العميل بوضوح. الرد الصحيح عند تجاوز الحد هو رمز 429 Too Many Requests مع ترويسات تشرح الحالة:
- Retry-After: عدد الثواني قبل المحاولة التالية، فيعرف العميل متى يعود بدل أن يعيد المحاولة فوراً ويضاعف الحمل.
- X-RateLimit-Limit: الحد الأقصى في النافذة.
- X-RateLimit-Remaining: ما تبقّى من الرصيد.
- X-RateLimit-Reset: متى يتجدد الرصيد.
هذه الترويسات تحوّل التحديد من جدار غامض إلى تعاقد واضح: العميل الجيد يحترم Retry-After ويهدّئ معدله تلقائياً. غياب هذه الترويسات يجعل العملاء يعيدون المحاولة بعنف عند الرفض، فيتفاقم الحمل بدل أن يخفّ، والأفضل أن توثّق حدودك في صفحة مطوري الواجهة بوضوح: كم الحد لكل نوع طلب، وكيف يقرأ العميل الترويسات، وماذا يفعل عند 429. المطور الذي يعرف الحدود مسبقاً يبني تطبيقه ليحترمها من البداية، فيقلّ اصطدامه بها ويقلّ الحمل على سيرفرك، والشفافية هنا في مصلحة الطرفين معاً.
التنفيذ عبر Nginx وLaravel
الحماية على طبقتين أقوى من طبقة واحدة، فكل طبقة تلتقط ما تفلته الأخرى.
الطبقة الأولى: Nginx
Nginx يحدّ الطلبات قبل أن تصل تطبيقك أصلاً، فيحمي حتى من الطلبات التي كانت ستستهلك موارد بدء PHP. يستخدم limit_req_zone لتعريف منطقة تحديد بمعدل محدد (مثلاً 60 في الدقيقة لكل IP) مع burst يسمح بدفعة قصيرة ثم يفرض المعدل. هذه أرخص حماية لأنها توقف الإساءة عند الباب قبل أن تكلّفك معالجاً. مفيدة خصوصاً ضد الزحف العدواني وهجمات الحجب البسيطة.
الطبقة الثانية: Laravel throttle
داخل التطبيق، وسيط throttle في Laravel يعطيك تحكماً دقيقاً لكل مستخدم موثق ولكل مسار على حدة، مثل throttle:60,1 لستين طلباً في الدقيقة، أو حدود مخصصة لكل نقطة عبر RateLimiter. هذه الطبقة تفهم هوية المستخدم (لا مجرد IP)، فتفرّق بين مستخدم موثق وزائر، وتضبط النقاط الحساسة بدقة. يخزّن Laravel العدّادات في الكاش (Redis مثالي للسرعة)، فالتحديد سريع بلا حمل يُذكر.
على أي مفتاح تحدّ: IP أم مستخدم أم مفتاح API
سؤال أهم من الرقم نفسه: بأي شيء تعرّف العميل؟ لكل مفتاح استخدامه:
- عنوان IP: الوحيد المتاح للطلبات غير الموثقة. عيبه أن مستخدمين خلف شبكة واحدة (مكتب، جامعة، مشغّل جوال) يتشاركون IP فيصطدمون بالحد معاً، فاجعل حد IP سخياً نسبياً.
- هوية المستخدم: الأدق للطلبات الموثقة، فيحصل كل حساب على حدّه مستقلاً عن موقعه الشبكي.
- مفتاح API: الأنسب لواجهات المطورين، فتربط الحد بالمفتاح وتمنح مفاتيح مختلفة حدوداً مختلفة حسب الباقة.
الأفضل الجمع: حدّ IP فضفاض كخط دفاع أول ضد المجهولين، وحدّ أدق على المستخدم أو المفتاح للموثقين. هكذا لا يتسبب مسيء مجهول في حجب مستخدم شرعي، ولا يفلت حساب مخترق من المحاسبة.
تخزين العدّادات: لماذا Redis
الحد يحتاج عدّاداً يُقرأ ويُكتب مع كل طلب، فمكان تخزينه يقرّر سرعته ودقته. تخزينه في قاعدة البيانات بطيء ويضيف حملاً على كل طلب. الخيار العملي Redis، فهو في الذاكرة وسريع جداً، ويدعم عمليات ذرية (INCR مع مهلة انتهاء) تعدّ وتصفّر بدقة دون تسابق بين الطلبات المتزامنة. الأهم أن Redis يوحّد العدّاد عبر عدة عمليات أو سيرفرات: لو كان تطبيقك موزّعاً، عدّاد في ذاكرة كل عملية على حدة يسمح للعميل بضرب الحد عدة مرات، بينما Redis مركزي يفرض حداً واحداً حقيقياً على كل العملاء. لهذا يخزّن Laravel عدّادات throttle في الكاش، وضبطه على Redis يجعل التحديد سريعاً ودقيقاً معاً.
حماية تكاليف السيرفر
Rate limiting ليس أماناً فقط، بل ضبط تكلفة. كل طلب يستهلك معالجاً وذاكرة ونطاقاً، وعلى باقة بموارد محددة يمكن لعميل واحد مسيء أو سكربت خارج السيطرة أن يبتلع طاقة السيرفر كلها ويحرم بقية المستخدمين. الحد يضمن توزيعاً عادلاً للموارد ويحمي من فواتير النطاق المفاجئة، فإضافة 1TB نقل بيانات تكلّف 1,150 ريالاً سنوياً، والحد الصحيح يمنع استنزافها في زحف عبثي. على خادم سحابي تدير موارده بنفسك، rate limiting هو صمام الأمان بين استقرار الخدمة وانهيارها تحت طلب واحد جامح.
القرار العملي
- ابدأ بـ60 طلباً في الدقيقة للعام و600 للموثق و5 للنقاط الحساسة.
- استخدم Token Bucket أو Sliding Window لتفادي ثغرة حافة Fixed Window.
- ضع الطبقة الأولى في Nginx عبر limit_req_zone لإيقاف الإساءة عند الباب.
- أضف الطبقة الثانية عبر Laravel throttle لحدود دقيقة لكل مستخدم ومسار.
- أعد دائماً 429 مع Retry-After وترويسات RateLimit ليتعاون العميل.
- راقب الرفض الحقيقي أسبوعاً واضبط الأرقام على البيانات لا التخمين.
الحد الجيد لا يُحسّ به المستخدم الشرعي أبداً، ويصطدم به المسيء فقط. صمّمه من بيانات استخدامك، واحمِ به تكلفتك واستقرارك معاً.


