In a Laravel named rate limiter, what do after() and response() change about which requests count and what a throttled client receives?
answer
- after(fn (Response $r) => bool)
- counted after the route runs
- count only 404s against enumeration
- response(fn ($request, array $headers))
- pass $headers to keep Retry-After
basics
~20 safter() 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.
solid answer
~40 sNormally a limit counts every request **before** the route runs. With `->after(fn (Response $response) => $response->status() === 404)` the middleware skips that early count and, **after** the route returns, counts the request only if the closure returns `true`. That lets a weather API throttle clients probing unknown station ids with 404s, or charge the daily quota only for successful responses. Because the count happens late, concurrent requests can all pass the check before any is counted, so a burst can overshoot the limit. `->response(fn (Request $request, array $headers) => response()->json([...], 429, $headers))` replaces the default `ThrottleRequestsException` with your own response; forward `$headers` or the client loses `Retry-After` and the `X-RateLimit-*` values.
code
php · 15 lines<?php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Symfony\Component\HttpFoundation\Response;
RateLimiter::for('station-lookup', function (Request $request) {
return Limit::perMinute(10)
->by($request->user()?->id ?: $request->ip())
->after(fn (Response $response) => $response->status() === 404)
->response(fn (Request $request, array $headers) => response()->json(
['error' => 'too_many_unknown_stations'], 429, $headers
));
});go deeper
Recall that after() decides which responses count and response() customises the reply to a throttled client.
Explain that after() moves counting to after the route while the check stays before it, and that response() receives the request and the headers.
Use response-based limits for enumeration and quota fairness, bound the overshoot from late counting, and keep headers on custom 429s.
Decide which outcomes a customer is charged for and how throttling is communicated, so quotas feel fair and clients back off correctly.
## Default behaviour Without either method, a Laravel limit: 1. checks whether its counter is exhausted; 2. if so, throws `ThrottleRequestsException` — a 429 with `Retry-After`, `X-RateLimit-Reset`, `X-RateLimit-Limit` and `X-RateLimit-Remaining`; 3. otherwise counts the request immediately, **before** the route runs; 4. adds the limit headers to the route's response. `after()` changes step 3; `response()` changes step 2. ## `after()`: count only some outcomes `after()` receives a closure that is called with the route's response (a `Symfony\Component\HttpFoundation\Response`) and returns `true` when that response should count: ```php RateLimiter::for('station-lookup', function (Request $request) { return Limit::perMinute(10) ->by($request->user()?->id ?: $request->ip()) ->after(fn (Response $response) => $response->status() === 404); }); ``` With an `after()` callback the middleware: - still **checks** the counter before the route runs, so an exhausted client gets the 429 immediately; - does **not** count the request up front; - runs the route, then calls the closure and counts the request only if it returned `true`. Useful patterns for a weather-data API: | Callback | Effect | |---|---| | `$response->status() === 404` | Slows clients enumerating station or city ids; normal lookups are free | | `$response->isSuccessful()` | The daily quota is spent only on answers the client actually received | | `$response->status() !== 422` | Validation mistakes do not burn quota | ### The cost of counting late Because the count happens after the route finishes, several concurrent requests can all pass the check while the counter still shows room, and each is counted only afterwards. A client that fires many parallel requests can therefore exceed the limit by roughly its concurrency. For a quota where that matters, prefer counting up front, or pair the response-based limit with an ordinary burst limit in the same array. ## `response()`: shape the throttled reply `response()` takes a callable that receives the **request** and the **array of headers** the middleware computed, and returns the response to send: ```php return Limit::perDay(1_000)->by('day:'.$id)->response( fn (Request $request, array $headers) => response()->json([ 'error' => 'daily_quota_exceeded', 'upgrade_url' => 'https://weather.example/pricing', ], 429, $headers) ); ``` - The middleware wraps your response in an `HttpResponseException`, so it is returned as-is instead of going through the default 429 rendering. - **Forward `$headers`**: they hold `Retry-After`, `X-RateLimit-Reset`, `X-RateLimit-Limit` and `X-RateLimit-Remaining`. Dropping them breaks clients that back off automatically. - **Keep status 429** unless you have a strong reason; clients and proxies treat it specially. - In a limit array, each limit can carry its own `response()`, so the burst limit and the daily quota can explain themselves differently. ## Worked example: an enumeration defence A weather API exposes `/stations/{code}`. Legitimate clients request a handful of known stations; a scraper walks through every possible code. A plain limit of 10 per minute would hurt a dashboard that polls ten stations, while a limit that counts **only 404s** leaves that dashboard untouched and stops the scraper after ten misses. Pair it with a generous ordinary burst limit in the same array, so a client cannot hammer valid codes either: ```php return [ Limit::perMinute(600)->by('burst:'.$key), Limit::perMinute(10)->by('misses:'.$key)->after(fn (Response $r) => $r->status() === 404), ]; ``` ## Choosing between them and the alternatives - To change **what counts**, use `after()`. - To change **what the client sees** for one limiter, use `response()`. - To change the 429 page for **every** throttled route, customise how `ThrottleRequestsException` is rendered instead. - To exempt a whole class of users, return `Limit::none()` from the closure rather than a callback that never counts. ## Common mistakes - Expecting `after()` to skip the pre-route check; exhausted clients are still rejected up front. - Returning a 200 from `response()` so that clients never notice they are throttled. - Building the custom response without the headers and then wondering why SDKs retry immediately.
- Does a limit with after() still reject a client whose counter is already exhausted before the route runs?Yes. The middleware always checks every limit before calling the route; `after()` only moves the counting step to after the response. An exhausted client gets the 429 immediately, and the route never runs for that request.
- Why can a burst of parallel requests exceed a Laravel limit that uses after()?Each request checks the counter before running and is counted only after it finishes. Requests that start together all see the same pre-burst count, pass the check, and are counted later, so the counter overshoots the maximum by up to the number of requests in flight.
saying these in an interview costs you the question
- after() runs the check after the route, so exhausted clients reach the controller
- response() should return status 200 so clients are not alarmed
- The response() callback receives only the request
- Custom throttled responses get the rate-limit headers automatically
- after() makes the limit exact even under concurrent requests