skip to content

Named Rate Limiters

Named limiters are defined with RateLimiter::for() and Limit objects, segmented with by() and attached with throttle:name. Interviewers ask how limits key per user and what a 429 returns.

on this pageshow

explore

questions

5

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?

level: middleimportance: must knowfreq 60%

answer

  1. AppServiceProvider::boot() registers it
  2. closure receives the Request
  3. by(): user id, else IP
  4. no by() means one shared bucket
  5. 429 with Retry-After and X-RateLimit-* headers

basics

~10 s

Register 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
<?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

for a junior

Recall RateLimiter::for in AppServiceProvider::boot(), a Limit such as perMinute(60), and throttle:name on the route, with a 429 when exceeded.

for a middle

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.

for a senior

Key limits on authenticated identities, make sure authentication runs first and the client IP is real, and share counters across servers.

for a principal

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
open as a page

In Laravel, how does RateLimiter::attempt() limit an arbitrary action such as sending an SMS weather alert, and what does it return?

level: juniorimportance: should knowfreq 30%

basics

~20 s

RateLimiter::attempt($key, $maxAttempts, $callback, $decaySeconds = 60) runs the callback and counts a hit only while attempts remain. It returns false when the limit is reached, otherwise the callback's return value, or true if that is null.

open as a page

In a Laravel named rate limiter, what do after() and response() change about which requests count and what a throttled client receives?

level: middleimportance: should knowfreq 30%

basics

~20 s

after() takes a closure that receives the response and returns true when that response should count, so only chosen outcomes use up the limit. response() replaces the default 429 with your own response built from the request and the rate-limit headers.

open as a page

Where does Laravel store rate-limiter counters, and when would you set the cache.limiter key or call throttleWithRedis() in bootstrap/app.php?

level: seniorimportance: should knowfreq 25%

basics

~10 s

RateLimiter keeps counters in the cache store named by cache.limiter, or the default store when that key is absent. throttleWithRedis() in bootstrap/app.php maps the throttle alias to ThrottleRequestsWithRedis, which counts directly in Redis.

open as a page

For a Laravel weather-data API with per-plan quotas, how would you combine Limit::perSecond, Limit::perDay, limit arrays and Limit::none() in one named limiter?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Have one RateLimiter::for closure read the user's plan and return an array such as [Limit::perSecond(20)->by('s:'.$id), Limit::perDay(100_000)->by('d:'.$id)], or Limit::none() for unlimited plans. Every limit in the array must pass.

open as a page