Security 13 min read Markdown

Locking Down Your Laravel API: Rate Limiting, Sanctum, and Abuse Prevention

API security goes far beyond authentication. Learn how to combine rate limiting strategies, Sanctum token scoping, request throttling, payload validation, and anomaly detection to protect your Laravel API from brute-force attacks, credential stuffing, and automated abuse.

Matt King
Matt King
July 30, 2026
Last updated: July 29, 2026
Locking Down Your Laravel API: Rate Limiting, Sanctum, and Abuse Prevention

Your Laravel API has authentication. Tokens are required. HTTPS is enforced. You might think you are covered, but authentication is only the front door. Without rate limiting, token scoping, payload validation, and monitoring, your API is still wide open to abuse.

Brute-force login attempts and credential stuffing are routine threats. Automated bots hammer API endpoints millions of times per day across the internet. A single unprotected endpoint can be the entry point for data exfiltration, account takeover, or denial-of-service.

This guide covers the practical steps to lock down a Laravel API beyond basic authentication. Every example is production-tested and ready to drop into your codebase.


The Threat Landscape for Laravel APIs

APIs are the primary attack surface for modern web applications. Unlike traditional web pages, APIs are designed for programmatic access, which makes them inherently easier to automate attacks against.

Common threats include:

  • Credential stuffing. Attackers use lists of stolen credentials from other breaches to attempt logins on your API. If any of your users reuse passwords, those accounts get compromised.
  • Brute-force attacks. Systematic attempts to guess passwords, API keys, or token values.
  • Enumeration attacks. Probing endpoints like /api/users/1, /api/users/2, etc. to discover valid resources.
  • Denial-of-service. Flooding your API with requests to exhaust server resources.
  • Data scraping. Automated extraction of data through legitimate endpoints at a scale you never intended.

Laravel gives you excellent tools to handle all of these. The challenge is configuring them correctly.


Rate Limiting Strategies

Laravel's rate limiter is flexible enough to handle simple per-IP limits and complex tiered strategies based on user plans.

Basic Rate Limiting Setup

Define your rate limiters in App\Providers\AppServiceProvider (or RouteServiceProvider in older Laravel versions):

// app/Providers/AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;

public function boot(): void
{
    // General API rate limit
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by(
            $request->user()?->id ?: $request->ip()
        );
    });

    // Strict limit for authentication endpoints
    RateLimiter::for('auth', function (Request $request) {
        return Limit::perMinute(5)->by(
            $request->ip() . '|' . $request->input('email', '')
        );
    });

    // Strict limit for sensitive operations
    RateLimiter::for('sensitive', function (Request $request) {
        return Limit::perMinute(10)->by(
            $request->user()?->id ?: $request->ip()
        );
    });
}

The key detail here is the by() method. For authentication endpoints, keying by both IP and email prevents an attacker from distributing attempts across multiple IPs to bypass per-IP limits.

Tiered Rate Limiting by Plan

If your application has different subscription tiers, your rate limits should reflect that:

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

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

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

Per-Endpoint Rate Limiting

Some endpoints deserve their own limits. Password reset, two-factor verification, and file upload endpoints should all have tighter restrictions:

// routes/api.php
Route::post('/forgot-password', ForgotPasswordController::class)
    ->middleware('throttle:auth');

Route::post('/verify-2fa', TwoFactorController::class)
    ->middleware('throttle:sensitive');

Route::post('/upload', FileUploadController::class)
    ->middleware('throttle:sensitive');

Returning Useful Rate Limit Headers

Laravel automatically adds X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After headers. Make sure your API consumers know about these. If you need custom headers, add middleware:

// app/Http/Middleware/RateLimitHeaders.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class RateLimitHeaders
{
    public function handle(Request $request, Closure $next): mixed
    {
        $response = $next($request);

        if ($request->user()) {
            $plan = $request->user()->plan ?? 'free';
            $response->headers->set('X-RateLimit-Plan', $plan);
        }

        return $response;
    }
}

