What Is Rate Limiting?
A technique that controls the number of requests a client can make to a server within a specified time period. Rate limiting protects against brute-force attacks, denial of service, API abuse, and web scraping by rejecting requests that exceed the defined threshold.
How It Works
Rate limiting enforces a maximum request frequency per client by tracking counters in a fast cache store and rejecting requests that exceed the threshold.
Step 1: Client identification — When a request arrives, the throttle middleware generates a cache key identifying the rate limit bucket. The default key combines the client's IP address and a hash of the route path: throttle_yourapp_192.168.1.1_login. Custom rate limiters (defined with RateLimiter::for()) can use different identifiers: the authenticated user's ID, their organization, or a combination of IP and route.
Step 2: Counter increment — The middleware calls RateLimiter::tooManyAttempts($key, $maxAttempts). Internally, this increments an atomic counter in the cache store (Redis in production) for the identified key. If the key does not exist, it is created with the counter set to 1 and an expiry equal to the decay window (1 minute for throttle:5,1).
Step 3: Threshold check — If the incremented counter exceeds $maxAttempts, the request is rejected immediately with HTTP 429 Too Many Requests. The response includes Retry-After: 60 and X-RateLimit-Limit: 5, X-RateLimit-Remaining: 0 headers that inform the client and any programmatic retry logic.
Step 4: Counter expiry — Redis automatically expires the key after the decay window, resetting the count. In the fixed window model (Laravel's default), the window resets at a fixed point: a client who makes 4 requests at 11:59:59 and 1 request at 12:00:00 effectively makes 5 requests without triggering the 5-request limit. The sliding window algorithm (RateLimiter stores individual timestamps) is more accurate but memory-intensive.
Algorithm comparison — Fixed window: simple, low memory, vulnerable to boundary bursts. Sliding window log: accurate, high memory. Token bucket: allows burst up to bucket capacity, refills at fixed rate. Leaky bucket: processes at constant rate, queues excess requests.
Types of Rate Limiting
Fixed Window Algorithm
Counts requests per fixed time window (e.g., per minute). Simple and efficient but vulnerable to a 2x burst at window boundaries — 5 requests at 11:59:59 and 5 at 12:00:00 all succeed against a limit of 5/minute.
// Laravel's default throttle:5,1 uses fixed window
Route::post('/login', LoginController::class)->middleware('throttle:5,1');
Sliding Window Log
Tracks the exact timestamp of each request and counts requests in the last N seconds. More accurate than fixed window but requires storing individual request timestamps per client.
// Custom sliding window using Redis sorted set
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
Token Bucket
Bucket holds up to N tokens, adds tokens at a fixed rate. Each request consumes one token. Allows bursts up to bucket size while enforcing an average rate.
// Token bucket via custom RateLimiter
// Allows burst of 10 but sustained rate of 5/minute
RateLimiter::for('api', function (Request $request) {
return [Limit::perMinute(10)->by($request->user()?->id ?: $request->ip())];
});
Leaky Bucket
Requests enter a queue (the bucket) and are processed at a constant outflow rate. Excess requests are dropped (or queued). Smooths traffic spikes to a constant rate.
# Leaky bucket is typically implemented at the infrastructure layer
# Nginx rate limiting uses leaky bucket:
# limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# limit_req zone=login burst=3 nodelay;
In Laravel Applications
Laravel provides built-in rate limiting through the throttle middleware. Configure it in RouteServiceProvider or directly in routes: Route::middleware("throttle:60,1") for 60 requests per minute. For login endpoints, use stricter limits like throttle:5,1.
Code Examples
Login Route Without vs. With Rate Limiting
Vulnerable
// VULNERABLE: No throttle — attacker can attempt unlimited passwords per second
// routes/web.php
Route::post('/login', [AuthenticatedSessionController::class, 'store'])
->name('login');
// No throttle middleware = brute force takes minutes
Secure
// SECURE: Strict rate limiting on authentication endpoints
// routes/web.php
Route::post('/login', [AuthenticatedSessionController::class, 'store'])
->middleware(['throttle:login'])
->name('login');
Route::post('/forgot-password', [PasswordResetLinkController::class, 'store'])
->middleware(['throttle:password-reset'])
->name('password.email');
// routes/RouteServiceProvider.php (or App\Providers\RouteServiceProvider.php)
protected function configureRateLimiting(): void
{
RateLimiter::for('login', function (Request $request) {
return [
Limit::perMinute(5)->by($request->ip()), // IP limit
Limit::perMinute(3)->by($request->input('email')), // Email limit
];
});
RateLimiter::for('password-reset', function (Request $request) {
return Limit::perMinute(3)->by($request->input('email'));
});
}
Real-World Example
Without rate limiting on /login, an attacker can attempt thousands of password combinations per minute. With throttle:5,1, they are limited to 5 attempts per minute, making brute-force impractical.
Why It Matters
Rate limiting is essential for any publicly accessible endpoint in a Laravel application. Without it, attackers can automate requests at a speed that overwhelms your application or enables credential attacks. Even a modest VPS can send hundreds of requests per second — enough to attempt thousands of passwords in minutes.
Laravel's throttle middleware is built on the RateLimiter facade and stores state in your configured cache driver (typically Redis in production). The default throttle:60,1 allows 60 requests per minute per IP — appropriate for general use but far too permissive for authentication endpoints.
For login endpoints, throttle:5,1 (5 attempts per minute) is a minimum. Laravel Breeze and Fortify (laravel/fortify) apply more sophisticated rate limiting that also considers username/email, not just IP, which prevents attackers from rotating IP addresses to bypass IP-only limits. Custom rate limiters defined in RouteServiceProvider::configureRateLimiting() allow granular control.
API endpoints deserve careful rate limiting consideration. Public API endpoints that return data without authentication should have strict limits to prevent scraping. Authenticated API endpoints should have per-user limits in addition to per-IP limits. The RateLimiter::for() method in Laravel 8+ allows building complex, context-aware rate limiting rules.
Common Misconceptions
Myth: Our CDN/WAF handles rate limiting, so we do not need throttle middleware.
Reality: CDN and WAF rate limiting operate at a coarser granularity and are not aware of your application's endpoint semantics. Application-level throttle middleware provides more precise control and can rate limit by user, route, and request context.
Myth: Rate limiting is only needed for login endpoints.
Reality: Any endpoint that could be abused benefits from rate limiting: password reset requests (to prevent email spam abuse), file uploads (to prevent resource exhaustion), search endpoints (to prevent scraping), and API endpoints serving sensitive data.
Myth: Setting a high throttle limit (e.g., 1000/minute) is safe as long as it is there.
Reality: A 1000-request-per-minute throttle allows 14,400 password attempts per hour per IP — sufficient for most wordlist attacks. Login endpoints should use single-digit per-minute limits.
How to Detect This
Inspect your route definitions in `routes/web.php` and `routes/api.php` for throttle middleware with `grep -n "throttle" routes/web.php routes/api.php`. Check your `RouteServiceProvider` for custom rate limiter definitions. To verify rate limiting is active in production, send multiple rapid requests to your login endpoint and confirm you receive a `429 Too Many Requests` response after the configured limit: `for i in {1..10}; do curl -s -o /dev/null -w "%{http_code}\n" -X POST https://yourdomain.com/login; done`. StackShield probes authentication endpoints for rate limiting as part of external scanning and flags endpoints that accept unlimited requests without 429 responses.
How to Prevent This in Laravel
-
1
Add `throttle:5,1` or a named custom limiter to `/login`, `/password/email`, and `/register` routes — these are the highest-priority endpoints for rate limiting.
-
2
Define custom rate limiters in `RouteServiceProvider::configureRateLimiting()` using `RateLimiter::for()` that limit by both IP (`$request->ip()`) and username/email to prevent IP rotation bypasses.
-
3
Set `CACHE_DRIVER=redis` in production `.env` — file and array cache drivers do not persist across processes or servers, making rate limiting unreliable in multi-process setups.
-
4
Verify rate limiting is active by running `for i in {1..10}; do curl -s -o /dev/null -w "%{http_code}\n" -X POST https://yourapp.com/login; done` — you should see 429 responses after the configured limit.
-
5
Configure a custom `429` error response in `app/Exceptions/Handler.php` that does not reveal your rate limit configuration details to attackers.
Frequently Asked Questions
How do I configure rate limiting in Laravel?
Apply the `throttle:N,M` middleware to routes (where N is max attempts and M is decay minutes): `Route::post('/login', ...)->middleware('throttle:5,1')`. For more control, define named limiters in `RouteServiceProvider::configureRateLimiting()` using `RateLimiter::for('login', fn($req) => Limit::perMinute(5)->by($req->ip()))`, then apply with `middleware('throttle:login')`.
Does rate limiting work across multiple application servers?
Only if you use a shared cache driver. The default `CACHE_DRIVER=file` stores counters on the local filesystem — each server maintains its own counter, so a client can make 5 requests to server A and 5 requests to server B without triggering the limit. Set `CACHE_DRIVER=redis` with a shared Redis instance to ensure all application servers share rate limit counters.
What is the difference between `throttle` middleware and custom rate limiters?
The `throttle:N,M` middleware uses a simple IP-based fixed-window counter. Custom rate limiters (defined with `RateLimiter::for()`) allow limiting by authenticated user ID, email address, organization, or any combination, applying different limits to different user tiers, and defining multiple limit rules simultaneously (both per-IP and per-email limits). For login endpoints, always use custom limiters that rate limit by email as well as IP to prevent attackers from rotating IP addresses to bypass per-IP limits.
How do I customize the 429 Too Many Requests response in Laravel?
Override `ThrottleRequestsException` handling in `app/Exceptions/Handler.php` using `$this->renderable(function (ThrottleRequestsException $e, Request $request) { return response()->json(['message' => 'Too many attempts. Please try again later.'], 429); })`. For API responses, Laravel automatically returns JSON 429 responses when the request expects JSON (`Accept: application/json`). Customize the response to avoid revealing your rate limit numbers or retry delay to attackers.
Related Terms
Brute-Force Attack
An attack method that tries every possible combination of credentials until the correct one is found. Brute-force attacks target login forms, API keys, encryption keys, and any authentication mechanism that does not limit the number of attempts.
DDoS (Distributed Denial of Service)
An attack that overwhelms a server or network with traffic from many sources simultaneously, making it unavailable to legitimate users. Unlike a simple DoS attack from one source, DDoS attacks use thousands of compromised devices (a botnet) to generate traffic that is difficult to filter.
Related Articles
OWASP Top 10 for AI Agents: What Laravel Teams Using the AI SDK Need to Know
OWASP released a new Top 10 for Agentic Applications in 2026, and Laravel 13 ships with a first-party AI SDK. This guide covers the intersection: prompt injection, excessive agency, insecure tool use, data leakage, and practical Laravel middleware and validation examples to secure your AI-powered features.
Securing Laravel Queues and Background Jobs
Your queue runs code with no user watching, often with elevated access, on payloads sitting in plaintext. Here is how to encrypt sensitive jobs, lock down the backend, validate user input inside handle(), and keep failed_jobs from leaking your secrets.
Security Logging and Monitoring Failures in Laravel (OWASP A09)
Most Laravel apps log application errors but record almost nothing about security events. Here is how to add a dedicated security channel, capture auth events, redact secrets, and alert on attacks before they become breaches.
Related Fix Guides
How to Fix Missing Rate Limiting in Laravel
Your Laravel login and API endpoints have no rate limiting, enabling brute-force attacks and API abuse. Add throttling now.
Laravel Trusted Proxies Wildcard: How to Configure TrustProxies Middleware Correctly
Setting TrustProxies to trust all proxies (*) lets attackers spoof IP addresses and bypass rate limiting, IP-based access controls, and audit logging.
Monitor Your Laravel Application's Security
StackShield continuously checks your Laravel application from the outside, catching security issues before attackers find them.
Start Free Trial