Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Rate Limiting and API Throttling in Laravel

By Kokil Thapa | Last reviewed: September 2026

A single aggressive scraper can starve your database pool before you notice anything wrong. Laravel API rate limiting stops that by capping requests per user, IP, or API key inside a sliding time window. Laravel ships a built-in throttle middleware backed by cache counters. You define policies once and attach them to routes. For any public API — mobile apps, partner integrations, or SaaS endpoints — this is baseline infrastructure, not an optional extra. Start with Laravel API best practices to see where throttling fits alongside auth, validation, and versioning.

The framework increments a counter on each request and compares it against your configured ceiling. Exceed the limit and Laravel returns HTTP 429 before your controller runs. That saves CPU, database connections, and queue workers from abuse you never asked for. On production Laravel applications I maintain, throttling sits alongside Sanctum or Passport authentication as the first line of defense after TLS termination.

How do you configure Laravel API rate limiting in Laravel 13?

Since Laravel 11, limiter definitions live in bootstrap/app.php instead of RouteServiceProvider. Laravel 13 keeps the same pattern. Laravel 12 works identically and remains supported through February 2027. Named limiters decouple policy from route files. You reuse the same throttle profile across API routes, webhooks, and even queued jobs that call external services.

Define named limiters in bootstrap/app.php

The most common pattern gives authenticated users a higher ceiling than anonymous traffic. Open bootstrap/app.php and register your limiters inside the application bootstrap callback:

<?php

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

// Inside bootstrap/app.php
RateLimiter::for('api', function (Request $request) {
    return $request->user()
        ? Limit::perMinute(120)->by($request->user()->id)
        : Limit::perMinute(30)->by($request->ip());
});

RateLimiter::for('auth-actions', function (Request $request) {
    return Limit::perMinute(5)->by($request->ip());
});

The by() method sets the partition key for each counter bucket. Use $request->user()->id for authenticated traffic. Omit it and every logged-in user shares one global bucket. I hit this during load testing on a legal-tech portal. One user's batch export locked out every other session within seconds.

For inline limits without a named limiter, pass arguments directly to the middleware: throttle:60,1 allows 60 requests per minute keyed by IP. Named limiters are cleaner when logic grows beyond a single number.

RequestAuth Checkuser() set?YesNoUser Bucketby user_idGuest Bucketby IP addressProceed
Laravel API rate limiting routes each request to a user or guest counter bucket before the controller executes.

Attach limiters to API routes

Apply the named limiter in routes/api.php. Place authentication middleware before throttle when the limiter reads $request->user():

<?php

use Illuminate\Support\Facades\Route;

Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
    Route::get('/documents', [DocumentController::class, 'index']);
    Route::post('/documents', [DocumentController::class, 'store']);
});

Route::middleware('throttle:auth-actions')->group(function () {
    Route::post('/login', [AuthController::class, 'login']);
    Route::post('/forgot-password', [AuthController::class, 'forgotPassword']);
});

Wrong middleware order is a silent bug. Throttle runs first, the user object is null, and every request lands in the guest bucket. See the official Laravel rate limiting documentation for additional limit types like perHour(), perDay(), and decay callbacks.

Why does Laravel API rate limiting need Redis in production?

The throttle middleware performs an atomic increment-and-check against your cache store. The file driver is neither atomic nor fast under concurrency. Two simultaneous requests can read the same counter, both pass, and your effective limit doubles. Under load, that race condition makes rate limiting unreliable.

Redis solves this with native atomic commands. INCR combined with EXPIRE runs as a single server-side operation. On projects like Nepal Gift Card and several legal portals, switching from file cache to Redis eliminated phantom 429 errors during traffic spikes. Limits now enforce precisely when they should. Pair this with Redis caching for Laravel so your infrastructure serves both throttling and application cache from one well-tuned instance.

Production Redis configuration

Set your cache driver to Redis in production:

# .env.production
CACHE_STORE=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

# Optional: separate DB index for rate limit keys
REDIS_CACHE_DB=1

Consider isolating rate limit keys from general application cache. Cache entries often carry longer TTLs. Under memory pressure, Redis may evict short-lived throttle keys first. That resets counters unexpectedly and lets abusers slip through. A dedicated Redis database index for throttling avoids this collision. For high-throughput APIs, read about custom rate limit keys in Laravel when multiple services share one Redis cluster.