Sanctum Token Security

Laravel Sanctum is the default API authentication package for most Laravel applications. Out of the box it works well, but there are several hardening steps you should take.

Token Scoping

Never issue tokens with full access when limited access will do:

// Creating a scoped token
$token = $user->createToken('mobile-app', [
    'read:profile',
    'read:orders',
    'write:orders',
]);

// Creating a read-only integration token
$integrationToken = $user->createToken('reporting-webhook', [
    'read:analytics',
]);

Check scopes in your controllers:

public function update(Request $request, Order $order): JsonResponse
{
    if (! $request->user()->tokenCan('write:orders')) {
        abort(403, 'Token does not have write:orders scope.');
    }

    // Process the update
    $order->update($request->validated());

    return response()->json($order);
}

Or use middleware for cleaner route definitions:

Route::middleware(['auth:sanctum', 'ability:write:orders'])
    ->put('/orders/{order}', [OrderController::class, 'update']);

Token Rotation

Tokens should not live forever. Implement automatic expiration and rotation:

// config/sanctum.php
'expiration' => 60 * 24 * 30, // 30 days in minutes

For higher security, implement manual rotation:

// app/Http/Controllers/Api/TokenController.php
public function rotate(Request $request): JsonResponse
{
    $currentToken = $request->user()->currentAccessToken();
    $abilities = $currentToken->abilities;

    // Delete the old token
    $currentToken->delete();

    // Issue a new one with the same scopes
    $newToken = $request->user()->createToken(
        'rotated-' . now()->timestamp,
        $abilities
    );

    return response()->json([
        'token' => $newToken->plainTextToken,
        'expires_at' => now()->addDays(30)->toIso8601String(),
    ]);
}

Pruning Expired Tokens

Add a scheduled command to clean up expired tokens. This reduces the attack surface if your database is ever compromised:

// app/Console/Kernel.php or routes/console.php
use Laravel\Sanctum\Sanctum;

Schedule::command('sanctum:prune-expired --hours=48')->daily();

Request Payload Validation

Validation is a security control, not just a UX feature. Every API endpoint should validate every field.

Strict Validation Rules

// app/Http/Requests/Api/StoreOrderRequest.php
namespace App\Http\Requests\Api;

use Illuminate\Foundation\Http\FormRequest;

class StoreOrderRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'product_id'  => ['required', 'integer', 'exists:products,id'],
            'quantity'     => ['required', 'integer', 'min:1', 'max:100'],
            'notes'        => ['nullable', 'string', 'max:500'],
            'coupon_code'  => ['nullable', 'string', 'alpha_num', 'size:8'],
        ];
    }
}

Key principles:

  • Always specify the data type (string, integer, array).
  • Always set maximum lengths. A missing max on a string field means an attacker can submit megabytes of data.
  • Use exists rules to validate foreign keys rather than letting database errors handle invalid references.
  • Use alpha_num, regex, or in rules to restrict values to expected formats.

Rejecting Unexpected Fields

By default, Laravel ignores fields that are not in your validation rules. But you should still be explicit about what you accept. Use $request->validated() instead of $request->all():

// Good: only uses validated fields
$order = Order::create($request->validated());

// Dangerous: includes any field the client sends
$order = Order::create($request->all());

Request Size Limits

Set maximum request body sizes in your nginx configuration to prevent oversized payloads from reaching PHP:

# /etc/nginx/sites-available/your-app.conf
server {
    client_max_body_size 10m;

    location /api/upload {
        client_max_body_size 50m;
    }

    location /api/ {
        client_max_body_size 2m;
    }
}

Middleware Chains for Defense in Depth

The real power of Laravel API security comes from stacking middleware. Each layer catches a different type of attack:

// routes/api.php
Route::prefix('v1')->middleware([
    'throttle:api',
    'auth:sanctum',
    \App\Http\Middleware\ValidateJsonContentType::class,
    \App\Http\Middleware\LogApiRequest::class,
])->group(function () {

    Route::prefix('orders')->group(function () {
        Route::get('/', [OrderController::class, 'index'])
            ->middleware('ability:read:orders');

        Route::post('/', [OrderController::class, 'store'])
            ->middleware(['ability:write:orders', 'throttle:sensitive']);

        Route::delete('/{order}', [OrderController::class, 'destroy'])
            ->middleware(['ability:delete:orders', 'throttle:sensitive']);
    });
});

JSON Content-Type Validation

Reject requests that claim to send JSON but do not:

// app/Http/Middleware/ValidateJsonContentType.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class ValidateJsonContentType
{
    public function handle(Request $request, Closure $next): mixed
    {
        if ($request->isMethod('POST') || $request->isMethod('PUT') || $request->isMethod('PATCH')) {
            if (! $request->isJson()) {
                return response()->json([
                    'error' => 'Content-Type must be application/json',
                ], 415);
            }
        }

        return $next($request);
    }
}

API Request Logging

Log every API request for security monitoring:

// app/Http/Middleware/LogApiRequest.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class LogApiRequest
{
    public function handle(Request $request, Closure $next): mixed
    {
        $response = $next($request);

        Log::channel('api')->info('API Request', [
            'method'    => $request->method(),
            'path'      => $request->path(),
            'status'    => $response->getStatusCode(),
            'ip'        => $request->ip(),
            'user_id'   => $request->user()?->id,
            'user_agent' => $request->userAgent(),
            'duration'  => defined('LARAVEL_START')
                ? round((microtime(true) - LARAVEL_START) * 1000, 2)
                : null,
        ]);

        return $response;
    }
}

API Versioning for Security

API versioning is not just about backwards compatibility. It is a security practice that gives you the freedom to ship breaking security fixes without disrupting existing clients.

URL-Based Versioning

The simplest approach:

// routes/api.php
Route::prefix('v1')->middleware('throttle:api')->group(function () {
    Route::apiResource('orders', V1\OrderController::class);
});

Route::prefix('v2')->middleware('throttle:api')->group(function () {
    Route::apiResource('orders', V2\OrderController::class);
});

Deprecation Headers

When you need to retire an insecure API version, give clients notice:

// app/Http/Middleware/ApiDeprecation.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class ApiDeprecation
{
    public function handle(Request $request, Closure $next): mixed
    {
        $response = $next($request);

        $response->headers->set('Deprecation', 'true');
        $response->headers->set('Sunset', 'Sat, 01 Nov 2026 00:00:00 GMT');
        $response->headers->set('Link', '</api/v2>; rel="successor-version"');

        return $response;
    }
}

Anomaly Detection and Monitoring

Rate limiting catches obvious abuse. Monitoring catches the subtle attacks that fly under rate limits.

Detecting Enumeration Attacks

Watch for sequential resource access patterns:

// app/Http/Middleware/DetectEnumeration.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class DetectEnumeration
{
    public function handle(Request $request, Closure $next): mixed
    {
        $response = $next($request);

        if ($response->getStatusCode() === 404) {
            $key = 'enumeration:' . $request->ip();
            $count = Cache::increment($key);

            if ($count === 1) {
                Cache::put($key, 1, now()->addMinutes(5));
            }

            if ($count > 20) {
                Log::channel('security')->warning('Possible enumeration attack', [
                    'ip'    => $request->ip(),
                    'path'  => $request->path(),
                    'count' => $count,
                ]);
            }
        }

        return $response;
    }
}

Monitoring Authentication Patterns

Track failed authentication attempts and alert on anomalies:

// app/Listeners/LogFailedLogin.php
namespace App\Listeners;

