A Laravel gotcha: the throttle counter is shared
I wrote a small cookie-free analytics beacon for my site: every page load and every button click sends one request to the server. The form allows five submissions per minute. While testing, the form returned a 429 error, although I had barely tried anything.
What happened
In Laravel, a rule such as throttle:5,1 builds the request identity from the IP address, regardless of the route. If I put different limits on two routes, the counter is still shared. The beacon's requests used up the form's five submissions.
The fix: named limiters
I give every limiter its own key and refer to the rule by name. That way the two routes have separate counters.
// The plain "throttle:N,M" form shares ONE counter per IP, whatever the route.
// Named limiters with their own keys keep the counters apart.
RateLimiter::for('contact', fn (Request $request) => Limit::perMinute(5)->by('contact|'.$request->ip()));
RateLimiter::for('events', fn (Request $request) => Limit::perMinute(120)->by('events|'.$request->ip()));
// routes/web.php
Route::post('/e', [EventController::class, 'store'])->middleware('throttle:events');
Route::post('/contact', [ContactController::class, 'send'])->middleware('throttle:contact');
An excerpt of real code, anonymised.
How I will notice next time
I wrote a test that sends eight beacon requests, then submits the form and expects success. If anyone goes back to the plain form, the test fails. This kind of bug is rarely noticed by reading the code, but when someone just sees the form not going through.
Working on a similar problem?
If you are stuck, or want a second look at your solution, write to me. I am happy to look.
Write to me →