In my experience building scalable backend systems, I’ve learned that a simple ‘100 requests per minute’ rule is rarely enough for production-grade APIs. When your traffic spikes—whether due to a viral marketing campaign or a malicious bot—basic counters fail. To maintain uptime, you need advanced api rate limiting strategies that can distinguish between a legitimate power user and a DDoS attack.
Rate limiting isn’t just about saying ‘no’ to users; it’s about gracefully degrading service to ensure the system remains available for everyone. In this post, I’ll share ten strategies I’ve implemented across various projects, moving from basic algorithms to sophisticated distributed patterns.
1. The Token Bucket Algorithm
This is my go-to for handling ‘bursty’ traffic. Instead of a rigid window, you have a ‘bucket’ that fills with tokens at a constant rate. Each request consumes a token. If the bucket is full, the user can send a burst of requests instantly until the bucket is empty.
I highly recommend reading my token bucket algorithm guide for a deep dive into the math behind this. It’s particularly effective for APIs where users naturally perform actions in clusters (like refreshing a feed).
2. The Leaky Bucket for Smooth Traffic
Unlike the token bucket, the leaky bucket processes requests at a fixed, constant rate regardless of the burst. Requests enter a queue (the bucket) and ‘leak’ out to the server at a steady pace. If the queue fills up, new requests are dropped.
Use this when your downstream legacy systems cannot handle any variance in throughput. It transforms jagged traffic spikes into a smooth, predictable stream.
3. Sliding Window Logs
Fixed windows (e.g., reset every hour on the hour) suffer from a ‘boundary problem’ where a user can double their quota by hitting the API at the end of one window and the start of the next. Sliding window logs track the timestamp of every single request.
// Conceptual Logic
const now = Date.now();
const windowStart = now - 60000; // 1 minute window
const userLogs = redis.zrangebyscore(userKey, windowStart, now);
if (userLogs.length >= LIMIT) return 429;
redis.zadd(userKey, now, now);
While accurate, this is memory-intensive. To mitigate this, I often combine this with Redis for API caching to keep the logs in-memory and fast.
4. Sliding Window Counter
To solve the memory issue of logs, the sliding window counter uses a weighted average of the current and previous fixed windows. If you are 25% into the current window, the limit is calculated as: (current_window_count) + (previous_window_count * 0.75).
This provides a smooth approximation of the sliding window without storing every timestamp.
5. Tiered Rate Limiting (Plan-Based)
Not all users are equal. I typically implement a tiered system based on API keys:
- Free: 1,000 req/day
- Pro: 100,000 req/day
- Enterprise: Custom/Unlimited
This is usually implemented via a middleware that fetches the user’s plan from a database and applies the corresponding limit.
6. Dynamic Rate Limiting based on System Load
Static limits are a guess. Advanced systems adjust limits based on current CPU or Memory usage. If the server load exceeds 80%, I automatically throttle ‘Free’ tier users more aggressively to protect ‘Enterprise’ users.
As shown in the architecture diagram below, the load balancer communicates with a health check service to signal the rate limiter to tighten constraints in real-time.
7. Distributed Rate Limiting with Redis
When you have ten API nodes, local in-memory limiting doesn’t work because a user could hit each node separately. You need a centralized store. Using Redis with Lua scripts ensures that the ‘check-and-increment’ operation is atomic, preventing race conditions.
8. Cost-Based Rate Limiting
Some endpoints are ‘expensive’ (e.g., a complex PDF export vs. a simple GET request). Instead of counting requests, assign a ‘cost’ to each endpoint.
GET /user: 1 unitPOST /generate-report: 50 units
Users have a balance of units per window, forcing them to be mindful of resource-heavy calls.
9. IP-Based Throttling for Unauthenticated Traffic
For endpoints like /login or /signup, you can’t use API keys. I implement strict IP-based limits here to stop brute-force attacks. This is a critical layer in preventing DDoS attacks and credential stuffing.
10. Adaptive Rate Limiting (The ‘Circuit Breaker’ Pattern)
If a specific downstream service is failing, there is no point in allowing requests to hit it. I use a circuit breaker that trips when error rates spike, instantly returning a 503 or 429 to all users for that specific resource until the service recovers.
Common Mistakes I’ve Seen
- Ignoring the Retry-After Header: Always tell the client when they can try again. Without it, clients will just spam your API blindly.
- Hard-coding Limits: Use environment variables or a config service. Changing a limit during a traffic spike should not require a full redeploy.
- Over-limiting: If you set limits too low, you frustrate your best users. Always monitor your 429 rates.
Measuring Success
To know if your strategies are working, track these three metrics:
- 429 Error Rate: Is it too high (frustrating users) or too low (not protecting the server)?
- Latency p99: Does the rate-limiting logic itself add more than 5-10ms to the request?
- Upstream Health: Did your server CPU stay stable during the last traffic spike?