use Illuminate\Auth\Events\Failed;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class LogFailedLogin
{
    public function handle(Failed $event): void
    {
        $ip = request()->ip();
        $email = $event->credentials['email'] ?? 'unknown';

        // Track per-IP failures
        $ipKey = "auth_failures:ip:{$ip}";
        $ipCount = Cache::increment($ipKey);
        if ($ipCount === 1) {
            Cache::put($ipKey, 1, now()->addMinutes(15));
        }

        // Track per-email failures
        $emailKey = "auth_failures:email:{$email}";
        $emailCount = Cache::increment($emailKey);
        if ($emailCount === 1) {
            Cache::put($emailKey, 1, now()->addMinutes(15));
        }

        Log::channel('security')->warning('Failed login attempt', [
            'ip'          => $ip,
            'email'       => $email,
            'ip_failures' => $ipCount,
            'email_failures' => $emailCount,
        ]);

        if ($ipCount > 20) {
            Log::channel('security')->critical('Possible brute-force from IP', [
                'ip' => $ip,
                'attempts' => $ipCount,
            ]);
        }

        if ($emailCount > 10) {
            Log::channel('security')->critical('Possible targeted attack on account', [
                'email' => $email,
                'attempts' => $emailCount,
            ]);
        }
    }
}

Security Headers for API Responses

Even API responses should include security headers. Add them globally:

// app/Http/Middleware/ApiSecurityHeaders.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class ApiSecurityHeaders
{
    public function handle(Request $request, Closure $next): mixed
    {
        $response = $next($request);

        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'DENY');
        $response->headers->set('Cache-Control', 'no-store');
        $response->headers->set('Pragma', 'no-cache');

        return $response;
    }
}

You can verify your security headers are configured correctly using the StackShield Header Check tool.


Putting It All Together: A Secure API Route File

Here is a complete example that combines everything discussed:

// routes/api.php
use App\Http\Controllers\Api\V1;
use Illuminate\Support\Facades\Route;

Route::prefix('v1')->middleware([
    'throttle:api',
    \App\Http\Middleware\ValidateJsonContentType::class,
    \App\Http\Middleware\ApiSecurityHeaders::class,
    \App\Http\Middleware\LogApiRequest::class,
])->group(function () {

    // Public (unauthenticated) routes with strict limits
    Route::middleware('throttle:auth')->group(function () {
        Route::post('/login', [V1\AuthController::class, 'login']);
        Route::post('/forgot-password', [V1\AuthController::class, 'forgotPassword']);
    });

    // Authenticated routes
    Route::middleware('auth:sanctum')->group(function () {

        Route::get('/user', [V1\UserController::class, 'show'])
            ->middleware('ability:read:profile');

        Route::apiResource('orders', V1\OrderController::class)
            ->middleware('ability:read:orders,write:orders');

        Route::post('/token/rotate', [V1\TokenController::class, 'rotate']);

        Route::delete('/token', [V1\TokenController::class, 'revoke']);
    });
});

Nginx Hardening for API Endpoints

Add these nginx configurations to complement your Laravel-level protections:

# /etc/nginx/snippets/api-security.conf

# Limit request rate at the nginx level (backup for Laravel rate limiting)
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;

server {
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        limit_req_status 429;

        # Reject requests without proper content type
        if ($request_method ~* "POST|PUT|PATCH") {
            set $check_content_type "Y";
        }
        if ($http_content_type !~* "application/json") {
            set $check_content_type "${check_content_type}N";
        }
        if ($check_content_type = "YN") {
            return 415;
        }
    }

    location /api/login {
        limit_req zone=auth burst=3 nodelay;
        limit_req_status 429;
    }

    # Block common attack patterns
    location ~* /api/.*\.(php|aspx|jsp|cgi)$ {
        return 404;
    }
}

Your API Security Checklist

