Topic 35 of 75·M16
Rate Limiting
Senior-levelLaravel RateLimiter · API throttling · user-based limits · IP-based limits · Redis-backed rate limiting · 100 req/min/user
The one-sentence answer
Rate limiting caps how often a client can hit an endpoint — throttle:60,1 or the RateLimiter facade — returning 429 when the limit is exceeded.
The mental model
request → rate limiter → under limit? → route runs
→ over limit? → 429 Too Many Requests
Rate limiting is the bouncer at the API door. It protects the server from abusive clients (bots, scraping, a misbehaving integration) and protects shared resources (login attempts, email sends, expensive queries) from being hammered.
The checklist's example: 100 requests/minute/user — each authenticated user gets a budget; exceed it and the API says 429 until the window resets.
How it works
The middleware — throttle
Route::middleware('throttle:60,1')->group(function () {
Route::get('/api/orders', ...); // 60 requests per minute
});
// throttle:{max attempts},{decay minutes}
The built-in throttle middleware tracks by IP by default and returns 429 with X-RateLimit-* headers when exceeded.
The RateLimiter facade — per-user and custom
// in a service provider's boot() or routes:
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(100) // 100 req/min/user
->by($request->user()?->id ?: $request->ip()); // authenticated → user id; else IP
});
// apply it:
Route::middleware('throttle:api')->group(function () { /* ... */ });
->by() is the key: the limit is scoped to a key — user id when logged in, IP otherwise. That's how "100/minute/user" is per-user, not per-server.
Other limit shapes
Limit::perMinute(60);
Limit::perHour(1000);
Limit::perDay(100);
->by($request->ip()); // IP-based
Limit::perMinute(5)->by($request->input('email')); // per-credential (login attempts)
Redis-backed
// .env: CACHE_STORE=redis
// the limiter stores its counters in the cache store — Redis means the limits are
// shared across all app servers, not per-server
With the file cache, each server counts its own requests — behind a load balancer, the limit is multiplied by the server count. Redis makes the counter global (Lesson 34).
The plain-JS model (what the exercise does)
function rateLimit(key, max, windowMs) {
const count = (counters[key] ||= 0);
if (count >= max) return 429; // over the limit
counters[key] = count + 1;
setTimeout(() => counters[key]--, windowMs); // window decay
return 200; // under the limit
}Interview questions
Q1. What is rate limiting, and why does it matter?
Capping how often a client may hit an endpoint — the bouncer at the API door. It protects the server from abusive clients and protects shared resources (logins, email, expensive queries) from being hammered. Laravel's
throttle:60,1middleware or theRateLimiterfacade enforce it, returning 429 when exceeded.
Q2. throttle:60,1 — what does that mean?
Max 60 attempts per 1-minute decay window. The built-in middleware tracks by IP and returns 429 with rate-limit headers when the limit is hit. The middleware form is the quick route-level version; the
RateLimiterfacade is the flexible one (per-user keys, custom limits).
Q3. How do you do per-user rate limiting?
With
RateLimiter::for('api', fn ($request) => Limit::perMinute(100)->by($request->user()?->id ?: $request->ip())). The->by()key is the scope: authenticated requests count against the user's id, anonymous ones against the IP. That's how "100 requests/minute/user" works — the budget follows the user, not the connection.
Q4. Why Redis for rate limiting?
Because the counter must be shared. With the file cache, each app server counts separately — a 100/min limit behind 4 servers is effectively 400/min. Redis gives one global counter (Lesson 34), so the limit is exact regardless of how many servers serve the traffic.
Q5. What does a rate-limited client see?
429 Too Many Requests (Lesson 25), with
X-RateLimit-Limit,X-RateLimit-Remaining, and usually aRetry-Afterheader so the client knows when the window resets. A well-behaved client backs off; the limit is enforced server-side regardless.
Senior follow-up: How do you rate-limit login attempts specifically?
Scope the limit to the credential, not the IP —
Limit::perMinute(5)->by($request->input('email'))— so an attacker can't rotate IPs to brute-force one account, and one user's failures don't lock out a shared office IP. The general principle: scope the limit to the thing being protected — user for API abuse, email for credential attacks, IP as the fallback.
Common mistakes
❌ IP-based limits behind a load balancer — every user shares the proxy's IP (unless configured).
❌ File-cache counters on multiple servers — the limit multiplies per server; use Redis.
❌ Rate-limiting by IP when per-user is the intent — authenticated requests should key on the user.
❌ No limit on expensive endpoints — an unthrottled report endpoint is a self-DoS.
Quick revision notes
- Rate limit = the bouncer — cap requests, return 429 when exceeded
throttle:60,1= middleware form ·RateLimiter::for(...)= flexible formLimit::perMinute(100)->by($key)— the key scopes the limit (user/IP/email)- Redis = one shared counter across servers (Lesson 34)
- 429 +
X-RateLimit-*/Retry-Afterheaders - Scope the limit to what's protected — user for API, email for logins
Check your understanding
Answer each one out loud. Then rate yourself honestly.
0/5What does throttle:60,1 actually enforce?
How is "100/minute/user" scoped to the user?
Why is file-cache rate limiting wrong behind a load balancer?
What status code and headers does an exceeded client get?
How do you rate-limit logins without locking out an office IP?