0% read
Skip to main content
Implementing Smart Rate Limiting in Laravel - Patterns & Examples

Implementing Smart Rate Limiting in Laravel - Patterns & Examples

Learn how to implement sophisticated rate limiting strategies in Laravel to protect your APIs and forms from abuse while maintaining great UX.

S
StaticBlock Editorial
12 min read

Why Rate Limiting Matters

Rate limiting isn't just about preventing abuse—it's about ensuring fair resource allocation and maintaining service quality for all users. A well-implemented rate limiting strategy protects your application while being invisible to legitimate users.

Laravel's Built-In Rate Limiting

Laravel provides excellent rate limiting out of the box through the throttle middleware:

// routes/web.php
Route::post('/contact', [ContactController::class, 'store'])
    ->middleware('throttle:5,1'); // 5 requests per minute

This works great for simple cases, but real-world applications often need more sophisticated approaches.

Advanced Rate Limiting Patterns

1. Dynamic Rate Limits Based on User Type

// app/Providers/RouteServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot() { RateLimiter::for('api', function (Request $request) { return $request->user()?->isPremium() ? Limit::perMinute(1000)->by($request->user()->id) : Limit::perMinute(60)->by($request->ip()); }); }

2. Sliding Window Rate Limiting

RateLimiter::for('contact-form', function (Request $request) {
    return [
        Limit::perMinute(5)->by($request->ip()),
        Limit::perHour(20)->by($request->ip()),
        Limit::perDay(50)->by($request->ip()),
    ];
});

3. Custom Response for Rate Limit Exceeded

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

use Closure; use Illuminate\Http\Request; use Illuminate\Routing\Middleware\ThrottleRequests;

class CustomThrottle extends ThrottleRequests { protected function buildException($request, $key, $maxAttempts, $responseCallback = null) { $retryAfter = $this->getTimeUntilNextRetry($key);

    if ($request->expectsJson()) {
        return response()->json([
            'message' => 'Too many requests. Please slow down.',
            'retry_after' => $retryAfter,
        ], 429);
    }

    return back()->with('error', "Please wait {$retryAfter} seconds before trying again.");
}

}

4. Honeypot + Rate Limiting Combo

Combine rate limiting with honeypot fields for robust spam protection:

// app/Http/Controllers/ContactController.php
public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string|max:100',
        'email' => 'required|email',
        'message' => 'required|string|max:2000',
        'hp' => 'max:0', // honeypot - should be empty
    ]);
// If honeypot is filled, silently fail
if (!empty($request->input('hp'))) {
    \Log::warning('Honeypot triggered', [
        'ip' => $request->ip(),
        'user_agent' => $request->userAgent(),
    ]);

    return back()->with('status', 'Message sent!'); // Fake success
}

// Process legitimate submission
// ...

}

<!-- resources/views/contact.blade.php -->
<form method="POST" action="/contact">
    @csrf

    <!-- Honeypot: hidden from humans, visible to bots -->
    <input type="text"
           name="hp"
           value=""
           style="display:none"
           tabindex="-1"
           autocomplete="off">

    <!-- Real fields... -->
</form>

5. Redis-Backed Rate Limiting for Distributed Systems

// config/cache.php - ensure Redis is configured

// app/Providers/RouteServiceProvider.php RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60) ->by($request->user()?->id ?: $request->ip()) ->response(function (Request $request, array $headers) { return response('Rate limit exceeded', 429, $headers); }); });

Testing Rate Limits

Always test your rate limiting logic:

// tests/Feature/RateLimitingTest.php
namespace Tests\Feature;

use Tests\TestCase;

class RateLimitingTest extends TestCase { public function test_contact_form_rate_limiting() { // Submit 5 forms (at the limit) for ($i = 0; $i < 5; $i++) { $response = $this->post('/contact', [ 'name' => "User $i", 'email' => "user$i@example.com", 'message' => 'Test message', 'hp' => '', ]);

        $response-&gt;assertStatus(302);
    }

    // 6th request should be rate limited
    $response = $this-&gt;post('/contact', [
        'name' =&gt; 'User 6',
        'email' =&gt; 'user6@example.com',
        'message' =&gt; 'Test message',
        'hp' =&gt; '',
    ]);

    $response-&gt;assertStatus(429);
}

public function test_honeypot_rejects_spam()
{
    $response = $this-&gt;post('/contact', [
        'name' =&gt; 'Spammer',
        'email' =&gt; 'spam@example.com',
        'message' =&gt; 'Spam message',
        'hp' =&gt; 'filled', // honeypot filled
    ]);

    $response-&gt;assertStatus(302);
    $response-&gt;assertSessionHasErrors('hp');
}

}

Best Practices

  1. Be Generous: Start with higher limits and tighten based on actual abuse patterns
  2. Inform Users: Show clear feedback when limits are approached
  3. Log Violations: Track repeated violations for potential blocking
  4. Use Multiple Strategies: Combine rate limiting with honeypots, CAPTCHA, and validation
  5. Consider Edge Cases: What about shared IPs? Legitimate high-volume users?

Monitoring & Alerting

Track rate limit hits in your monitoring system:

