API Rate Limiting: Design Patterns That Scale From Hobby to Enterprise
Rate limiting is one of those features that seems trivially simple until you actually have to implement it properly. A counter and a timer, right? Sure — until you need to handle multiple pricing tiers, burst allowances, distributed servers, graceful degradation, and clear communication to API consumers about their limits and remaining quota.
I have implemented rate limiting in five different production systems over the past four years. Here is everything I learned.
Why Rate Limit At All
Rate limiting serves three purposes:
Protection from abuse. Without limits, a single misbehaving client (or attacker) can overwhelm your service, degrading performance for everyone. Even well-intentioned clients can accidentally DDoS you with a misconfigured retry loop.
Fair resource allocation. If you have 100 API consumers sharing infrastructure, one consumer using 90% of capacity is unfair to the other 99. Rate limits ensure equitable access.
Business model enforcement. If you sell API access in tiers (free: 100 requests/day, pro: 10,000 requests/day), rate limiting is how you enforce those tiers. It is the technical implementation of your pricing page.
Algorithm 1: Fixed Window
The simplest algorithm: count requests in a fixed time window (e.g., per minute). If the count exceeds the limit, reject requests until the window resets.
Pros: Simple to implement and understand. Low memory usage (one counter per window per client).
Cons: Burst problem at window boundaries. A client can make 100 requests at 11:59:59 and another 100 at 12:00:00 — 200 requests in 2 seconds despite a 100/minute limit. This double-burst at window boundaries is the classic weakness.
Implementation: store a counter with a TTL equal to the window duration. Redis INCR with EXPIRE is the canonical approach. In-memory maps work for single-server deployments.
Algorithm 2: Sliding Window Log
Track the timestamp of every request. To check if a new request is allowed, count requests in the last N seconds. If under the limit, allow it and record the timestamp.
Pros: No boundary burst problem. Perfectly accurate.
Cons: Memory intensive. Storing every request timestamp for every client uses significant memory at scale. Also requires scanning the log for each check, which is O(n) in the window size.
This algorithm works for low-volume APIs where accuracy matters more than performance. For high-volume APIs, the memory cost is prohibitive.
Algorithm 3: Sliding Window Counter
A hybrid of fixed window and sliding window. Maintain counters for the current and previous window. Calculate the weighted count: previous_count * overlap_percentage + current_count. If under the limit, allow.
Pros: Nearly as accurate as the sliding window log with the memory efficiency of fixed window (two counters per client instead of a full log).
Cons: Slightly less accurate at window boundaries, but the approximation is good enough for virtually all use cases.
This is what we use at AIDToolStack for our API endpoints. The implementation in Redis is elegant: two counters (current window and previous window) with appropriate TTLs. The weighted calculation runs server-side and is nearly free computationally.
Algorithm 4: Token Bucket
Each client has a bucket that fills with tokens at a steady rate. Each request costs one token. When the bucket is empty, requests are rejected. The bucket has a maximum capacity, allowing bursts up to that capacity.
Pros: Naturally handles bursty traffic. A client can burst up to their bucket capacity, then is limited to the refill rate. This maps well to real usage patterns — humans interact in bursts, not at steady rates.
Cons: More complex to implement correctly, especially in distributed systems where multiple servers need to atomically decrement the same bucket.
Implementation: store the last request timestamp and the current token count. On each request, calculate tokens accumulated since the last request (elapsed_time * refill_rate), add them to the count (up to max), then subtract one for the current request. If the count would go negative, reject.
Algorithm 5: Leaky Bucket
Requests enter a queue (the bucket) and are processed at a fixed rate. If the queue is full, new requests are rejected. Unlike token bucket which allows bursts, leaky bucket enforces a steady output rate.
Pros: Perfectly smooth output rate. Good for protecting downstream services that cannot handle bursts.
Cons: Adds latency because requests queue instead of being immediately processed or rejected. Not suitable for real-time APIs where latency matters.
Distributed Rate Limiting
All the above algorithms become harder in distributed systems where multiple application servers handle requests for the same client. You need a shared state store for counters.
Redis is the standard choice. It is fast enough (sub-millisecond latency), supports atomic operations (INCR, EXPIRE), and handles the concurrency. A Lua script that atomically increments the counter, checks the limit, and sets the TTL ensures no race conditions.
The failure mode matters: what happens when Redis is down? Options:
Fail open: allow all requests when the rate limiter is unavailable. Risky for abuse protection but ensures availability.
Fail closed: reject all requests when the rate limiter is unavailable. Safer but causes outages when Redis is down.
Local fallback: maintain an approximate in-memory counter and fall back to it when Redis is unavailable. Inaccurate (each server counts independently) but provides some protection without total failure.
We fail open with alerting. If Redis is down, requests are allowed but an alert fires immediately. This is the right trade-off for us — a few minutes without rate limiting during a Redis outage is less damaging than blocking all API access.
Response Headers: Communicating Limits to Clients
Good rate limiting communicates clearly with API consumers through headers:
X-RateLimit-Limit: the maximum requests allowed in the window.
X-RateLimit-Remaining: requests remaining in the current window.
X-RateLimit-Reset: Unix timestamp when the window resets.
Retry-After: seconds until the client should retry (on 429 responses).
These headers let well-behaved clients self-throttle before hitting the limit. Our API clients check X-RateLimit-Remaining and slow down when it gets low, avoiding rejected requests entirely.
Tiered Rate Limiting
For SaaS products with pricing tiers, rate limits need to be configurable per plan:
Store the plan's rate limit configuration alongside the API key. When validating a request, look up the key, determine the plan, and apply the plan's limits. This adds one database lookup per request — cache the key-to-plan mapping in Redis with a short TTL (5 minutes) to avoid hitting the database on every request.
Multiple limit types per plan are common: per-second burst limit (prevents hammering), per-day total limit (enforces plan quota), per-endpoint limits (expensive operations have lower limits). Check all applicable limits and reject if any are exceeded.
Graceful Degradation
Rejecting requests outright is not always the best user experience. Consider these alternatives:
Queuing: instead of rejecting, queue the request and process it when capacity is available. Return a 202 Accepted with a URL to poll for the result. Works well for non-real-time operations.
Degraded responses: return cached or simplified data instead of rejecting. An AI product search that exceeds rate limits could return cached results instead of running a fresh database query.
Priority lanes: reserve some capacity for paying customers. Free tier traffic gets rate limited first, ensuring paying customers are never throttled during traffic spikes.
Monitoring and Alerting
Monitor your rate limiting system:
Track the number of rejected requests per client, per plan, and per endpoint. A sudden spike in rejections from a single client might indicate a misconfigured integration (help them) or an attack (block them).
Track the ratio of approaching-limit warnings to actual rejections. If most clients hit their limits regularly, your limits might be too low (or your pricing too restrictive).
Alert on rate limit failures (Redis down, configuration errors). A broken rate limiter is invisible until it causes an incident.
Rate limiting is infrastructure that earns its investment over time. Get the fundamentals right early, and you will avoid scrambling when your first misbehaving client shows up at 3am.
Written by AIDToolStack Editorial
The AIDToolStack editorial team reviews developer tools, AI products, and MCP servers with hands-on testing and honest assessments. We use the tools we review every day in production.
References & Further Reading
Related Posts
Sponsor Our Newsletter
Reach thousands of developers who are actively evaluating AI tools, MCP servers, and dev infrastructure. Our weekly newsletter goes to engaged technical decision-makers.
All sponsored content is clearly labeled per our editorial policy.