Laravel API Rate-Limiting: Custom Limiters &amp; Headers | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Laravel API Rate-Limiting: Custom Limiters, Per-Route Strategies, and Header Contracts

 Laravel API Rate-Limiting: Custom Limiters, Per-Route Strategies, and Header Contracts
=======================================================================================

 Go beyond the default throttle middleware. Build named rate limiters with dynamic keys, per-user tiers, and consistent response headers that API consumers can rely on in production.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 27 Jul 2026 · Updated 27 Jul 2026 · 4 min read

ShareCopy linkCopied

 ![Laravel API Rate-Limiting: Custom Limiters, Per-Route Strategies, and Header Contracts](https://cdn.msaied.com/474/47b5edd1bda5625b01ae20f7684ed199.png) 

  On this page +1. [Beyond throttle:60,1](#beyond-codethrottle601code)
2. [Defining Named Limiters in a Service Provider](#defining-named-limiters-in-a-service-provider)
3. [Attaching Limiters to Routes](#attaching-limiters-to-routes)
4. [Consistent Response Headers](#consistent-response-headers)
5. [Handling 429 Responses Gracefully](#handling-429-responses-gracefully)
6. [Testing Limiters with Pest](#testing-limiters-with-pest)
7. [Key Takeaways](#key-takeaways)

 Beyond `throttle:60,1`
----------------------

The built-in `throttle` middleware is fine for a quick demo, but production APIs need rate-limiting that reflects business rules: free-tier users get 100 requests/minute, paid users get 2 000, and certain endpoints — like password reset — have their own hard caps regardless of tier.

Laravel's `RateLimiter` facade, introduced in Laravel 8 and refined since, gives you exactly that control.

---

Defining Named Limiters in a Service Provider
---------------------------------------------

Register all limiters in `AppServiceProvider::boot` (or a dedicated `RateLimitServiceProvider`).

```php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    // Tier-aware API limiter
    RateLimiter::for('api', function (Request $request) {
        $user = $request->user();

        if (! $user) {
            return Limit::perMinute(30)->by($request->ip());
        }

        return match ($user->plan) {
            'enterprise' => Limit::none(),
            'pro'        => Limit::perMinute(2000)->by($user->id),
            default      => Limit::perMinute(100)->by($user->id),
        };
    });

    // Hard cap on auth-sensitive endpoints
    RateLimiter::for('auth-sensitive', function (Request $request) {
        return [
            Limit::perMinute(5)->by($request->ip()),
            Limit::perHour(20)->by($request->ip()),
        ];
    });
}

```

Returning an **array** of `Limit` objects lets you stack multiple windows on a single route — both must pass.

---

Attaching Limiters to Routes
----------------------------

```php
// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:api'])
    ->group(function () {
        Route::get('/widgets', [WidgetController::class, 'index']);
    });

Route::post('/forgot-password', [PasswordController::class, 'store'])
    ->middleware('throttle:auth-sensitive');

```

The string passed to `throttle:` maps directly to the name registered with `RateLimiter::for`.

---

Consistent Response Headers
---------------------------

Laravel automatically adds `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `Retry-After` when a limiter is hit. But for clients that need to *proactively* back off, you want those headers on every response, not just 429s.

Add a middleware that reads the current hit count without incrementing it:

```php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Symfony\Component\HttpFoundation\Response;

class AddRateLimitHeaders
{
    public function handle(Request $request, Closure $next, string $limiterName = 'api'): Response
    {
        $response = $next($request);

        $limiter = RateLimiter::limiter($limiterName);
        /** @var Limit $limit */
        $limit = value($limiter, $request);

        if ($limit instanceof Limit) {
            $key       = $limit->key;
            $maxAttempts = $limit->maxAttempts;
            $remaining = max(0, $maxAttempts - RateLimiter::attempts($key));

            $response->headers->set('X-RateLimit-Limit', $maxAttempts);
            $response->headers->set('X-RateLimit-Remaining', $remaining);
        }

        return $response;
    }
}

```

Register it after `throttle` in the middleware stack so it runs on successful responses too.

---

Handling 429 Responses Gracefully
---------------------------------

The default 429 response is an HTML page. Override it in your exception handler:

```php
// bootstrap/app.php (Laravel 11+)
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (
        \Illuminate\Http\Exceptions\ThrottleRequestsException $e,
        Request $request
    ) {
        return response()->json([
            'error'       => 'rate_limit_exceeded',
            'retry_after' => $e->getHeaders()['Retry-After'] ?? null,
        ], 429, $e->getHeaders());
    });
})

```

Passing `$e->getHeaders()` ensures `Retry-After` and `X-RateLimit-*` headers survive the custom renderer.

---

Testing Limiters with Pest
--------------------------

```php
use Illuminate\Support\Facades\RateLimiter;

it('blocks free-tier users after 100 requests per minute', function () {
    $user = User::factory()->create(['plan' => 'free']);

    // Exhaust the limit without real HTTP overhead
    RateLimiter::hit('100|' . $user->id, 60, 100);

    $this->actingAs($user)
        ->getJson('/api/widgets')
        ->assertStatus(429)
        ->assertJsonFragment(['error' => 'rate_limit_exceeded']);
});

it('does not throttle enterprise users', function () {
    $user = User::factory()->create(['plan' => 'enterprise']);

    $this->actingAs($user)
        ->getJson('/api/widgets')
        ->assertOk();
});

```

`RateLimiter::hit` lets you seed the counter directly, avoiding 100 actual HTTP calls in your test suite.

---

Key Takeaways
-------------

- Use `RateLimiter::for` with closures to express business-tier logic, not magic middleware strings.
- Return an array of `Limit` objects to enforce multiple windows (per-minute **and** per-hour) simultaneously.
- Always emit `X-RateLimit-*` headers on successful responses so clients can self-throttle.
- Override the 429 renderer to return JSON with the `Retry-After` header intact.
- Seed `RateLimiter::hit` in tests to simulate exhausted limits without real HTTP loops.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [api](https://www.msaied.com/public/articles?search=api)
- [rate-limiting](https://www.msaied.com/public/articles?search=rate-limiting)
- [middleware](https://www.msaied.com/public/articles?search=middleware)
- [pest](https://www.msaied.com/public/articles?search=pest)

 Frequently asked questions 
---------------------------

  Can I use a database or Redis key other than the default cache store for rate limiters?Yes. Laravel's RateLimiter uses the default cache store, but you can swap it by binding a custom RateLimiter instance in the service container that points to a specific cache driver, or by configuring your default cache to Redis in production — which is the recommended approach for distributed deployments.

   How do I reset a user's rate-limit counter programmatically, for example after a plan upgrade?Call `RateLimiter::clear($key)` where `$key` matches the string returned by your limiter closure (e.g. `'100|' . $user-&gt;id`). This removes the counter from the cache immediately, giving the user a fresh window.

   Does returning `Limit::none()` for enterprise users skip the throttle middleware entirely?Yes. `Limit::none()` signals the throttle middleware to allow unlimited requests. No cache key is written and no headers are added, so there is zero overhead for those users.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleProfiling Laravel with Blackfire and Xdebug: Finding Real Bottlenecks](https://www.msaied.com/public/articles/profiling-laravel-with-blackfire-and-xdebug-finding-real-bottlenecks-1) [Next articleEloquent Query Optimization: Killing N+1 Problems at the Source](https://www.msaied.com/public/articles/eloquent-query-optimization-killing-n1-problems-at-the-source)  

   On this page
-------------

1. [Beyond throttle:60,1](#beyond-codethrottle601code)
2. [Defining Named Limiters in a Service Provider](#defining-named-limiters-in-a-service-provider)
3. [Attaching Limiters to Routes](#attaching-limiters-to-routes)
4. [Consistent Response Headers](#consistent-response-headers)
5. [Handling 429 Responses Gracefully](#handling-429-responses-gracefully)
6. [Testing Limiters with Pest](#testing-limiters-with-pest)
7. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