RateLimiter::for('api', function (Request $request) {
    $limit = Limit::perMinute(60)->by($request->ip());
// Log when limits are hit
if (RateLimiter::tooManyAttempts($request-&gt;ip(), $limit-&gt;maxAttempts)) {
    \Log::warning('Rate limit exceeded', [
        'ip' =&gt; $request-&gt;ip(),
        'endpoint' =&gt; $request-&gt;path(),
    ]);
}

return $limit;

});

IP Blacklisting for Repeat Offenders

For persistent abusers, implement automatic IP blacklisting:

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

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

class BlockAbusiveIPs { public function handle(Request $request, Closure $next) { $ip = $request->ip();

    // Check if IP is blacklisted
    if (Cache::has(&quot;blocked_ip:{$ip}&quot;)) {
        abort(403, 'Your IP has been blocked due to abusive behavior.');
    }

    // Track violations
    $violations = Cache::get(&quot;violations:{$ip}&quot;, 0);

    // If rate limit exceeded, increment violations
    if ($request-&gt;headers-&gt;has('X-RateLimit-Exceeded')) {
        $violations++;
        Cache::put(&quot;violations:{$ip}&quot;, $violations, now()-&gt;addHour());

        // Block after 5 violations in an hour
        if ($violations &gt;= 5) {
            Cache::put(&quot;blocked_ip:{$ip}&quot;, true, now()-&gt;addDay());
            \Log::warning(&quot;IP blocked for repeated violations&quot;, ['ip' =&gt; $ip]);
            abort(403, 'Your IP has been blocked.');
        }
    }

    return $next($request);
}

}

Register the middleware:

// app/Http/Kernel.php
protected $middlewareGroups = [
    'api' => [
        'throttle:api',
        \App\Http\Middleware\BlockAbusiveIPs::class,
        // ...
    ],
];

Rate Limiting Per-Route with Different Strategies

Different endpoints need different strategies:

// routes/api.php
Route::middleware(['auth:sanctum'])->group(function () {
    // Expensive operations: very restrictive
    Route::post('/reports/generate', [ReportController::class, 'generate'])
        ->middleware('throttle:1,60'); // 1 per hour
// Read operations: generous
Route::get('/posts', [PostController::class, 'index'])
    -&gt;middleware('throttle:300,1'); // 300 per minute

// Write operations: moderate
Route::post('/posts', [PostController::class, 'store'])
    -&gt;middleware('throttle:10,1'); // 10 per minute

});

Handling Legitimate High-Volume Clients

For known integrations that need higher limits:

// app/Providers/RouteServiceProvider.php
RateLimiter::for('api', function (Request $request) {
    // Check for API key
    if ($apiKey = $request->bearerToken()) {
        $client = ApiClient::where('key', $apiKey)->first();
    if ($client &amp;&amp; $client-&gt;isWhitelisted()) {
        return Limit::none(); // No rate limit
    }

    if ($client &amp;&amp; $client-&gt;tier === 'enterprise') {
        return Limit::perMinute(10000)-&gt;by($client-&gt;id);
    }
}

// Default for everyone else
return Limit::perMinute(60)-&gt;by($request-&gt;ip());

});

Real-World Production Considerations

1. Shared IPs and Corporate Networks

Corporate networks often share single IPs across thousands of users:

RateLimiter::for('web', function (Request $request) {
    // If authenticated, rate limit by user ID
    if ($request->user()) {
        return Limit::perMinute(60)->by($request->user()->id);
    }
// For guests, use fingerprinting or more generous IP limits
return Limit::perMinute(100)-&gt;by($request-&gt;ip());

});

2. CDN and Proxy Considerations

When behind Cloudflare or AWS CloudFront, get the real IP:

// config/trustedproxy.php
return [
    'proxies' => '*', // Trust all proxies (be careful in production)
    'headers' => [
        Request::HEADER_X_FORWARDED_FOR,
        Request::HEADER_X_FORWARDED_HOST,
        Request::HEADER_X_FORWARDED_PORT,
        Request::HEADER_X_FORWARDED_PROTO,
    ],
];

Then rate limit by the real client IP:

RateLimiter::for('api', function (Request $request) {
    $clientIp = $request->getClientIp(); // Gets real IP behind proxy
    return Limit::perMinute(60)->by($clientIp);
});

3. Graceful Degradation

Instead of hard blocking, consider degrading service:

public function index(Request $request)
{
    $ip = $request->ip();
    $attempts = RateLimiter::attempts($ip);
// Reduce result quality as load increases
$limit = match(true) {
    $attempts &lt; 50 =&gt; 100,  // Full results
    $attempts &lt; 100 =&gt; 50,  // Reduced results
    $attempts &lt; 200 =&gt; 20,  // Minimal results
    default =&gt; 5,           // Severely limited
};

return Post::latest()-&gt;limit($limit)-&gt;get();

}

Conclusion

Smart rate limiting is about balance. Too strict, and you frustrate legitimate users. Too lenient, and you're vulnerable to abuse. Start conservative, monitor real-world usage, and adjust based on data.

The combination of Laravel's built-in middleware, custom logic, and honeypot fields provides robust protection without compromising user experience.


What rate limiting strategies have worked well for your applications? Share your experiences in the comments.

Found this helpful? Share it!

Related Articles

S

Written by StaticBlock Editorial

StaticBlock Editorial is a technical writer and software engineer specializing in web development, performance optimization, and developer tooling.