· Admin

Laravel'de API Rate Limiting: Basit Throttle'dan Dinamik Stratejilere

API'nizi açtınız, dökümantasyonu yazdınız, güzel. Sonra biri dakikada 10.000 request atıp sunucunuzu çökertti. Veya bir script kiddie brute force denedi. Ya da sadece bir müşterinin frontend'indeki sonsuz döngü API'nizi DDos'ladı.

Rate limiting bunların hepsine karşı ilk savunma hattı. Laravel'de bunu yapmak birkaç satır kod, ama doğru stratejiyi seçmek biraz düşünmeyi gerektiriyor.

En Basit Hali: throttle Middleware

Laravel'de rate limiting zaten var. Route'a middleware ekleyin, bitti:

Route::middleware('throttle:60,1')->group(function () {
    Route::get('/api/users', [UserController::class, 'index']);
    Route::get('/api/posts', [PostController::class, 'index']);
});

throttle:60,1 = dakikada 60 istek. Aşarsa 429 Too Many Requests döner. Client tarafında X-RateLimit-Remaining header'ından kalan hakkını görebilir.

Çoğu küçük API için bu yeterli. Ama gerçek dünyada "herkese aynı limit" her zaman mantıklı değil.

RateLimiter ile Akıllı Limitler

AppServiceProvider (veya RouteServiceProvider) boot metodunda RateLimiter tanımlayabilirsiniz. Burası işin eğlenceli kısmı.

Giriş yapmış vs anonim kullanıcı

En yaygın senaryo: giriş yapmış kullanıcılara daha yüksek limit, anonim ziyaretçilere düşük limit:

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('api', function ($request) {
    if ($request->user()) {
        return Limit::perMinute(120)->by($request->user()->id);
    }

    return Limit::perMinute(20)->by($request->ip());
});

by() parametresi önemli — limiti neye göre sayacağınızı belirler. Kullanıcı giriş yapmışsa user ID'ye göre, yapmamışsa IP'ye göre. Aynı ofisteki 50 kişi aynı IP'den geliyorsa ve IP bazlı limit koyduysanız, birbirlerinin limitini yerler — bunun farkında olun.

Plan bazlı limit (SaaS senaryosu)

API'nizi farklı planlarla satıyorsanız:

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

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

    return match ($user->plan) {
        'enterprise' => Limit::perMinute(1000)->by($user->id),
        'pro'        => Limit::perMinute(300)->by($user->id),
        default      => Limit::perMinute(60)->by($user->id),
    };
});

Endpoint bazlı farklı limitler

Bazı endpoint'ler daha maliyetlidir. Rapor oluşturma ile liste çekme aynı limiti paylaşmamalı:

// Hafif endpoint'ler
Route::get('/api/users', [UserController::class, 'index'])
    ->middleware('throttle:120,1');

// Ağır endpoint'ler
Route::post('/api/reports/generate', [ReportController::class, 'generate'])
    ->middleware('throttle:5,1');

// Login denemesi — brute force koruması
Route::post('/api/login', [AuthController::class, 'login'])
    ->middleware('throttle:5,1');

Login endpoint'ine dakikada 5 istek — bu ciddi bir koruma. Birisi brute force denerse 5 denemeden sonra 1 dakika beklemek zorunda.

Zaman dilimli limit

Mesai saatlerinde trafiğiniz yoğunsa gece daha agresif limit koyabilirsiniz:

RateLimiter::for('api', function ($request) {
    $hour = now()->hour;
    $isBusinessHours = $hour >= 9 && $hour <= 18;

    $limit = $isBusinessHours ? 200 : 50;

    return Limit::perMinute($limit)->by(
        $request->user()?->id ?: $request->ip()
    );
});

429 Response'unu Özelleştirmek

Default 429 response'u yalın. API'niz JSON dönüyorsa tutarlı bir format verin:

RateLimiter::for('api', function ($request) {
    return Limit::perMinute(60)
        ->by($request->user()?->id ?: $request->ip())
        ->response(function ($request, $headers) {
            return response()->json([
                'success' => false,
                'message' => 'Çok fazla istek gönderdiniz. Lütfen bekleyin.',
                'retry_after' => $headers['Retry-After'] ?? 60,
            ], 429);
        });
});

Cache Driver Seçimi

Rate limiting arka planda cache kullanır. Default file driver'ı ile çalışır ama production'da Redis kullanın. Sebebi:

  • File driver her request'te disk I/O yapıyor, yavaş
  • Birden fazla sunucunuz varsa (load balancer arkasında) her sunucu kendi file cache'ine bakıyor — limitler paylaşılmıyor
  • Redis atomic increment destekler, race condition olmaz
CACHE_STORE=redis

Bu kadar. Rate limiter otomatik olarak Redis'i kullanmaya başlar.

Test Etmek

Rate limiting'i test etmezseniz production'da sürprizlerle karşılaşırsınız:

public function test_rate_limit_blocks_excessive_requests(): void
{
    $user = User::factory()->create();

    // 60 istek at — hepsi geçmeli
    for ($i = 0; $i < 60; $i++) {
        $response = $this->actingAs($user)->getJson('/api/users');
        $response->assertOk();
    }

    // 61. istek — bloklanmalı
    $response = $this->actingAs($user)->getJson('/api/users');
    $response->assertStatus(429);
}

Test ortamında cache'i temizlemeyi unutmayın, yoksa testler birbirini etkiler:

protected function setUp(): void
{
    parent::setUp();
    RateLimiter::clear('api');
}

Pratikte Ne Yapıyorum

Kendi projelerimde kullandığım yaklaşım:

  1. Genel API: Dakikada 60 istek (giriş yapmış), 20 istek (anonim)
  2. Auth endpoint'leri: Dakikada 5 istek (brute force koruması)
  3. Ağır endpoint'ler (rapor, export): Dakikada 3 istek
  4. Webhook'lar: Dakikada 30 istek (dış servisler için)
  5. Admin API: Dakikada 200 istek (güvenilir kullanıcılar)

Bu limitler projeye göre değişir ama başlangıç noktası olarak işe yarıyor. Loglarınızı izleyin, gerçek kullanım paternlerine göre ayarlayın. Çok sıkı limit koyarsanız meşru kullanıcılar 429 alır, çok gevşek bırakırsanız koruma işe yaramaz. Dengeyi bulmak biraz deneyim gerektiriyor.