Implementing Rate Limiting in Laravel APIs

· 3 min read
laravel php api rate-limiting

What We’re Building

A thorough rate limiting strategy for Laravel APIs. Per-user limits, endpoint-specific throttling, and graceful handling of limit exceeded responses.

Prerequisites

  • Laravel 10+
  • Redis (recommended for distributed systems)
  • API routes configured

The Approach

  1. Configure rate limiters
  2. Apply to routes
  3. Handle exceeded limits
  4. Build tiered limits
  5. Add bypass for testing

Step 1: Basic Rate Limiter

// app/Providers/AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    RateLimiter::for('api', function ($request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}

Apply to routes:

// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
    Route::get('/users', [UserController::class, 'index']);
});

Step 2: Endpoint-Specific Limits

Different endpoints need different limits:

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

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

RateLimiter::for('auth', function ($request) {
    return Limit::perMinute(5)->by($request->ip());
});
Route::post('/upload', UploadController::class)->middleware('throttle:uploads');
Route::get('/search', SearchController::class)->middleware('throttle:search');
Route::post('/login', LoginController::class)->middleware('throttle:auth');

Step 3: Tiered Rate Limits

Different user tiers get different limits:

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

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

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

Step 4: Multiple Limits

Apply several limits simultaneously:

RateLimiter::for('api', function ($request) {
    return [
        Limit::perMinute(60)->by($request->user()->id),
        Limit::perDay(1000)->by($request->user()->id),
    ];
});

Step 5: Custom Response

RateLimiter::for('api', function ($request) {
    return Limit::perMinute(60)
        ->by($request->user()?->id ?: $request->ip())
        ->response(function ($request, $headers) {
            return response()->json([
                'error' => 'Too many requests',
                'retry_after' => $headers['Retry-After'],
                'upgrade_url' => route('pricing'),
            ], 429, $headers);
        });
});

Step 6: Handle in Exception Handler

// app/Exceptions/Handler.php
use Illuminate\Http\Exceptions\ThrottleRequestsException;

public function register(): void
{
    $this->renderable(function (ThrottleRequestsException $e, $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'error' => 'Rate limit exceeded',
                'message' => 'Please slow down. You can retry after '.$e->getHeaders()['Retry-After'].' seconds.',
                'retry_after' => (int) $e->getHeaders()['Retry-After'],
            ], 429);
        }
    });
}

Step 7: Redis for Distributed Systems

Configure Redis limiter:

// config/cache.php
'limiter' => env('LIMITER_DRIVER', 'redis'),

This ensures rate limits work across multiple servers.

Step 8: Testing Bypass

// app/Providers/AppServiceProvider.php
RateLimiter::for('api', function ($request) {
    if (app()->environment('testing')) {
        return Limit::none();
    }

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

Or use a header:

RateLimiter::for('api', function ($request) {
    if ($request->header('X-Bypass-Rate-Limit') === config('app.rate_limit_bypass_token')) {
        return Limit::none();
    }

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

Step 9: Monitor Rate Limit Usage

// app/Http/Middleware/LogRateLimitApproaching.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\RateLimiter;

class LogRateLimitApproaching
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        $remaining = $response->headers->get('X-RateLimit-Remaining');
        $limit = $response->headers->get('X-RateLimit-Limit');

        if ($remaining && $limit && ($remaining / $limit) < 0.1) {
            Log::warning('User approaching rate limit', [
                'user_id' => $request->user()?->id,
                'remaining' => $remaining,
                'limit' => $limit,
            ]);
        }

        return $response;
    }
}

The Result

  • Protection against API abuse
  • Tiered limits for different users
  • Clear error responses
  • Monitoring for approaching limits
  • Testing support

What I’d Do Differently

Communicate rate limits in onboarding and docs, not just in 429 responses. Users should know their limits before they hit them.

Rate limiting isn’t just about protection, it’s about creating a fair, predictable API experience. The limits you set communicate expectations.

Available for rescue and re-platforming work

I take over platforms that already exist and are in trouble. Node, TypeScript, React and Laravel, mostly in regulated or high-traffic environments. If something needs rescuing, re-platforming or finishing, my full history is on the CV.

Related Posts

Comments