Dev.to Security 🔐 Cybersecurity 👁 0 📖 3 min read

Rate Limiting

One-liner: Rate limiting controls how many requests a client can make in a given time window — protecting your system from abuse, DDoS, and runaway clients. 📌 Why Rate Limit? Prevent abuse — stop scrape

One-liner: Rate limiting controls how many requests a client can make in a given time window — protecting your system from abuse, DDoS, and runaway clients.

📌 Why Rate Limit?

  • Prevent abuse — stop scrapers, bots, bad actors
  • Prevent DDoS — absorb attack traffic before it reaches your servers
  • Fairness — ensure no single user hogs resources
  • Cost control — prevent runaway API usage on paid upstream services
  • SLA enforcement — tier-based limits (free: 100/min, pro: 10K/min)

🔢 Rate Limiting Algorithms

1. Fixed Window Counter

Window: 1 minute (60s)
Limit: 100 requests

|------ 0:00 to 1:00 ------|------ 1:00 to 2:00 ------|
  count: 100 ✅                count: 0 (reset)

Implementation:

const key = `rate:${userId}:${Math.floor(Date.now() / 60000)}`;
const count = await redis.incr(key);
await redis.expire(key, 60);
if (count > 100) throw new TooManyRequestsError();

✅ Simple, O(1)
❌ Boundary burst problem: User sends 100 at 0:59, 100 at 1:01 → 200 in 2 seconds!

2. Sliding Window Log

Keep a log of timestamps of all recent requests:

Requests: [0:30, 0:45, 0:55, 1:05, 1:15]
At time 1:20, window is [0:20 to 1:20]
Valid requests: [0:30, 0:45, 0:55, 1:05, 1:15] = 5
const now = Date.now();
const windowStart = now - 60000; // 1 minute ago

// Remove old entries, add new one, count
await redis.zremrangebyscore(key, 0, windowStart);
await redis.zadd(key, now, now);
const count = await redis.zcard(key);

if (count > 100) throw new TooManyRequestsError();

✅ Accurate, no boundary bursting
❌ Memory-heavy — stores every request timestamp
❌ O(log N) per request

3. Sliding Window Counter (Best Balance)

Approximate the sliding window using two fixed windows:

Current window count: 40 (60% through window)
Previous window count: 80

Estimate: 80 × (1 - 0.60) + 40 = 80 × 0.40 + 40 = 32 + 40 = 72

✅ Memory efficient (just 2 counters)
✅ Close to accurate
✅ O(1)

4. Token Bucket

Bucket capacity: 100 tokens
Refill rate: 10 tokens/second

Each request: consume 1 token
No tokens: reject request

Allows bursts up to bucket capacity
class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.tokens = capacity
        self.capacity = capacity
        self.refill_rate = refill_rate  # tokens per second
        self.last_refill = time.time()

    def consume(self, tokens=1):
        now = time.time()
        elapsed = now - self.last_refill
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
        self.last_refill = now

        if self.tokens >= tokens:
            self.tokens -= tokens
            return True  # allowed
        return False  # rejected

✅ Allows controlled bursts
✅ Smooth rate over time
Used by: AWS API Gateway, Stripe

5. Leaky Bucket

Requests pour in → [Bucket] → drip out at fixed rate
Bucket overflows → requests rejected

Unlike token bucket, leaky bucket enforces strict constant rate (no bursts).

✅ Strict rate enforcement
❌ No burst allowance

📊 Algorithm Comparison

Algorithm Memory Accuracy Burst Complexity
Fixed Window O(1) Low (boundary issue) Yes Simple
Sliding Log O(N) High No Medium
Sliding Counter O(1) Medium No Simple
Token Bucket O(1) High Yes Medium
Leaky Bucket O(1) High No Medium

In interviews: Sliding Window Counter or Token Bucket are the best answers.

🗺️ Where to Apply Rate Limiting

Client → [CDN Rate Limit]       ← block volumetric attacks early
       → [API Gateway Limit]    ← per-user, per-endpoint limits
       → [Service-level Limit]  ← protect individual services
       → [DB Connection Limit]  ← max connections in pool

🔑 Rate Limit Headers

Always return these headers so clients can adapt:

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 63
X-RateLimit-Reset: 1711641600

# When rate limited:
HTTP/1.1 429 Too Many Requests
Retry-After: 30

🏗️ Distributed Rate Limiting

Multiple app servers → each has local counter → no coordination → limits not enforced accurately.

Solution: Centralized counter in Redis

[App Server 1] ─┐
[App Server 2] ─┼──► [Redis] ← single source of truth for counters
[App Server 3] ─┘

Redis INCR is atomic → no race conditions.

🌍 Rate Limiting in Real Systems

Company Strategy
GitHub API 5,000 req/hr (authenticated), 60/hr (anonymous)
Twitter API Per-endpoint limits (15 min windows)
Stripe 100 req/sec per secret key
AWS API Gateway Token bucket, configurable per stage
Cloudflare Rule-based at CDN level (before hitting origin)

🔑 Key Takeaways

  • Rate limiting is a must-have in any public API
  • Use Token Bucket when bursts are acceptable; Sliding Window Counter for strict limits
  • Always use Redis for distributed rate limiting (atomic INCR)
  • Return proper headers (X-RateLimit-*) so clients can self-throttle
  • Apply rate limiting at multiple layers (CDN, gateway, service)
📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.