Cache Backend for Rate LimitingFile Cache DriverRace conditions under loadNo atomic INCRSlow disk I/O per requestSingle-server onlyNot for production APIsRedis 8.10Atomic INCR + EXPIRESub-millisecond lookupsShared across app serversTTL auto-expires keysRequired for production
Laravel API rate limiting depends on atomic cache operations that only Redis provides reliably at scale.

How do you set tiered and endpoint-specific rate limits?

Static limits rarely match real usage. A free-tier consumer should not share the same ceiling as an enterprise partner. Laravel resolves limits dynamically at request time based on user attributes, API keys, or subscription tiers.

Tiered limits by subscription level

RateLimiter::for('partner-api', function (Request $request) {
    if (!$request->user()) {
        return Limit::perMinute(10)->by($request->ip());
    }

    $tier = $request->user()->api_tier;

    return match ($tier) {
        'enterprise' => Limit::perMinute(1000)->by($request->user()->id),
        'pro'        => Limit::perMinute(300)->by($request->user()->id),
        default      => Limit::perMinute(60)->by($request->user()->id),
    };
});

Centralizing tier logic in one closure beats scattering conditionals across controllers. When pricing changes, you update a single file. For legal-tech platforms with different entitlements for firms versus individuals, this pattern keeps billing and throughput aligned. Our Mijar Law Associates client portal uses similar tiered access for document operations.

Segment limits by endpoint cost

Not every route costs the same. A cached listing tolerates higher throughput than a PDF export that spawns queue workers:

  • Read-only listings: 200 requests per minute — cheap index lookups.
  • Write operations: 60 per minute — protects database write capacity.
  • Export and generation: 5 per minute — limits CPU and memory spikes.
  • Authentication routes: 5 per minute per IP — slows brute-force attempts.

Define separate named limiters for each category. Attach the appropriate one per route group. This prevents one expensive call from consuming a user's entire quota on cheap reads. The concept parallels token bucket and sliding window strategies used in gateway-level throttling, but Laravel's built-in limiter covers most application needs without extra packages.

Free Tier60 req/minGET routesPOST routesExport routesPro Tier300 req/minGET routesPOST routesExport routesEnterprise1000 req/minGET routesPOST routesExport routes
Tiered Laravel API rate limiting aligns throughput with subscription level and per-endpoint resource cost.

What HTTP headers and 429 responses should your API return?

Rate limiting only works when clients can react to it. HTTP 429 tells them to slow down. Standard headers tell them exactly how. Laravel's throttle middleware injects these automatically. Verify your reverse proxy or CDN forwards them unchanged.

Standard rate limit headers

HeaderDescriptionExample
X-RateLimit-LimitMaximum requests allowed in the window120
X-RateLimit-RemainingRequests remaining in current window87
Retry-AfterSeconds until window resets (sent with 429)34

Well-behaved clients read X-RateLimit-Remaining and throttle themselves before hitting the wall. On 429, Retry-After removes guesswork from backoff logic. The RFC 6585 specification defines 429 semantics that Laravel follows. Document these headers in your API reference. The guide on building REST APIs in Laravel the right way covers header conventions that pair well with throttling.

Custom JSON error bodies for 429

Mobile apps and third-party SDKs benefit from structured error payloads:

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(120)
        ->by($request->user()?->id ?: $request->ip())
        ->response(function (Request $request, array $headers) {
            return response()->json([
                'error' => 'rate_limit_exceeded',
                'message' => 'Too many requests. Please slow down.',
                'retry_after' => (int) ($headers['Retry-After'] ?? 60),
                'limit' => (int) ($headers['X-RateLimit-Limit'] ?? 0),
            ], 429, $headers);
        });
});

Include retry_after in the JSON body. Some HTTP clients strip headers on error responses. Duplicate the value so SDKs can parse it reliably. Use our JSON formatter tool to validate response payloads during development.

How do you test and monitor Laravel API rate limiting?

Rate limit configs fail silently. A typo in the limiter name, a missing Redis connection, or wrong middleware order leaves endpoints unprotected. Test explicitly. Monitor 429 rates in production.

Automated feature tests

public function test_api_respects_rate_limit(): void
{
    $user = User::factory()->create();

    for ($i = 0; $i < 120; $i++) {
        $this->actingAs($user, 'sanctum')
            ->getJson('/api/documents')
            ->assertOk();
    }

    $this->actingAs($user, 'sanctum')
        ->getJson('/api/documents')
        ->assertStatus(429)
        ->assertHeader('Retry-After')
        ->assertJsonPath('error', 'rate_limit_exceeded');
}

