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.
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
maxon a string field means an attacker can submit megabytes of data. - Use
existsrules to validate foreign keys rather than letting database errors handle invalid references. - Use
alpha_num,regex, orinrules 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:
- Rate limiting is configured per-endpoint, with stricter limits on auth endpoints. Check your security configuration.
- Sanctum tokens are scoped to minimum required abilities.
- Token expiration is set and expired tokens are pruned regularly.
- Request validation uses Form Requests with explicit type and size constraints.
- Only validated data is used in database operations (never
$request->all()). - Security headers are present on all API responses. Test yours with our header check tool.
- Request logging captures method, path, status, IP, and user ID.
- Anomaly detection monitors for enumeration, brute-force, and credential stuffing patterns.
- API versioning is in place so security fixes can ship without breaking clients.
- 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.
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.
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.
Related Security Terms
Related Articles
Laravel Session Security: Cookies, Hijacking & config/session.php
A deep dive into Laravel session security. Learn how cookie flags, session drivers, and config/session.php settings protect against hijacking, fixation, and sidejacking attacks.
SecurityAutomated Security Testing in Laravel CI/CD Pipelines
How to add security gates to your Laravel CI/CD pipeline with GitHub Actions. Covers dependency scanning, static analysis, secret detection, and automated security monitoring.
SecurityLaravel Content Security Policy: Configure CSP Without Breaking Your App
Only 22% of Laravel apps have a Content Security Policy. Learn how to implement CSP with spatie/laravel-csp, handle Livewire and Vite nonces, and avoid the mistakes that break production.
Compare StackShield
Security Checklists
Laravel Production Deployment Security Checklist
A comprehensive security checklist for deploying Laravel applications to production. Covers environment config, server hardening, access control, and monitoring.
20 itemsLaravel API Security Checklist
Secure your Laravel API endpoints against common vulnerabilities. Covers authentication, input validation, rate limiting, and response security.
Stay Updated on Laravel Security
Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.