
August 14, 2026
9 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Unprotected endpoints invite abuse, inflated infrastructure bills, and degraded service for legitimate users. Implementing effective API rate limiting with token bucket and sliding window algorithms is the standard engineering defense against these operational risks. Before you write custom middleware or deploy a third-party gateway, read this practical guide to Laravel API best practices that covers the exact implementation patterns I use in production.
How does API rate limiting with token bucket and sliding window actually work?
Rate limiting is not a single algorithm; it is a family of strategies chosen based on traffic shape and business tolerance. Understanding the mechanical difference prevents misconfiguration that either blocks valid users during spikes or fails to stop sustained abuse.
The token bucket algorithm maintains a counter representing available permits. Tokens refill at a constant rate up to a maximum capacity. Each request consumes one token; if the bucket is empty, the request is rejected. This naturally absorbs short traffic spikes without failing legitimate bursty clients, making it ideal for user-facing APIs where interaction patterns are uneven.
The sliding window log or counter tracks individual requests within a rolling timeframe. Unlike fixed windows that reset abruptly at minute boundaries (causing double-rate bursts at edges), sliding windows interpolate between current and previous periods. This provides mathematically smoother enforcement but requires more storage operations per request. For financial or legal-tech portals where compliance demands exact adherence to stated limits, sliding window eliminates edge-case overages that token buckets might permit.
When should you choose token bucket over sliding window for Laravel APIs?
Algorithm selection depends on your traffic profile and business constraints, not theoretical purity. In my experience building legal-tech portals and eCommerce systems, the choice typically follows these patterns:
- Token bucket when client applications have legitimate burst needs (mobile apps reconnecting, dashboard widgets loading simultaneously, webhook receivers processing queued events). The burst capacity acts as a buffer against network jitter.
- Sliding window when regulatory or contractual obligations require exact limit enforcement (payment processing APIs, document verification services, partner integrations with SLA-defined quotas). No burst tolerance means no ambiguity in billing or compliance reporting.
- Hybrid approach for multi-tier systems: token bucket at the edge (Nginx/Cloudflare) for DDoS protection, sliding window at the application layer for business logic enforcement. This separates infrastructure concerns from domain rules.
| Criteria | Token Bucket | Sliding Window |
|---|---|---|
| Burst handling | Allowed up to capacity | Strictly prevented |
| Redis operations per request | 1–2 (GET + SET or Lua script) | 2–4 (ZADD + ZRANGEBYSCORE + ZREMRANGEBYSCORE + EXPIRE) |
| Memory usage | O(1) per key | O(N) where N = requests in window |
| Implementation complexity | Low (native Laravel support) | Moderate (custom Lua or package) |
| Best for | User-facing APIs, mobile backends | Billing APIs, compliance-bound endpoints |
| Laravel 12 built-in driver | Yes (token_bucket) | No (use sliding_window community package or custom) |
For most Laravel projects I ship, token bucket via Laravel's native cache driver covers 90% of cases. I reserve sliding window for specific endpoints where a client contract explicitly states "exactly 100 requests per minute, no exceptions." Over-engineering with sliding window everywhere increases Redis load and debugging complexity without meaningful benefit.
How do you implement token bucket rate limiting in Laravel 12 with Redis?
Laravel 12 includes first-class token bucket support through the cache system. This requires PHP 8.2+ and Redis 7.x (7.4 recommended for performance). Never use the file or array cache drivers for rate limiting in production—they lack atomicity across multiple workers and will fail under concurrent load.
Configure Redis as the rate limiter store
<?php
// config/cache.php
'default' => env('CACHE_STORE', 'redis'),
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => env('CACHE_REDIS_CONNECTION', 'default'),
'lock_connection' => env('CACHE_REDIS_LOCK_CONNECTION', 'default'),
],
], Define rate limiter in RouteServiceProvider
<?php
// app/Providers/AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
// Token bucket: 60 requests per minute, burst up to 20
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)
->by($request->user()?->id ?: $request->ip())
->response(function () {
return response()->json([
'message' => 'Too many requests.',
], 429);
});
});
// Stricter limit for auth endpoints
RateLimiter::for('auth', function (Request $request) {
return Limit::perMinute(10)->by($request->ip());
});
} Apply middleware to routes
// routes/api.php
Route::middleware(['throttle:api'])->group(function () {
Route::get('/documents', [DocumentController::class, 'index']);
Route::post('/documents', [DocumentController::class, 'store']);
});
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:auth'); Laravel's throttle middleware uses an atomic Lua script internally when Redis is configured. This prevents race conditions where two concurrent requests both read "1 token remaining" and both proceed. Always verify your Redis connection is dedicated for caching—sharing with session or queue connections risks eviction under memory pressure. Set maxmemory-policy volatile-lru in redis.conf so rate limit keys expire predictably rather than evicting critical data.
How do you build a custom sliding window rate limiter in Laravel?
Laravel's built-in throttle doesn't support true sliding window counters. For endpoints requiring exact rolling-window enforcement, implement a custom solution using Redis sorted sets. This pattern stores each request timestamp as a score, enabling O(log N) range queries and automatic expiration.
Create the sliding window service
<?php
namespace App\Services;
use Illuminate\Support\Facades\Redis;
class SlidingWindowRateLimiter
{
public function attempt(string $key, int $limit, int $windowSeconds): bool
{
$now = microtime(true);
$windowStart = $now - $windowSeconds;
$redisKey = 'rate:' . $key;
// Atomic pipeline: remove expired, add current, count, set TTL
$result = Redis::pipeline(function ($pipe) use ($redisKey, $now, $windowStart, $windowSeconds) {
$pipe->zremrangebyscore($redisKey, '-inf', $windowStart);
$pipe->zadd($redisKey, $now, $now . ':' . uniqid('', true));
$pipe->zcard($redisKey);
$pipe->expire($redisKey, $windowSeconds);
});
$count = $result[2];
return $count <= $limit;
}
public function getRemaining(string $key, int $limit, int $windowSeconds): int
{
$now = microtime(true);
$windowStart = $now - $windowSeconds;
$count = Redis::zcount('rate:' . $key, $windowStart, '+inf');
return max(0, $limit - $count);
}
} Integrate as middleware
<?php
namespace App\Http\Middleware;
use App\Services\SlidingWindowRateLimiter;
use Closure;
use Illuminate\Http\Request;
class SlidingWindowThrottle
{
public function __construct(
private SlidingWindowRateLimiter $limiter
) {}
public function handle(Request $request, Closure $next, int $limit = 100, int $window = 60)
{
$key = $request->user()?->id ?: $request->ip();
$identifier = 'sliding:' . $request->route()->getName() . ':' . $key;
if (!$this->limiter->attempt($identifier, $limit, $window)) {
return response()->json([
'message' => 'Rate limit exceeded.',
'retry_after' => $window,
], 429)->withHeaders([
'X-RateLimit-Limit' => $limit,
'X-RateLimit-Remaining' => 0,
'Retry-After' => $window,
]);
}
$remaining = $this->limiter->getRemaining($identifier, $limit, $window);
return $next($request)->withHeaders([
'X-RateLimit-Limit' => $limit,
'X-RateLimit-Remaining' => $remaining,
]);
}
} This implementation performs four Redis operations per request. Under high load (>1000 req/s per key), consider consolidating into a single Lua script to reduce round trips. Monitor Redis SLOWLOG in production—sorted set operations on large windows can degrade if cleanup lags. For legal-tech document signing APIs I've built, this level of precision justified the overhead; for general browsing endpoints, it did not.
What production pitfalls break API rate limiting with token bucket and sliding window?
Correct algorithm implementation still fails in production due to environmental oversights. These are recurring issues I encounter when auditing or taking over existing Laravel deployments:
- Non-atomic operations: Using separate GET and SET commands instead of Lua scripts or Laravel's built-in atomic methods. Under concurrency, this creates race conditions allowing 2–3× the intended limit. Always use
RateLimiter::attempt()or verified Lua scripts. - Shared Redis instance: Rate limit keys competing with sessions, queues, and cache for memory. When Redis hits maxmemory, volatile keys get evicted unpredictably. Dedicate a Redis database (db 1 or db 2) exclusively for rate limiting with its own maxmemory allocation.
- Missing rate limit headers: Clients cannot implement backoff without
X-RateLimit-Remaining,X-RateLimit-Limit, andRetry-Afterheaders. Omitting these causes retry storms that amplify load during recovery. Laravel adds these automatically with throttle middleware; custom implementations must replicate them. - IP-based limiting behind proxies: Trusting
$request->ip()without configuring trusted proxies returns the load balancer IP, applying one global limit to all users. ConfigureTrustProxiesmiddleware and use$request->user()?->idas primary key when authenticated. - Ignoring timezone and clock skew: Sliding window relies on server time. If your Redis server and PHP-FPM workers have divergent clocks (common in containerized environments), windows drift. Use Redis TIME command or ensure NTP synchronization across all nodes.
A common mistake in Nepal-based deployments is testing rate limits only on local development machines with single-process PHP servers. Production environments running PHP-FPM with 10+ workers against shared Redis expose atomicity bugs that never appear locally. Always test rate limiting behavior with concurrent requests using tools like wrk or k6 against a staging Redis instance before deploying. Verify header values match actual consumption under load.
Implementing API Rate Limiting with Token Bucket and Sliding Window Correctly
Effective API rate limiting with token bucket and sliding window requires matching algorithm to business need, implementing atomically in Redis, exposing proper headers, and validating under realistic concurrency. Start with Laravel's native token bucket for most endpoints; escalate to custom sliding window only when compliance or billing demands exactness. Monitor Redis performance, dedicate infrastructure, and treat rate limiting as integral to your REST API architecture rather than an afterthought. For teams needing hands-on implementation support or audit of existing rate limiting configurations, reach out directly to discuss your specific traffic patterns and requirements.

