How does Laravel Fortify throttle login attempts, and what changes when config/fortify.php sets limiters.login to a named rate limiter?
answer
- two modes, chosen by one config key
- null: EnsureLoginIsNotThrottled, 5 tries
- key: lowercased username plus IP
- named: throttle:login route middleware
- missing definition: MissingRateLimiterException
basics
~20 sWith limiters.login empty, Fortify's own EnsureLoginIsNotThrottled step allows 5 failed attempts per username-and-IP key before a 429. With limiters.login set to a name, as in the stub, the login route gets throttle:login instead, using the RateLimiter::for('login') definition in FortifyServiceProvider.
solid answer
~50 sFortify has two throttling modes. If `fortify.limiters.login` is `null` (the package default), the login pipeline starts with `EnsureLoginIsNotThrottled`, backed by `LoginRateLimiter`: the key is the lowercased, transliterated username plus the IP, only **failed** attempts are counted, the limit is 5 with a 60-second decay, a success clears the counter, and a lockout throws a validation error with status 429. If `limiters.login` names a limiter, as the published stub's `'login'` does, that pipeline step is skipped and `POST /login` gets the `throttle:login` middleware instead. The rule then comes from the `RateLimiter::for('login', ...)` in the published `FortifyServiceProvider`, which in the stub is `Limit::perMinute(5)->by(username|ip)`; this counts **every** attempt, successful or not. Rename or delete that definition while the config still names it and requests fail with `MissingRateLimiterException`. The stub also defines `two-factor` (5 per minute per `login.id`) and `passkeys` (10 per minute) limiters.
code
php · 16 lines<?php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Laravel\Fortify\Fortify;
// App\Providers\FortifyServiceProvider::boot() - the published stub's rule
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(
Str::lower($request->input(Fortify::username())).'|'.$request->ip()
);
return Limit::perMinute(5)->by($throttleKey);
});go deeper
Know that Fortify limits login attempts per username and IP, five by default, and returns a 429 when the limit is hit.
Explain the two modes selected by limiters.login, where each rule is defined, and how the published provider's RateLimiter::for('login') is wired in.
Anticipate the traps: successful logins counting in named mode, a mismatched username field, proxies collapsing IPs, and MissingRateLimiterException.
Decide how login throttling layers with two-factor, bot defence at the edge and account lockout policy, and who owns tuning it under attack.
## Why Fortify throttles at all A login endpoint is the obvious target for password guessing. Fortify throttles it out of the box, keyed by **the username being tried and the client IP**, so one attacker hammering one account is slowed while other users on the same network are not. The rule for building that limiter with `RateLimiter::for` belongs to Laravel's routing layer; what matters here is how Fortify decides which mechanism applies. ## Mode 1: `limiters.login` is empty The package's own default config sets `'limiters' => ['login' => null, ...]`. In that mode the login pipeline begins with `Laravel\Fortify\Actions\EnsureLoginIsNotThrottled`, which asks `Laravel\Fortify\LoginRateLimiter`: - **Key:** `Str::transliterate(Str::lower($username)).'|'.$ip`. - **Limit:** `tooManyAttempts($key, 5)`: five attempts. - **Counting:** `increment()` is called only when credentials fail, with a 60-second decay. - **Reset:** `PrepareAuthenticatedSession` calls `clear()` after a successful login. - **Lockout:** a `Lockout` event fires and `LockoutResponse` throws a `ValidationException` on the username field with the `auth.throttle` message and **status 429**. ## Mode 2: `limiters.login` names a limiter The stub published by `fortify:install` sets `'login' => 'login'`. Fortify's route file then adds `throttle:login` to `POST /login`, and the login pipeline **omits** `EnsureLoginIsNotThrottled`. The rule is whatever `RateLimiter::for('login', ...)` returns; the published `FortifyServiceProvider` defines it as five per minute, keyed by the lowercased username plus IP. | | `limiters.login` = `null` | `limiters.login` = `'login'` (stub) | |---|---|---| | Enforced by | `EnsureLoginIsNotThrottled` pipeline step | `throttle:login` route middleware | | Rule defined in | Fortify's `LoginRateLimiter` (5 tries) | your `RateLimiter::for('login')` | | What counts | failed attempts only | every request to `POST /login` in the stub's rule | | Success resets | yes | no; the named limiter uses its own key | | Customisable | only by replacing the pipeline | edit the closure: by IP, per account, per plan | The second mode exists so you can change the rule, for example to throttle by IP alone or to add a daily cap, without touching Fortify. ## The traps 1. **A missing definition.** If config names `login` but the `RateLimiter::for('login')` call was deleted or renamed, the throttle middleware cannot find the limiter and throws `Illuminate\Routing\Exceptions\MissingRateLimiterException` on every login. 2. **Successful logins count in mode 2.** With the stub's rule, a user who signs in and out repeatedly can hit the limit; success does not clear the named limiter's key. 3. **The username field must match.** Both modes read `$request->input(Fortify::username())`. If a tax-filing app signs in by taxpayer reference but leaves `'username' => 'email'`, the key uses an empty username and collapses to per-IP throttling. 4. **Proxies.** Keys include `$request->ip()`; behind a load balancer without trusted-proxy configuration every user shares one IP and one budget. ## The other Fortify limiters The stub's `limiters` array also names: - `two-factor`: five challenge submissions per minute, keyed by the `login.id` value Fortify stores in the session when it redirects to the challenge; - `passkeys`: ten requests per minute on the passkey login, confirmation and registration routes, keyed by credential ID or session ID plus IP. Email verification uses `limiters.verification`, which falls back to `'6,1'` when unset.
- What happens if config/fortify.php names the login limiter but FortifyServiceProvider no longer defines it?`POST /login` still carries `throttle:login`, but the rate limiter registry has no `login` entry, so `ThrottleRequests` treats `login` as a max-attempts value, finds it non-numeric and throws `MissingRateLimiterException`. Every login attempt fails until the definition is restored or the config key is set to `null`.
- In Fortify's default mode, does a successful login reset the counter?Yes. `PrepareAuthenticatedSession` calls `LoginRateLimiter::clear()` after the user is signed in, so earlier failures for that username and IP are forgotten. In named-limiter mode that call does not touch the named limiter's key.
saying these in an interview costs you the question
- Fortify has no login throttling unless you add the throttle middleware yourself.
- Fortify's throttle key is the IP address alone.
- In the stub setup, only failed attempts count toward the limit.
- Deleting RateLimiter::for('login') simply turns throttling off.
- A lockout returns a 401 with no retry information.