Implementing Rate Limiting in Laravel APIs
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
- Configure rate limiters
- Apply to routes
- Handle exceeded limits
- Build tiered limits
- 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.