API Rate Limiting with Token Bucket and Sliding Window

Khimananda Oli 9 min read Programming and Languages
API Rate Limiting with Token Bucket and Sliding Window

By Khimananda Oli | Last reviewed: August 2026

Unprotected APIs are a primary vector for denial-of-service attacks and runaway cloud bills in modern distributed systems. Effective API Rate Limiting with Token Bucket and Sliding Window algorithms provides the necessary guardrails to balance legitimate user access against abusive traffic patterns without degrading performance. This guide moves beyond theory to provide production-grade implementation patterns using Redis, helping you choose the right throttling strategy for your specific compliance and latency requirements.

Client AppRequest StreamAPI Gateway / EdgeRate Limiter MiddlewareToken BucketSliding WindowBackend SvcProtected ResourceRedis Store
Architecture overview showing where API Rate Limiting with Token Bucket and Sliding Window sits between clients and backend services

How does the Token Bucket algorithm handle bursty API traffic?

The Token Bucket algorithm is the industry standard for API Rate Limiting with Token Bucket and Sliding Window because it gracefully handles real-world traffic patterns that are rarely perfectly uniform. Unlike rigid counters, it permits short bursts of activity up to a defined capacity while enforcing a long-term average rate. This behavior mirrors how actual users interact with applications: loading a dashboard might trigger twenty parallel API calls instantly, followed by minutes of silence.

Core mechanics and configuration parameters

A token bucket consists of three configurable variables that determine its behavior in production:

  • Capacity (C): The maximum number of tokens the bucket can hold. This defines your burst limit. If C=100, a client can make 100 requests instantly after a period of inactivity.
  • Refill Rate (R): Tokens added per second. This sets your sustainable throughput. R=10 means 10 requests/second on average.
  • Current Tokens (T): The live count, always constrained between 0 and C.

When a request arrives, the system calculates tokens generated since the last check: T = min(C, T + (now - last_refill) * R). If T ≥ 1, the request proceeds and T decrements. Otherwise, return HTTP 429. For deeper context on protecting downstream databases from the resulting traffic patterns, see our MySQL performance tuning guide.

Production-ready Redis implementation

In distributed environments, you cannot store bucket state in application memory. Use Redis with a Lua script to guarantee atomicity. Race conditions between reading and writing token counts will silently corrupt your limits under load.

-- KEYS[1] = bucket key, ARGV[1] = capacity, ARGV[2] = refill_rate, ARGV[3] = now_ms
local key = KEYS[1]
local cap = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

local data = redis.call('HMGET', key, 'tokens', 'last')
local tokens = tonumber(data[1])
local last = tonumber(data[2])

if tokens == nil then
  tokens = cap
  last = now
else
  local delta = math.max(0, now - last)
  tokens = math.min(cap, tokens + (delta * rate / 1000))
end

if tokens >= 1 then
  tokens = tokens - 1
  redis.call('HMSET', key, 'tokens', tokens, 'last', now)
  redis.call('EXPIRE', key, math.ceil(cap / rate) + 60)
  return {1, math.floor(tokens)}
else
  redis.call('HMSET', key, 'tokens', tokens, 'last', now)
  redis.call('EXPIRE', key, math.ceil(cap / rate) + 60)
  return {0, math.floor(tokens)}
end

This script executes atomically inside Redis. The TTL prevents orphaned keys from accumulating indefinitely. Always set expiry slightly longer than the time required to fully refill the bucket.

When should you use Sliding Window Log over fixed windows?

Fixed window counters suffer from the boundary burst problem: a client allowed 100 requests/minute could send 100 at :59 and another 100 at :00, creating a 200-request spike in two seconds. The Sliding Window Log eliminates this edge case entirely by tracking individual request timestamps within a rolling period. For compliance-sensitive workloads like payment processing or authentication endpoints where strict adherence to rate policies matters more than burst tolerance, this precision justifies the additional storage cost.

Fixed Window vs Sliding Window EnforcementFixed Window (Boundary Burst)Window NWindow N+12x Spike!Sliding Window (Smooth)Rolling 60sConsistent LimitSliding Window Log Mechanics1. Store timestamp for each request in sorted set2. Remove entries older than (now - window_size)3. Count remaining members4. Allow if count < limit, else reject
Visual comparison demonstrating why Sliding Window prevents the boundary burst vulnerability inherent in fixed-window counters

Atomic Sliding Window implementation with Redis Sorted Sets

Use Redis sorted sets (ZSET) where both member and score are the timestamp in milliseconds. This enables efficient range queries and automatic deduplication handling.

-- KEYS[1] = window key, ARGV[1] = limit, ARGV[2] = window_ms, ARGV[3] = now_ms
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cutoff = now - window

redis.call('ZREMRANGEBYSCORE', key, '-inf', cutoff)
local current = redis.call('ZCARD', key)

if current < limit then
  redis.call('ZADD', key, now, now .. '-' .. math.random(100000))
  redis.call('PEXPIRE', key, window)
  return {1, limit - current - 1}
else
  return {0, 0}
end

Note the random suffix appended to the timestamp member. Without it, multiple requests within the same millisecond would overwrite each other in the sorted set, causing undercounting. The PEXPIRE ensures keys self-clean even if cleanup logic fails. Teams managing observability for these patterns should correlate rate-limit metrics with their four golden signals monitoring dashboards.

What are the trade-offs between Token Bucket and Sliding Window?

Choosing between these algorithms requires understanding their operational characteristics beyond theoretical correctness. The following comparison reflects production realities observed across dozens of deployments:

CriteriaToken BucketSliding Window Log
Burst HandlingAllows controlled bursts up to capacityStrictly enforces limit; no burst tolerance
Memory per KeyO(1) — two fields (tokens, timestamp)O(N) — one entry per request in window
Computational CostConstant time arithmeticO(log N) for ZREMRANGEBYSCORE + ZCARD
Boundary BehaviorSmooth transition; no edge spikesPerfectly smooth; eliminates boundary issue
Implementation ComplexityModerate (refill math + atomicity)Higher (sorted set management + uniqueness)
Best Use CaseUser-facing APIs, microservice-to-microserviceAuth endpoints, billing APIs, compliance scopes
Fairness Under LoadEarly requesters consume burst capacityUniform enforcement regardless of timing

In practice, many platforms implement both: Token Bucket for general API endpoints where developer experience matters, and Sliding Window for security-critical paths where predictability trumps flexibility. This hybrid approach appears frequently in architectures documented in our API gateways for microservices guide.

How do you implement distributed rate limiting without race conditions?

Single-instance rate limiting fails immediately in horizontal scaling scenarios. Distributed rate limiting centralizes state in Redis or Memcached, but introduces network latency and failure modes that must be addressed explicitly.

Critical implementation requirements

  1. Atomic operations are non-negotiable. Never read-modify-write across separate Redis commands. Use Lua scripts or Redis 7.4+ functions to encapsulate the entire check-and-update in a single round trip.
  2. Set defensive TTLs on every key. Orphaned rate limit keys consume memory indefinitely. TTL should exceed your window or refill period by a safety margin (typically 2x).
  3. Implement graceful degradation. If Redis is unreachable, decide upfront: fail open (allow all traffic, risk abuse) or fail closed (block all traffic, risk outage). For most user-facing APIs, failing open with local in-memory fallback is preferable. For payment endpoints, fail closed.
  4. Include rate limit headers in every response. Return X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. Clients need this information to implement backoff intelligently rather than hammering your API blindly.
  5. Namespace keys rigorously. Include API version, endpoint path, and client identifier in the key schema. Example: ratelimit:v2:users:list:client_abc123. This prevents accidental collisions during deployments and enables granular policy changes.

Handling clock skew and timezone considerations

Distributed systems rarely have perfectly synchronized clocks. For Token Bucket, minor skew affects refill accuracy negligibly. For Sliding Window, skew can cause premature expiration or extended windows. Mitigate this by using Redis server time (TIME command) within Lua scripts rather than trusting client-supplied timestamps. In Nepal-based deployments serving global users, remember that BS calendar conversions are irrelevant at the infrastructure layer—always operate in UTC epoch milliseconds internally and convert only at presentation boundaries.

Start: New EndpointIs strict compliance required?YesNoSliding Window LogPrecise, audit-readyBurst tolerance needed?YesNoToken BucketFlexible, burst-friendlyFixed WindowSimplest, low-costAlways: Add Redis + Lua Atomicity + Defensive TTLs
Decision framework for choosing between Token Bucket, Sliding Window, and Fixed Window based on compliance and burst requirements

How do you monitor and tune rate limiters in production?

Deploying rate limits without observability is operating blind. You need visibility into three dimensions: effectiveness (are limits catching abuse?), user impact (are legitimate users being throttled?), and system health (is the rate limiter itself becoming a bottleneck?).

Instrument these metrics at minimum:

  • Rejection rate by endpoint and client tier: Sudden spikes indicate either an attack or misconfigured limits. Track separately for free vs. paid tiers.
  • P99 latency of rate limit checks: If your Lua script takes 5ms but your SLA budget is 50ms total, rate limiting consumes 10% of your error budget. Optimize aggressively.
  • Token bucket depletion frequency: High depletion rates suggest capacity is too low for legitimate usage patterns. Adjust before users complain.
  • Redis connection pool saturation: Rate limiters generate high-frequency, low-payload traffic. Monitor connection churn separately from application data connections.

Establish feedback loops with your support team. When legitimate users report 429 errors, correlate with your metrics to distinguish between genuine bugs and expected enforcement. Document tuning decisions in runbooks linked to your SLO-driven alerting configuration so future engineers understand why limits were set at specific values.

Securing Your API Surface with Intelligent Throttling

Implementing API Rate Limiting with Token Bucket and Sliding Window correctly requires matching algorithm choice to business intent, enforcing atomicity in distributed state stores, and maintaining continuous observability. Start with Token Bucket for general-purpose endpoints to preserve user experience, reserve Sliding Window for compliance-scoped paths, and never deploy either without comprehensive monitoring. If your team needs assistance designing rate limiting strategies that align with SOC 2 requirements or multi-region architectures, reach out to discuss your specific infrastructure challenges.

Frequently Asked Questions

Token bucket allows traffic bursts by accumulating tokens over time, while sliding window enforces a strict average request rate across a moving time frame without permitting sudden spikes beyond the defined limit.

Use token bucket when your clients need burst capacity for batch processing or variable loads. It accommodates temporary spikes better than sliding window, which is stricter and better suited for uniform traffic enforcement.

Redis uses sorted sets with timestamps as scores. Commands like ZRANGEBYSCORE count requests within the window, and ZREMRANGEBYSCORE purges old entries, ensuring O(log N) performance even at high throughput in 2026 deployments.

Yes. Apply token bucket per client for burst allowance and sliding window globally for total system protection. This hybrid approach balances user experience with infrastructure safety during traffic surges.

Setting refill rates too low causes unnecessary throttling during normal usage. Too high defeats the purpose. Always base refill calculations on observed p95 latency and actual client consumption patterns, not theoretical maximums.

Rely on server-side monotonic clocks or synchronized time sources like chrony. Never trust client timestamps. Use Redis TIME command or similar centralized time references to ensure consistent window boundaries across nodes.

Yes. Each client requires storing current token count and last refill timestamp. For millions of users, consider hierarchical buckets or approximate counting to reduce per-client state overhead in production systems.

Return Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. These standard headers help clients implement backoff logic correctly and improve debugging during integration testing phases.

CDNs may cache 429 responses if not configured properly. Set Cache-Control: no-store on rate limit responses. Apply limiting at the origin or edge compute layer to avoid caching throttled states.

No. Token bucket manages legitimate client behavior but cannot stop volumetric attacks. Pair it with network-layer filtering, WAF rules, and anomaly detection for comprehensive DDoS mitigation in 2026 architectures.

Sliding window costs more due to range queries and cleanup operations. Fixed window uses simple counters but suffers from boundary burst issues. Accept the overhead for accuracy or use approximate sliding windows.

Mock time dependencies in unit tests using injectable clock interfaces. In staging, use tools like k6 or vegeta with configurable request rates to validate bucket drain and refill behavior deterministically.

Apply unauthenticated limits first to protect login endpoints from brute force. Then apply authenticated, user-specific limits post-auth. This two-tier approach prevents resource exhaustion before identity verification occurs.

Requests are rejected immediately with 429 status until tokens regenerate. Clients must respect Retry-After headers or implement exponential backoff to avoid compounding failures during recovery periods.

Track 429 response rates, token depletion frequency, and client retry patterns via Prometheus metrics. Alert on sustained throttling indicating misconfigured limits or abuse, not transient spikes during expected peak hours.