In Laravel, how do you define a named rate limiter with RateLimiter::for and Limit, key it per user with by(), and attach it with throttle:name?
answer
- AppServiceProvider::boot() registers it
- closure receives the Request
- by(): user id, else IP
- no by() means one shared bucket
- 429 with Retry-After and X-RateLimit-* headers
basics
~10 sRegister RateLimiter::for('weather-api', fn (Request $r) => Limit::perMinute(60)->by($r->user()?->id ?: $r->ip())) in AppServiceProvider::boot(), then add ->middleware('throttle:weather-api') to routes. Over the limit, Laravel answers 429 with Retry-After.
solid answer
~30 s`RateLimiter::for($name, $closure)` registers a limiter, usually in `AppServiceProvider::boot()`. The closure receives the current `Request` and returns a `Limit` such as `Limit::perMinute(60)`. `by()` sets the **bucket key**: `by($request->user()?->id ?: $request->ip())` gives each user, or each guest IP, its own counter; the stored key is also namespaced by the limiter's name. Without `by()` the key is empty, so **every client shares one counter**. Routes opt in with `->middleware('throttle:weather-api')`. When the count is exhausted the middleware throws `ThrottleRequestsException`, a **429** carrying `Retry-After`, `X-RateLimit-Reset`, `X-RateLimit-Limit` and `X-RateLimit-Remaining`; successful responses carry the last two. A misspelled name throws `MissingRateLimiterException`.
code
php · 16 lines<?php
use App\Http\Controllers\ForecastController;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
// AppServiceProvider::boot()
RateLimiter::for('weather-api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:weather-api'])
->get('/forecast/{city}', ForecastController::class);go deeper
Recall RateLimiter::for in AppServiceProvider::boot(), a Limit such as perMinute(60), and throttle:name on the route, with a 429 when exceeded.
Explain by() as the bucket key, the shared bucket when it is missing, the per-request closure, and the Retry-After and X-RateLimit headers.
Key limits on authenticated identities, make sure authentication runs first and the client IP is real, and share counters across servers.
Decide which identity a quota belongs to — user, team, API key or IP — and how quotas map to commercial plans and abuse controls.
## Two ways to throttle a route Laravel's `throttle` middleware alias (`Illuminate\Routing\Middleware\ThrottleRequests`) works in two modes: - **Inline numbers**: `throttle:60,1` means 60 attempts per 1 minute, keyed by the authenticated user's identifier or, for guests, by the route's domain plus the client IP. - **A named limiter**: `throttle:weather-api` looks up a limiter registered with `RateLimiter::for('weather-api', ...)` and lets PHP code decide the limit per request. Named limiters are the modern default because the rule lives in one place and can depend on the request, the user or their plan. ### Inline numbers in more detail The inline form has two lesser-known variants. `throttle:10|60,1` gives guests 10 and authenticated users 60 requests per minute: the value before the pipe applies when `$request->user()` is null. `throttle:rate_limit,1` reads the maximum from an attribute of the authenticated user model, such as a `rate_limit` column set per customer. Both still key by user id or domain plus IP, and neither can express a daily quota next to a burst limit — that is where named limiters take over. ## Defining a named limiter ```php use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; public function boot(): void { RateLimiter::for('weather-api', function (Request $request) { return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); } ``` - `RateLimiter::for()` stores the closure under the name; it runs on **every request** that hits a route using the limiter. - The closure returns a `Limit`. The builders are `Limit::perSecond()`, `perMinute()`, `perHour()` and `perDay()`, each taking the maximum attempts and an optional multiplier for the window (`Limit::perMinute(100, 5)` is 100 per five minutes). `Limit::none()` means no limit. - It may also return an array of limits, or a ready-made response that is sent immediately. ## What `by()` really does `by()` sets the **segment key** — the identity whose requests are counted together. The middleware stores the counter under a hash of the limiter name plus that key, so two limiters never share a counter by accident. Typical choices: | Key | Effect | |---|---| | `$request->user()?->id ?: $request->ip()` | One bucket per user, one per guest IP | | `$request->user()->team_id` | A whole team shares a quota | | `$request->header('X-Api-Key')` | Per-key quotas (validate the key first) | | nothing | **One global bucket for all clients** | The last row is the classic mistake: `Limit::perMinute(60)` without `by()` lets the whole world make 60 requests per minute combined. ## Attaching it to routes ```php Route::middleware(['auth:sanctum', 'throttle:weather-api'])->group(function () { Route::get('/forecast/{city}', ForecastController::class); }); ``` The default middleware priority runs authentication **before** throttling, so `$request->user()` is resolved inside the closure when the route also authenticates. On a route without authentication the closure only sees guests and falls back to the IP. Behind a load balancer, make sure the app sees the real client IP, or every guest shares the proxy's address. ## What the client sees 1. While under the limit, each response gets `X-RateLimit-Limit` and `X-RateLimit-Remaining`. 2. Once the count reaches the maximum, the middleware throws `Illuminate\Http\Exceptions\ThrottleRequestsException` (message "Too Many Attempts."), rendered as **429 Too Many Requests**. 3. The 429 carries `Retry-After` (seconds until the window resets) and `X-RateLimit-Reset` (a Unix timestamp), plus `X-RateLimit-Remaining: 0`. The window starts with the first counted request and lasts the decay period; the counter lives in the application cache. ## Failure modes worth knowing - **Undefined name**: `throttle:weather-apii` does not silently fall back; the middleware throws `MissingRateLimiterException` ("Rate limiter [weather-apii] is not defined."), a server error. - **Per-server cache**: counters in a store each server keeps separately are not shared across servers, so the effective limit multiplies. - **Keys from unvalidated input**: keying by a header or input value lets a client rotate values to get fresh buckets. Key by something the server has authenticated.
- What happens if a Laravel limiter returns Limit::perMinute(60) without calling by()?The segment key is empty, so the middleware stores one counter under the limiter's name for everybody. All clients together get 60 requests per minute, and one busy client can lock everyone out. Always key by an authenticated identity, falling back to the IP for guests.
- How does a named limiter differ from throttle:60,1 on the same route?`throttle:60,1` hard-codes the numbers and always keys by user id or domain plus IP. A named limiter runs PHP per request, so it can pick limits by plan, return several limits, skip limiting with `Limit::none()`, count only some responses with `after()` and customise the 429 with `response()`.
saying these in an interview costs you the question
- Without by(), each client automatically gets its own bucket
- A misspelled throttle name falls back to 60 requests per minute
- An exceeded limit returns 403 Forbidden
- Limiter closures run once at boot, not per request
- Rate-limit counters live in the session