العودة للمدونة
laravelنُشر في August 31, 2026 · 10 دقيقة قراءة

Rate Limiting في Laravel: كيف تحمي الـ API من كثرة الطلبات؟

دليل عملي لشرح Rate Limiting في Laravel: متى تحتاجه، كيف تضيفه إلى المسارات، وكيف تنشئ حدودًا مختلفة حسب المستخدم أو عنوان IP مع تخصيص استجابة 429 واختبارها.

Rate Limiting في Laravel: كيف تحمي الـ API من كثرة الطلبات؟

Rate Limiting في Laravel: كيف تحمي الـ API من كثرة الطلبات؟

الـ API عندك شغال وكل شيء تمام… لكن لو فجأة بدأ يوصله آلاف الـ requests خلال وقت قصير، إيش بيصير؟

ممكن يرتفع استهلاك السيرفر، تتباطأ الاستجابات، أو تتأثر الخدمة على كل المستخدمين بسبب مستخدم واحد أو bot يرسل طلبات بشكل مبالغ فيه.

هنا يجي دور Rate Limiting.

الفكرة ببساطة: تحدد عدد الطلبات المسموح بها خلال مدة معينة. مثلًا، كل مستخدم يقدر يرسل 60 طلبًا في الدقيقة. إذا تجاوز العدد، Laravel يوقف الطلب مؤقتًا ويرجع استجابة بالحالة 429 Too Many Requests.

في هذا المقال سنطبقها خطوة بخطوة، من أبسط استخدام إلى إعداد عملي يناسب API حقيقي.

متى تحتاج Rate Limiting؟

ليست كل المسارات تحتاج نفس الحد. غالبًا ستحتاج Rate Limiting في هذه الحالات:

  • مسارات تسجيل الدخول وإعادة تعيين كلمة المرور.
  • إرسال رمز التحقق OTP أو إعادة إرساله.
  • البحث والتقارير الثقيلة.
  • الـ APIs العامة أو المستخدمة من تطبيقات خارجية.
  • أي endpoint يستدعي خدمة مدفوعة مثل SMS أو البريد أو الذكاء الاصطناعي.
  • حماية قاعدة البيانات والسيرفر من الاستخدام الخاطئ أو الهجمات الآلية.

المهم هنا أن Rate Limiting ليس بديلًا عن authentication أو validation أو firewall. هو طبقة إضافية تساعدك تتحكم في الضغط قبل ما يتحول إلى مشكلة.

أسرع طريقة: استخدام throttle مباشرة

لو تحتاج حدًا بسيطًا لمسار معين، استخدم middleware باسم throttle:

use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;

Route::get('/products', [ProductController::class, 'index'])
    ->middleware('throttle:60,1');

القيمة 60,1 تعني:

  • السماح بـ 60 طلبًا.
  • خلال دقيقة واحدة.

ويمكنك تطبيقه على مجموعة كاملة من المسارات:

Route::middleware(['auth:sanctum', 'throttle:60,1'])
    ->group(function () {
        Route::get('/profile', [ProfileController::class, 'show']);
        Route::get('/orders', [OrderController::class, 'index']);
        Route::post('/orders', [OrderController::class, 'store']);
    });

هذه الطريقة ممتازة للبداية، لكن في المشاريع الفعلية ستحتاج غالبًا حدًا باسم واضح ومنطقًا مختلفًا للمستخدم المسجل والزائر.

إنشاء Rate Limiter مخصص

يمكنك تعريف الحدود في AppServiceProvider ثم استخدامها بالاسم في أي route.

افتح الملف:

app/Providers/AppServiceProvider.php

وأضف limiter باسم api داخل boot:

<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        RateLimiter::for('api', function (Request $request) {
            $key = $request->user()?->getAuthIdentifier()
                ?? $request->ip();

            return Limit::perMinute(60)->by($key);
        });
    }
}

بعدها اربطه بالمسارات:

Route::middleware(['auth:sanctum', 'throttle:api'])
    ->group(function () {
        Route::apiResource('orders', OrderController::class);
    });

استخدام by() مهم جدًا، لأنه يحدد الـ key الذي سيحسب Laravel الطلبات بناءً عليه:

  • إذا كان المستخدم مسجلًا، نستخدم رقم المستخدم.
  • إذا كان زائرًا، نستخدم عنوان IP.

بهذا الشكل لا يستهلك مستخدم واحد الحصة الخاصة ببقية المستخدمين.

أكثر من حد لنفس المسار

أحيانًا 60 طلبًا في الدقيقة ليست كافية وحدها. مستخدم يستطيع الالتزام بهذا الحد وإرسال عدد كبير جدًا خلال اليوم.

يمكنك الجمع بين حد قصير وحد يومي:

RateLimiter::for('api', function (Request $request) {
    $identity = $request->user()?->getAuthIdentifier()
        ?? $request->ip();

    return [
        Limit::perMinute(60)->by("minute:{$identity}"),
        Limit::perDay(1_000)->by("day:{$identity}"),
    ];
});