Use a dedicated Redis database in your test environment. Flush throttle keys in tearDown() to prevent flaky tests from leftover counters. For broader API security coverage, cross-reference the OWASP API Top 10 checklist and securing Laravel against OWASP risks.

Production monitoring

Track 429 response rates as an operational metric. A sudden spike signals an attack, a misconfigured client, or limits set too low for legitimate traffic. Log throttled requests with context:

  1. User ID or IP address that triggered the limit.
  2. Endpoint path and HTTP method.
  3. Configured limit and remaining count at time of block.
  4. User-Agent string to distinguish bots from real clients.

On legal-tech portals, I log auth throttling separately from general API throttling. Login spikes often mean credential stuffing. Document retrieval spikes may indicate scraping. Different events need different responses. For infrastructure teams in Nepal, cloud hosting options and pricing affects whether you run Redis on the same VPS or a managed instance with persistence enabled.

Laravel AppThrottle MiddlewareRedisAtomic CountersMetrics429 rate chartApp LogsIP and user contextAlertsThreshold triggersOps TeamAdjust limits
Monitor Laravel API rate limiting by correlating Redis counters, structured logs, and 429 alert thresholds.

Combine application-level throttling with edge protection where needed. Laravel middleware patterns let you layer IP blocklists, request signing, and throttle policies in a predictable stack. For queue-heavy endpoints, coordinate limits with Redis queue worker scaling so rejected requests do not pile up in retry loops.

Key Takeaways

  • Define named limiters in bootstrap/app.php and partition counters with by() — never share one bucket across all users.
  • Use Redis as your cache store in production; the file driver races under concurrent load.
  • Place auth middleware before throttle when limits depend on the authenticated user.
  • Segment limits by subscription tier and endpoint cost instead of applying one global cap.
  • Return structured 429 JSON with Retry-After in both headers and response body.
  • Write feature tests that hit the limit and monitor 429 rates as a production health signal.

People Also Ask

What is the default Laravel API rate limit?

Laravel's default API middleware group applies a limit of 60 requests per minute keyed by user ID or IP address. You override this by defining a custom named limiter and attaching it with throttle:your-limiter-name on route groups.

Can Laravel rate limiting work without Redis?

Yes, any cache driver works for development and low-traffic staging. Production APIs with concurrent requests need Redis or another atomic store. File and database drivers can race and double-count under parallel traffic.

How do you bypass rate limiting for internal services?

Check for a shared secret header or internal IP range inside your limiter closure and return Limit::none() for trusted callers. Keep the bypass list small and log every exempt request for audit purposes.

Does Cloudflare rate limiting replace Laravel throttling?

Cloudflare protects at the edge before traffic reaches your server. Laravel throttling protects application resources like database writes and queue dispatch. Use both layers — edge filtering for DDoS, application limits for business logic and per-user fairness.

Ship Rate Limiting Before Your Next Traffic Spike

Laravel API rate limiting is foundational infrastructure. Configure named limiters, back them with Redis, tier limits by user and endpoint, expose standard headers, and test the 429 path explicitly. Silent misconfigurations leave you exposed until a scraper or brute-force campaign proves it. Need an audit of your API security posture or tiered throttling for a production system? Explore API development services or contact us to discuss your project. For ongoing support after launch, reach out directly about maintenance and monitoring.

Frequently Asked Questions

Laravel 12 applies 60 requests per minute for authenticated API routes and no global limit for web routes by default, configured in bootstrap/app.php.

Modify the RateLimiter facade configuration inside bootstrap/app.php to adjust the global throttle middleware parameters or define custom named limiters for specific route groups.

Yes, it uses atomic file locks or cache drivers like Memcached, but Redis is required for accurate distributed throttling across multiple server instances in production.

Define a named limiter in bootstrap/app.php using RateLimiter::for that accepts the request object. You can then inspect auth()->user() within the closure to return different Limit objects based on subscription tier or role. Attach this named limiter to your API routes using the throttle:middleware-name syntax. This approach keeps logic centralized rather than scattering conditional checks across controllers, making policy changes safer and easier to audit during security reviews.

Laravel returns a 429 Too Many Requests HTTP status with Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining headers. The response body contains a JSON error message by default. Clients should respect the Retry-After header value before retrying. In my experience building REST APIs for Nepal-based services, properly documented 429 responses reduce support tickets significantly because developers understand exactly when they can resume requests without triggering further blocks.

Absolutely. Define multiple named limiters in bootstrap/app.php such as search-limiter, auth-limiter, and upload-limiter with distinct configurations. Apply them individually to route groups or single routes using the throttle:name syntax. For example, authentication endpoints might allow only five attempts per minute while search allows thirty. This granularity prevents aggressive scraping on sensitive endpoints without degrading performance for legitimate high-volume features on the same application.

The throttle middleware relies entirely on your configured cache driver to store attempt counters atomically. File and database caches work for single-server deployments but create race conditions under load. Redis provides true atomic increments necessary for accurate counting across horizontal scaling. On production Laravel applications I maintain, switching from file cache to Redis eliminated phantom 429 errors caused by stale lock files during concurrent request bursts. Always verify your CACHE_STORE matches your throttling requirements.

Sanctum itself has no built-in rate limiter; it depends entirely on the global or named throttle middleware you configure. Passport includes its own internal throttling for token issuance endpoints separate from your application routes. When integrating payment gateways like eSewa or Khalti where token refresh frequency matters, I typically configure stricter limits on Passport token routes while keeping standard API limits elsewhere. Both packages respect the same underlying cache infrastructure, so ensure your Redis connection is stable regardless of which authentication system you choose.

Use Laravel's fake rate limiter in tests via RateLimiter::fake() to assert hit counts without actual delays. For manual testing, temporarily lower the limit to two or three requests per minute in your local environment configuration. You can also override the limiter at runtime in tinker sessions. Never test against production limits during development as this risks blocking legitimate traffic. I keep a separate testing-limiter definition with minimal thresholds specifically for QA validation on staging environments.

Application-level throttling mitigates low-volume abuse but cannot stop volumetric DDoS attacks that saturate network bandwidth before reaching PHP. Rate limiting protects backend resources from exhausted connections and database overload during moderate spikes. For serious protection, combine Laravel throttling with Cloudflare, AWS WAF, or nginx limit_req directives at the edge. On legal-tech portals handling sensitive documents, I always layer infrastructure-level rate limiting above application middleware to ensure the PHP process pool remains available even during coordinated credential stuffing attempts.

Listen for the ThrottleRequests exception in your exception handler or use middleware events to capture violation metadata including IP, user ID, endpoint, and timestamp. Write these to a dedicated log channel or push to monitoring tools like Sentry. Avoid logging every successful request as this creates noise. In production systems processing thousands of daily API calls, I filter logs to only record repeated violations from the same source within short windows. This pattern surfaces genuine abuse patterns without bloating storage costs or obscuring actionable alerts.

Misconfiguring the cache driver is the most frequent issue; using file cache on multi-server setups causes inconsistent limiting. Another mistake is applying identical limits to all endpoints regardless of cost; expensive report generation needs stricter caps than simple lookups. Developers also forget that unauthenticated routes share the global limit unless explicitly separated. Finally, hardcoding values instead of using environment variables makes staging-to-production transitions painful. Always validate your throttle configuration matches both your infrastructure topology and business requirements before deploying.

Incoming webhooks from providers like Stripe, eSewa, or ConnectIPS must never be throttled or you risk missing critical payment confirmations. Exclude webhook routes from global throttle middleware entirely or assign them an extremely permissive named limiter. Validate webhook signatures instead of relying on rate limits for security. On eCommerce platforms I have built, accidental throttling of payment callbacks caused order fulfillment delays until we identified the misconfiguration. Always document excluded routes clearly so future maintainers understand why certain endpoints bypass standard protections.

Override the render method in your application exception handler to catch ThrottleRequests exceptions and return a custom JSON structure matching your API specification. Include machine-readable error codes alongside human messages for better client integration. Some teams add links to documentation explaining tier upgrades or contact information for higher limits. Maintain consistent envelope formatting with your other error responses. I standardize on RFC 7807 Problem Details format across Laravel APIs to ensure frontend and mobile clients handle throttling uniformly without special-case parsing logic.

Basic configuration takes two to four hours of senior developer time, roughly NPR 15,000 to 30,000 (USD 110 to 220). Adding Redis infrastructure adds NPR 1,000 to 3,000 monthly for managed hosting or self-hosted VPS allocation. Custom tiered limiting with monitoring integration may require eight to twelve hours depending on complexity. For small businesses starting out, the default configuration provides adequate protection at zero additional cost beyond initial setup. Budget more only when you need role-based policies, webhook exclusions, or compliance-driven audit logging for regulated sectors.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: