php 113 lines · 3 tabs

Per-User API Rate Limiting With a Custom RateLimiter in Laravel

Shared by codesnips Aug 2026
3 tabs
<?php

namespace App\Providers;

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

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->configureRateLimiting();
    }

    protected function configureRateLimiting(): void
    {
        RateLimiter::for('api', function (Request $request) {
            $user = $request->user();

            if ($user) {
                $perMinute = $user->onPaidPlan() ? 300 : 60;

                return Limit::perMinute($perMinute)
                    ->by('user:' . $user->id)
                    ->response(fn () => $this->tooManyRequests());
            }

            return Limit::perMinute(20)
                ->by('ip:' . $request->ip())
                ->response(fn () => $this->tooManyRequests());
        });

        RateLimiter::for('uploads', function (Request $request) {
            $key = optional($request->user())->id ?: $request->ip();

            return Limit::perMinute(5)
                ->by('upload:' . $key)
                ->response(fn () => $this->tooManyRequests());
        });
    }

    protected function tooManyRequests()
    {
        return response()->json([
            'message' => 'Rate limit exceeded. Please slow down.',
        ], 429, ['Retry-After' => 60]);
    }
}
3 files · php Explain with highlit

Laravel ships a RateLimiter facade that lets applications define named rate-limit policies once and apply them anywhere via the throttle middleware. This snippet shows how to define a per-user policy that falls back to the client IP for guests, expose it as a route middleware alias, and shape the response returned when a caller exceeds the limit.

The AppServiceProvider tab wires everything up in boot(). RateLimiter::for('api') registers a named limiter whose closure receives the current Request and returns a Limit describing how many hits are allowed per window. When the request is authenticated, the limit is keyed by $user->id so every account gets its own bucket; unauthenticated traffic is keyed by $request->ip() so guests are throttled per address. The by() call is what isolates buckets — without it every caller would share one global counter. The provider also registers a second, stricter uploads limiter to show that multiple policies can coexist, and passes a response() callback so exhausted callers receive a clean JSON payload with a Retry-After header instead of Laravel's default plain text.

Because the limiter is a closure, the policy can vary at runtime. In AppServiceProvider the allowed count is raised for users on a paid plan, demonstrating dynamic limits driven by application state rather than a fixed number. Returning Limit::none() would exempt a caller entirely, which is useful for internal service tokens.

The routes/api.php tab attaches the policy with ->middleware('throttle:api'), where api matches the limiter name. The stricter group uses throttle:uploads. This keeps rate-limit configuration out of controllers entirely.

The ApiController tab is deliberately thin — it contains no throttling logic, proving that the concern lives in the limiter definition and middleware. The trade-off of this approach is that limiter state lives in the configured cache store; a single-server file or array store will not coordinate limits across nodes, so production deployments should point the cache at Redis so counters are shared. A common pitfall is keying by a value that changes per request (such as a full URL) which silently defeats the limit, so keys should identify the caller, not the action.


Related snips

Share this code

Here's the card — post it anywhere.

Per-User API Rate Limiting With a Custom RateLimiter in Laravel — share card
Link copied