استخدمنا prefix مختلفًا لكل key حتى يبقى عداد الدقيقة منفصلًا عن عداد اليوم.

هذه الطريقة مفيدة عندما تريد حماية السيرفر من الارتفاع المفاجئ، وفي نفس الوقت تضع سقفًا واضحًا للاستخدام اليومي.

حد أقوى لتسجيل الدخول

مسار تسجيل الدخول حساس، ولا يُفضل وضعه تحت نفس حد باقي الـ API. خمسة محاولات في الدقيقة عادةً نقطة بداية معقولة، لكن الرقم النهائي يعتمد على طبيعة مشروعك.

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;

RateLimiter::for('login', function (Request $request) {
    $email = Str::lower((string) $request->input('email'));
    $key = hash('sha256', $email.'|'.$request->ip());

    return Limit::perMinute(5)
        ->by($key)
        ->response(function (Request $request, array $headers) {
            return response()->json([
                'message' => 'Too many login attempts. Please try again later.',
                'status' => 429,
                'data' => null,
            ], 429, $headers);
        });
});

ثم نستخدمه على مسار تسجيل الدخول فقط:

Route::post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:login');

جمعت هنا البريد مع عنوان IP حتى لا يستطيع شخص تجربة كلمات مرور كثيرة لنفس الحساب بسهولة، واستخدمت hash حتى لا يتم حفظ البريد نفسه داخل مفتاح الـ cache.

لا تجعل رسالة الخطأ تكشف هل البريد مسجل عندك أم لا. رسالة عامة أفضل من ناحية الأمان.

حدود مختلفة حسب نوع المستخدم

لو عندك خطط مجانية ومدفوعة، تستطيع إعطاء كل خطة حدًا مختلفًا:

RateLimiter::for('api', function (Request $request) {
    $user = $request->user();

    if (! $user) {
        return Limit::perMinute(20)->by($request->ip());
    }

    if ($user->isPremium()) {
        return Limit::perMinute(300)->by($user->id);
    }

    return Limit::perMinute(60)->by($user->id);
});

بهذا يصبح الحد جزءًا من منطق المنتج نفسه، وليس رقمًا ثابتًا على الجميع.

استخدام RateLimiter داخل الكود

الـ middleware مناسب للمسارات، لكن أحيانًا تريد التحكم في عملية محددة داخل Controller أو Service، مثل إرسال تقرير إلى البريد.

use Illuminate\Support\Facades\RateLimiter;

public function sendReport(Request $request)
{
    $key = 'send-report:'.$request->user()->id;

    if (RateLimiter::tooManyAttempts($key, 3)) {
        return response()->json([
            'message' => 'You have reached the report limit.',
            'retry_after' => RateLimiter::availableIn($key),
        ], 429);
    }

    RateLimiter::hit($key, 3600);

    SendReport::dispatch($request->user());

    return response()->json([
        'message' => 'The report is being prepared.',
    ]);
}

هنا سمحنا بثلاث محاولات خلال ساعة. الدالة availableIn() ترجع عدد الثواني المتبقية قبل أن يستطيع المستخدم المحاولة من جديد.

ولو نجحت عملية تريد بعدها تصفير العداد، استخدم:

RateLimiter::clear($key);

هذا مفيد في تسجيل الدخول اليدوي: تحسب المحاولات الفاشلة، ثم تمسحها بعد نجاح تسجيل الدخول.

ماذا يرجع Laravel عند تجاوز الحد؟

عندما يتجاوز العميل الحد، سيرجع Laravel:

HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0

الـ frontend أو تطبيق الجوال يجب أن يتعامل مع 429 بشكل واضح:

  • لا يعيد الطلب مباشرة في loop.
  • يقرأ Retry-After إن كان موجودًا.
  • يعرض للمستخدم رسالة بسيطة.
  • يستخدم exponential backoff في العمليات التي يعاد تنفيذها تلقائيًا.

Rate Limiting لن يفيد كثيرًا إذا كان العميل يستمر بإرسال نفس الطلب عشرات المرات بعد استلام 429.

استخدام Redis في الإنتاج

Laravel يعتمد على الـ cache لحفظ عدادات Rate Limiting. على جهاز واحد قد يعمل cache عادي، لكن إذا كان التطبيق يعمل على أكثر من server، يجب أن تشترك كل السيرفرات في نفس العداد.

هنا Redis هو الاختيار العملي غالبًا، لأنه سريع ومشترك بين نسخ التطبيق. تأكد أن إعداد limiter في config/cache.php يستخدم اتصال Redis المناسب، ولا تترك كل server يحسب الطلبات بشكل منفصل.

كذلك تستطيع توجيه middleware الافتراضي لاستخدام النسخة المحسنة لـ Redis من خلال إعداد middleware في bootstrap/app.php:

use Illuminate\Foundation\Configuration\Middleware;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->throttleWithRedis();
})

كيف تختار الرقم المناسب؟