Use this checklist to audit your Laravel API:

  1. Rate limiting is configured per-endpoint, with stricter limits on auth endpoints. Check your security configuration.
  2. Sanctum tokens are scoped to minimum required abilities.
  3. Token expiration is set and expired tokens are pruned regularly.
  4. Request validation uses Form Requests with explicit type and size constraints.
  5. Only validated data is used in database operations (never $request->all()).
  6. Security headers are present on all API responses. Test yours with our header check tool.
  7. Request logging captures method, path, status, IP, and user ID.
  8. Anomaly detection monitors for enumeration, brute-force, and credential stuffing patterns.
  9. API versioning is in place so security fixes can ship without breaking clients.
  10. Nginx-level rate limiting provides a backup layer outside of PHP.

Want to know if your API endpoints are leaking information or missing protections? Run a free StackShield scan to check your application's external security posture in under 60 seconds. For ongoing monitoring, check out our pricing plans.


API security is not a feature you ship once. It is a practice you maintain. Every new endpoint, every new token, every new integration is a potential attack surface. Build your defenses in layers, monitor continuously, and assume that someone is already testing your limits.

Free security check

Is your Laravel app exposed right now?

34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.

18% have debug mode on
72% missing security headers
12% have exposed .env
Scan My App Free No signup required. Results in 60 seconds.

Frequently Asked Questions

What is the difference between rate limiting and throttling in Laravel?

Rate limiting and throttling are closely related but serve slightly different purposes. Rate limiting sets a hard cap on how many requests a client can make within a time window. Once the limit is hit, subsequent requests receive a 429 Too Many Requests response. Throttling is a broader concept that can include rate limiting but also encompasses slowing down responses, queuing requests, or degrading service quality for abusive clients. In Laravel, the RateLimiter facade and throttle middleware handle both concepts. You define rate limits in your RouteServiceProvider or AppServiceProvider, and the throttle middleware enforces them at the route or group level.

How should I scope Sanctum tokens for API security?

Sanctum token scoping lets you limit what each API token can do. When creating a token, pass an array of abilities like ["read:orders", "write:orders"]. In your controllers or middleware, check abilities using $request->user()->tokenCan("write:orders"). Always follow the principle of least privilege. A token used by a mobile app to display data should only have read scopes. A token used for webhooks should only have the specific write scopes it needs. Avoid issuing tokens with wildcard or full-access scopes unless absolutely necessary.

How do I prevent credential stuffing attacks on my Laravel API?

Credential stuffing uses stolen username and password pairs from data breaches to attempt logins on your application. To defend against it, implement aggressive rate limiting on your login endpoint, typically 5 to 10 attempts per IP per minute. Add exponential backoff by increasing the lockout duration after repeated failures. Use Laravel rate limiting keyed by both IP and email to prevent distributed attacks. Consider adding CAPTCHA after a few failed attempts. Monitor for patterns like many failed logins from different IPs targeting the same account, which suggests a targeted attack. StackShield can help you detect exposed authentication endpoints and missing rate limits with a free scan at /free-scan.

Should I version my API for security reasons?

Yes. API versioning is a security practice, not just a developer convenience. When you discover a vulnerability in an endpoint, you may need to change request validation, response structure, or authentication requirements to fix it. Without versioning, these changes can break existing clients, which creates pressure to delay security fixes. With versioned endpoints like /api/v1/ and /api/v2/, you can ship a secure version immediately while giving clients time to migrate. Always deprecate and eventually remove insecure API versions rather than maintaining them indefinitely.

What should I log for API security monitoring?

At minimum, log every authentication attempt (success and failure), every rate limit hit, every validation failure, and every request to sensitive endpoints. Include the IP address, user agent, authenticated user ID, endpoint, HTTP method, response status code, and timestamp. Avoid logging sensitive data like passwords, tokens, or full request bodies containing personal information. Store logs in a centralised system and set up alerts for anomalies such as spikes in 429 responses, unusual geographic patterns, or sudden increases in authentication failures. Laravel makes this straightforward with custom middleware that writes to a dedicated security log channel.

Stay Updated on Laravel Security

Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.