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.

API Rate Limiting with Token Bucket and Sliding Window

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.

Token BucketTokensRequest (-1)Refill (+N/sec)Allows bursts up to capacitySliding WindowRolling 60s CounterCheck count < limitExpire old entriesStrict per-second precision
Token bucket allows controlled bursts while sliding window enforces strict rolling limits for API rate limiting

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.
CriteriaToken BucketSliding Window
Burst handlingAllowed up to capacityStrictly prevented
Redis operations per request1–2 (GET + SET or Lua script)2–4 (ZADD + ZRANGEBYSCORE + ZREMRANGEBYSCORE + EXPIRE)
Memory usageO(1) per keyO(N) where N = requests in window
Implementation complexityLow (native Laravel support)Moderate (custom Lua or package)
Best forUser-facing APIs, mobile backendsBilling APIs, compliance-bound endpoints
Laravel 12 built-in driverYes (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');
ClientLaravelRedisBusiness LogicHTTP RequestEVALSHA lua_script{allowed: true, remaining: 59}Execute controllerJSON response200 OK + headersIf denied: 429
Laravel token bucket rate limiting executes atomically in Redis before reaching business logic

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:

  1. 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.
  2. 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.
  3. Missing rate limit headers: Clients cannot implement backoff without X-RateLimit-Remaining, X-RateLimit-Limit, and Retry-After headers. Omitting these causes retry storms that amplify load during recovery. Laravel adds these automatically with throttle middleware; custom implementations must replicate them.
  4. IP-based limiting behind proxies: Trusting $request->ip() without configuring trusted proxies returns the load balancer IP, applying one global limit to all users. Configure TrustProxies middleware and use $request->user()?->id as primary key when authenticated.
  5. 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.
New endpoint needs limitingIs exact per-second compliance required?NoYesToken BucketSliding WindowUse Laravel throttleCustom Redis sorted setMonitor burst toleranceAudit Redis SLOWLOG
Decision tree for selecting API rate limiting with token bucket and sliding window based on compliance requirements

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.

Frequently Asked Questions

Token bucket allows traffic bursts up to a defined capacity while maintaining an average rate, whereas sliding window enforces a strict limit over a rolling time period. I use token bucket for user-facing APIs needing flexibility and sliding window for backend services requiring precise throughput control.

Choose token bucket when clients need burst capacity for variable workloads like file uploads or search queries. Use sliding window for billing enforcement or preventing abuse where strict adherence to limits matters more than user experience. In my Laravel projects, token bucket usually provides better UX without sacrificing protection.

Custom implementation in Laravel takes 8-16 hours, roughly NPR 40,000-80,000 (USD 300-600). Using packages like spatie/laravel-rate-limiter reduces this to 2-4 hours. Redis infrastructure adds NPR 1,500-3,000 monthly on local hosting. Budget extra for testing edge cases and monitoring setup.

Yes, using database or file storage, but performance degrades significantly above 100 requests per second. For production Laravel apps handling real traffic, Redis is essential. I have seen database-backed limiters cause latency spikes during peak hours on client projects. Stick to Redis or Memcached for anything beyond development environments.

Install spatie/laravel-rate-limiter via Composer, publish the config, and set your Redis connection. Define buckets in config/rate-limiter.php with capacity and refill rate. Apply middleware to routes using ->middleware('throttle:api'). Test with curl or Postman to verify burst behavior matches expectations before deploying to production.

The API returns HTTP 429 Too Many Requests with Retry-After header indicating seconds until the next allowed request. Well-designed clients implement exponential backoff. On legal-tech portals I have built, we also log violations for abuse detection. Never silently drop requests; always communicate limits clearly so integrations can self-correct.

Use centralized Redis as the shared state store instead of local memory or database. Configure all application servers to connect to the same Redis instance. This ensures consistent counting regardless of which server handles the request. On Deployer-managed deployments, verify Redis connectivity is identical across all release symlinks.

Apply per authenticated user or API key for fair usage tracking. Fall back to IP only for unauthenticated endpoints like login or registration. Combining both prevents credential stuffing while allowing legitimate users higher limits. In eCommerce systems, I tier limits by subscription level, giving premium customers higher burst allowances.

Use time mocking in PHPUnit with Carbon::setTestNow() to simulate elapsed time between requests. Create dedicated test classes that verify bucket refill logic and window expiration. For integration tests, configure a separate Redis database and flush it between test runs. Never test against production Redis; isolation prevents accidental service disruption.

Forgetting to handle clock skew between servers causes inconsistent refill timing. Not setting reasonable defaults leads to either blocking legitimate users or failing to prevent abuse. Another pitfall is neglecting to document limits in API responses. Always include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers so clients can adapt proactively.

Fixed window resets counts at arbitrary boundaries, allowing double the limit during transition periods. Sliding window uses a rolling timeframe, eliminating this edge case. The trade-off is slightly higher computational overhead. For billing-critical APIs where accuracy matters, sliding window justifies the cost. For general protection, fixed window may suffice with simpler implementation.

Rate limiting mitigates application-layer attacks but cannot stop volumetric DDoS targeting network infrastructure. Layer it behind Cloudflare or AWS Shield for comprehensive defense. Application-level limits still matter because they prevent resource exhaustion even when some malicious traffic passes through. On high-traffic sites, combine CDN filtering with precise API throttling.

Track 429 response rates, average latency, and Redis memory usage via Laravel Telescope or Datadog. Set alerts when rejection rates exceed 5% of total requests, indicating limits may be too aggressive. Log rejected requests with client identifiers to identify patterns. Regular review prevents silent failures where misconfigured limits degrade legitimate user experience without triggering alarms.

Use sorted sets for sliding windows with timestamps as scores and request IDs as members. Token buckets fit well in hashes storing current tokens and last refill timestamp. Avoid simple string counters for complex algorithms. Enable Redis persistence only if losing rate state on restart is unacceptable; most APIs tolerate brief reset periods during maintenance.

Implement circuit breaker pattern falling back to in-memory limiting or temporary permissive mode. Log the failure for immediate investigation. Never let rate limiter outages crash your entire API. On production systems, I configure health checks that disable limiting after consecutive Redis failures, prioritizing availability over strict enforcement during infrastructure incidents.

Share this article

Quick Contact Options
Choose how you want to connect me: