Setting up Laravel rate limits
app/Providers/AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
RateLimiter::for('api', function ($request) {
$key = $request->header('X-Api-Key') ?? $request->ip();
return Limit::perMinute(100)->by($key);
});
RateLimiter::for('login', function ($request) {
return [
// 5 per minute per IP
Limit::perMinute(5)->by($request->ip()),
// 20 per minute per email (prevents targeted stuffing)
Limit::perMinute(20)->by($request->input('email')),
];
});
}routes/api.php
Route::middleware('throttle:api')->group(function () {
Route::get('/users', [UserController::class, 'index']);
Route::post('/orders', [OrderController::class, 'store']);
});
Route::post('/login', LoginController::class)
->middleware('throttle:login');Track 429s in your APM
429 responses show up in your request telemetry. The value is per-route grouping:
- /api/* 429 spike → a consumer's retry logic is broken, or an API key is compromised
- /login 429 spike → credential stuffing attack
- /password/reset 429 spike → someone farming password reset emails
- /api/webhooks/* 429 spike → your upstream is retrying because processing is slow
Log rate-limit hits with context
Custom middleware that logs 429s
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class LogRateLimitHits
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
if ($response->status() === 429) {
Log::warning('Rate limit hit', [
'route' => $request->route()?->uri(),
'method' => $request->method(),
'ip' => $request->ip(),
'api_key' => $request->header('X-Api-Key'),
'user_id' => auth()->id(),
'limit_after' => $response->headers->get('X-RateLimit-Remaining'),
]);
}
return $response;
}
}Distributed rate limiting with Redis
On a multi-server fleet, Laravel's default cache-store rate limiter doesn't sync across servers — each server has its own counter:
.env
CACHE_STORE=redis
REDIS_HOST=your-redis-host
REDIS_PORT=6379
# Optional: use a dedicated connection for rate limits
# so they're not evicted by general cache pressure:
# CACHE_RATE_LIMITER_CONNECTION=ratelimitWith Redis, increment+check operations are atomic and global. 100 req/min on a 3-server fleet means 100 total, not 300.
Alerting thresholds
| Route pattern | Alert at | Likely cause |
|---|---|---|
| /login | 10+ 429s/min | Credential stuffing |
| /api/* | 5x baseline sustained 10min | Runaway client retries |
| /password/* | 3+ 429s/min | Password-reset farming |
| /api/webhooks/* | any sustained | Upstream retry storm from slow processing |
THE EASY WAY
NightOwl surfaces 429s as part of request monitoring
Filter the requests dashboard by status=429 per route. Drill into specific rate-limited requests to see which IP, API key, or user hit the ceiling. Set alerts on 429 rate thresholds via any configured alert channel.
composer require nightowl/agent
php artisan nightowl:install