skip to content

How does Laravel Fortify throttle login attempts, and what changes when config/fortify.php sets limiters.login to a named rate limiter?

level: middleimportance: should knowfreq 30%

answer

  1. two modes, chosen by one config key
  2. null: EnsureLoginIsNotThrottled, 5 tries
  3. key: lowercased username plus IP
  4. named: throttle:login route middleware
  5. missing definition: MissingRateLimiterException

basics

~20 s

With 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 s

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

for a junior

Know that Fortify limits login attempts per username and IP, five by default, and returns a 429 when the limit is hit.

for a middle

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.

for a senior

Anticipate the traps: successful logins counting in named mode, a mismatched username field, proxies collapsing IPs, and MissingRateLimiterException.

for a principal

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.