لا يوجد رقم سحري يصلح لكل مشروع. ابدأ بسؤال بسيط: كم طلبًا يحتاج المستخدم الطبيعي لإكمال المهمة؟

مثلًا:

المسارحد بداية مقترح
تسجيل الدخول5 محاولات في الدقيقة
إعادة إرسال OTP3 محاولات في 10 دقائق
API لمستخدم مسجل60–120 طلبًا في الدقيقة
بحث ثقيل10–30 طلبًا في الدقيقة
تصدير تقرير3–5 مرات في الساعة

هذه أرقام بداية وليست قواعد ثابتة. راقب الاستخدام الحقيقي، زمن الاستجابة، ونسبة استجابات 429، ثم عدّلها.

إذا كان عدد كبير من المستخدمين الحقيقيين يصطدم بالحد، فربما الرقم منخفض. وإذا كان السيرفر يتعب قبل الوصول للحد، فإما الرقم مرتفع أو أن الـ endpoint نفسه يحتاج تحسينًا.

اختبار Rate Limiting

لا تعتمد على التجربة اليدوية فقط. أضف Feature Test يثبت أن Laravel يسمح بالعدد المطلوب ثم يرفض الطلب التالي:

public function test_login_is_rate_limited_after_five_attempts(): void
{
    $payload = [
        'email' => 'user@example.com',
        'password' => 'wrong-password',
    ];

    for ($attempt = 1; $attempt <= 5; $attempt++) {
        $this->withServerVariables(['REMOTE_ADDR' => '192.0.2.10'])
            ->postJson('/api/login', $payload)
            ->assertStatus(422);
    }

    $this->withServerVariables(['REMOTE_ADDR' => '192.0.2.10'])
        ->postJson('/api/login', $payload)
        ->assertStatus(429)
        ->assertHeader('Retry-After');
}

قد تختلف حالة محاولات تسجيل الدخول الفاشلة في مشروعك (401 أو 422 مثلًا)، لذلك عدّل assertion حسب الاستجابة الفعلية عندك.

أخطاء شائعة

وضع حد واحد على كل شيء

تسجيل الدخول ليس مثل قراءة قائمة المنتجات، وتصدير تقرير ليس مثل فتح الملف الشخصي. قسّم الحدود حسب تكلفة وحساسية كل endpoint.

الاعتماد على IP فقط للمستخدمين المسجلين

قد يشترك عشرات المستخدمين في نفس عنوان IP داخل شركة أو جامعة. استخدم رقم المستخدم بعد تسجيل الدخول، واترك IP للزوار أو اجمع الاثنين عندما تكون الحالة حساسة مثل تسجيل الدخول.

نسيان البيئة متعددة السيرفرات

إذا كان كل server يستخدم عدادًا محليًا، سيستطيع العميل تجاوز الحد بالتنقل بين السيرفرات. استخدم cache مشتركًا مثل Redis.

وضع Rate Limiting داخل Controller فقط

الـ middleware يوقف الطلب قبل الوصول إلى منطق الـ Controller، وهذا أفضل للمسارات العامة. استخدم RateLimiter داخل الكود فقط عندما يكون الحد متعلقًا بعملية محددة وليس بالمسار كاملًا.

الخلاصة

Rate Limiting من الأشياء البسيطة التي قد لا تشعر بأهميتها والمشروع صغير، لكن عند زيادة الاستخدام تصبح طبقة أساسية لحماية الـ API.

ابدأ بالمسارات الحساسة مثل login وOTP، ثم أضف حدودًا باسم واضح للـ API. قسّم الطلبات حسب المستخدم أو IP، واستخدم Redis إذا كان التطبيق يعمل على أكثر من server، والأهم: راقب النتائج ولا تتعامل مع الأرقام كأنها ثابتة للأبد.

بهذه الخطوات، لو وصلتك موجة كبيرة من الـ requests، لن يدخل كل شيء إلى التطبيق دفعة واحدة. Laravel سيوقف الزيادة ويرجع 429 بشكل منظم، بينما تظل الخدمة متاحة للمستخدمين الطبيعيين.

مصادر إضافية

#Laravel Rate Limiting #Laravel API #throttle middleware #RateLimiter #Limit perMinute #HTTP 429 #حماية API #Laravel Redis
لنتواصل

هل لديك مشروع في بالك؟

أنا متاح للعمل الحر والتعاون. لنصنع شيئاً رائعاً معاً.

النشرة البريدية

أفكار مفيدة تصل مباشرة إلى بريدك

رسائل مختصرة من حين لآخر عن Laravel وNuxt والذكاء الاصطناعي وبناء منتجات رقمية أفضل، دون إزعاج.

Logo
akramdev

قضيت أكثر من 9 سنوات في بناء تطبيقات Laravel وأنظمة الأعمال والخدمات الخلفية التي تعتمد عليها.

تواصل معي

© 2026 Akram Ghaleb · جميع الحقوق محفوظة

بُني بواسطة