Skip to content

Rate Limiting Fails with Multiple Distinct Keys and Order-Sensitive Limits #54386

Open
@mohammadkhoshnood88

Description

@mohammadkhoshnood88

Laravel Version

11

PHP Version

8.1.4

Database Driver & Version

No response

Description

The current ThrottleRequests middleware incorrectly handles rate limits when:

  1. Multiple distinct keys (e.g., user_id + product_id) are used in the same limiter.
  2. Order of limits in the array affects functionality (e.g., per-day limit checked before per-minute).

Steps To Reproduce

Issue 1: Over-Throttling with Multiple Distinct Keys

  1. Define a limiter:
RateLimiter::for('update-product', function (Request $request) {
    return [
        Limit::perDay(1)->by('user:' . $request->user()->id),
        Limit::perDay(1)->by('product:' . $request->product->id)
        
    ];
});
  1. Scenario:
  • User 1 updates Product 1 → 200 OK.
  • User 2 updates Product 1 → 429 (Correct: product limit).
  • User 2 tries to update Product 2 → 429 (Wrong: should be allowed!).
  1. Expected:
  • Throttling should only apply to the violated key (product_id).

Issue 2: Order-Sensitive Limits in Documentation

  1. Description:
    The documentation example shows limits ordered as perMinute then perDay, but reversing the order breaks functionality.

  2. Broken Example:

RateLimiter::for('uploads', function (Request $request) {
    return [
        Limit::perDay(1000)->by($request->user()->id), // Long-term first
        Limit::perMinute(10)->by($request->user()->id), // Short-term second
    ];
});
  1. Scenario:
  • Send 1000 requests in 1 minute.
  1. Expected:
  • After a minute, the user can upload but cannot.

Proposed Solution

Modify src/Illuminate/Routing/Middleware/ThrottleRequests.php to:

  • Check all limits first, then apply hit().

Code Fix:

protected function handleRequest($request, Closure $next, array $limits) {
    // Check all limits first
    foreach ($limits as $limit) {
        if ($this->tooManyAttempts($limit->key, $limit->maxAttempts)) {
            throw $this->buildException(...);
        }
    }

    // Apply hits if all checks pass
    foreach ($limits as $limit) {
       $this->limiter->hit($limit->key, $limit->decaySeconds);
    }

    return $next($request);
}

Suggested Labels: bug, rate limiting

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions