Why concurrency breaks naive shared counters
Accessing a token bucket involves three logical steps:- Read remaining tokens and the last-update timestamp.
- Compute tokens to add based on elapsed time.
- Write the new token count and timestamp.
Atomicity with Redis Lua scripts
Redis supports running Lua scripts atomically: while a script executes, no other client can mutate the keys it touches. Encapsulate the read–compute–write sequence in a single Lua script and run it withEVAL/EVALSHA. This guarantees correctness no matter how many gateways you have.
Below is a compact, practical Lua script that implements a token-bucket check and update atomically. It assumes each bucket is a Redis hash with fields tokens and ts (timestamp in milliseconds).
allowed— 1 if allowed, 0 if rate-limited.remaining tokens— the new token count.retry_after— milliseconds until the next token (0 whenallowedis 1).
Note: Use integer millisecond timestamps (
now) to avoid floating-point drift in production.
Decide and document behavior when the shared store is unavailable: do you fail open (allow traffic) or fail closed (reject traffic)? This is a critical trade-off—prioritize availability for general APIs and stricter protection for security-sensitive endpoints (for example, login or payment flows).
Operational considerations and trade-offs
Quick parameter examples
For security-critical APIs (e.g., login, password reset, anti-abuse paths), prefer failing-closed on store outages. For general product APIs where availability is more important, failing-open might be acceptable. Document and test your chosen mode.
Deployment and scaling recommendations
- Run the Lua script via
EVALSHA(load script once, call by SHA) to reduce overhead. - Keep Redis instances in the same availability zone or region as your gateways to minimize latency.
- Monitor Redis operation latency, script execution time, and ops/s. Alert on degraded performance.
- If you need higher throughput, shard keys by user ID across a Redis Cluster or partition users by consistent hashing.
- Use replication and automated failover (Sentinel or managed Redis offerings). If you need cross-region failover, evaluate read/write latency and consistency implications.
Interview-friendly summary (talking points)
- Put the rate limiter at the API gateway so you reject work early.
- Count per account for authenticated users and per IP for anonymous traffic.
- Use the token-bucket algorithm: capacity = burst, refill = sustained rate. Example: capacity 50; refill 1.67 tokens/s.
- Store buckets centrally in Redis so all gateways operate on the same counters.
- Make the check and update atomic by running the logic inside Redis as a Lua script. This prevents races when multiple gateways access the same bucket.
- Return
429 Too Many Requestswith aRetry-Afterheader when rate-limited. - Decide and document fail-open vs fail-closed behavior for Redis outages based on endpoint sensitivity.
- Key takeaway: local counters lie in distributed systems — authoritative counters must live in a shared store and be updated atomically.
- Redis scripting: https://redis.io/docs/manual/programmability/eval-intro/
- Token bucket algorithm: https://en.wikipedia.org/wiki/Token_bucket
- Rate limiting design patterns: https://blog.cloudflare.com/rate-limiting-best-practices/
