15.9. Rate Limiting
Table of Contents
Why Rate Limiting Matters
Rate limiting is a security and stability technique that controls how many requests a client can make to your backend in a given amount of time.
Think of your API as a small shop with a single cashier. If 1,000 people rush in at the same time, the shop cannot serve anyone properly. Rate limiting is like a doorman who lets people in at a controlled pace, and stops people who come back too many times too quickly.
Rate limiting helps you:
- Protect against brute force attacks on login endpoints
- Protect against API abuse or scraping
- Prevent accidental overload from buggy clients
- Keep your service stable and fair for all users
Important: Every public-facing backend should have some form of rate limiting on sensitive or expensive operations, such as login, password reset, and resource-intensive endpoints.
Core Concepts
What Are “Rates” And “Limits”?
A rate limit always has at least two parts:
- Who is limited
- How many requests they can make in what time window
Common examples:
| Who is limited | Limit Example | Meaning |
|---|---|---|
| IP address | 100 requests per 1 minute | Each IP can make 100 requests every 60 seconds |
| User account (user ID) | 10 login attempts per 15 mins | Each user can try to log in 10 times in 15 mins |
| API key | 1,000 API calls per hour | Each key can call the API 1,000 times per hour |
| Entire system (global) | 500 requests per second total | System will only process 500 RPS globally |
Time Windows
Common time windows:
- Per second
- Per minute
- Per hour
- Per day
You can combine windows, for example:
- 10 requests / second
- 1,000 requests / hour
- 50,000 requests / day
This prevents both short bursts and long-term abuse.
What To Limit On
Identifiers: How To Know “Who” To Limit
You need an identifier to know which client you are tracking.
Typical identifiers:
| Identifier | When to use | Pros | Cons |
|---|---|---|---|
| IP address | Unauthenticated or public endpoints | Easy to get, no login required | Shared IPs, NAT, VPNs, mobile networks |
| User ID | Authenticated users | Stable per user, fairer than IP | Needs authentication |
| API key | Public APIs, 3rd-party consumers | Directly tied to a consumer or application | Needs key management |
| Session ID | Web apps with cookies/sessions | Treat each session as separate client | Session rotation can complicate counts |
| Device ID | Mobile apps | Per-device control | Requires device ID logic |
In practice, you often combine identifiers, for example:
- Limit logins by IP + username
- Limit API usage by API key
- Extra safety limit by IP to block obvious abusers
Simple Rate Limiting Strategies
Fixed Window
Fixed window rate limiting is the simplest approach.
Example rule:
- Each IP can perform 100 requests per minute
You divide time into fixed windows, for example:
- 12:00:00 to 12:00:59
- 12:01:00 to 12:01:59
For each window you count how many requests each identifier has made. If they exceed the limit, you reject further requests until the next window.
How It Works
For each request:
- Compute window key, for example,
current_minute = floor(current_time / 60) - Identify client, for example, IP address
- Key in storage:
rate:ip:<ip>:<current_minute> - Increment counter
- If counter > limit, block request
Example With Redis (Conceptual)
Key: rate:ip:203.0.113.10:202408271230
Value: number of requests in that minute
Pseudocode:
def is_allowed(ip):
window = current_minute_string() # e.g. "202408271230"
key = f"rate:ip:{ip}:{window}"
count = redis.incr(key)
if count == 1:
redis.expire(key, 60) # key auto deletes after 60 seconds
return count <= 100
If is_allowed returns False, you respond with an error.
Pros And Cons
| Pros | Cons |
|---|---|
| Very simple conceptually | Windows reset suddenly at boundary |
| Easy to implement with Redis | Short bursts across boundary can slip by |
| Fast and cheap | Less fair close to window reset |
Boundary problem example:
- Limit: 100 requests / minute
- Client makes 100 requests at 12:00:59
- Then 100 requests at 12:01:01
- Total 200 requests in 2 seconds, but both windows are within allowed limits
Sliding Window
Sliding window methods make rate limiting smoother by considering a moving time window instead of strict 1-minute blocks.
Two common variants:
- Sliding window counter (approximate)
- Sliding window log (more precise, more storage)
Sliding Window Counter (Approximate)
You keep counters for current window and previous window, and compute a weighted sum.
Example: 100 requests per minute
- At time $t$, within minute $M$
- Get counter for minute $M$ and minute $M - 1$
- Compute how far into the current minute we are, for example 30 seconds in is 0.5
- Effective count is:
$$
\text{effective} = C_M + (1 - \text{progress}) \cdot C_{M-1}
$$
If effective <= 100, allow.
This smooths out the boundary but is still approximate.
Sliding windows help avoid sudden resets at time boundaries. They provide fairer limits under bursty traffic than fixed windows.
Sliding Window Log (Precise)
You store a timestamp for each request per client, often in a sorted set or list.
Then, for each request:
- Remove timestamps older than
now - window_size - Count remaining timestamps
- If count >= limit, block
- Else, record current timestamp and allow
Storage example in Redis, key per user like: rate:user:123 with scores as timestamps.
Pros:
- Very accurate
- True sliding window
Cons:
- More memory, especially for large user bases
- Slower than simple counters
For most backend APIs, fixed window or token bucket is enough. Sliding windows are useful when you need smoother behavior.
Token Bucket And Leaky Bucket
These are classic algorithms that control burstiness more precisely.
Token Bucket
Imagine each user has a bucket that holds “tokens.”
- The bucket has a maximum size, for example 100 tokens
- Tokens are added over time, for example 1 token every 0.6 seconds
- Each request consumes 1 token
- If the bucket is empty, the request is blocked (or delayed)
This allows short bursts, as long as the user has accumulated tokens, but forces the average rate to stay under a certain limit.
Parameters
Two key parameters:
- Bucket capacity $B$
- Refill rate $R$ tokens per second
Example:
- $B = 100$
- $R = 1$ token per second
Behaviour:
- If the user does nothing for 100 seconds, the bucket fills to 100 tokens.
- They can then make 100 requests at once (burst), consuming all tokens.
- After that, they can only make 1 request per second on average.
Simple Token Bucket Formula
We often store:
- Last refill time $t_{last}$
- Current tokens $T$
On each request at time $t$:
- Calculate time difference: $\Delta t = t - t_{last}$
- Add new tokens:
$$
T = \min(B, T + \Delta t \cdot R)
$$
- Set $t_{last} = t$
- If $T \ge 1$, then:
- Allow request
- Decrease $T$ by 1
- Else:
- Block request
Token Bucket Rule:
At each request, first refill tokens using
$$T = \min(B, T + \Delta t \cdot R)$$
then consume 1 token if available. If not, block the request.
This algorithm is widely used in networking and APIs because it is simple and works well.
Leaky Bucket
Leaky bucket is very similar, but you imagine a bucket that leaks water at a constant rate.
Two versions exist:
- Queue based: Requests are queued and processed at a constant rate.
- Token-bucket-like: Implementation is mathematically similar to token bucket.
In many practical API systems, a token bucket is sufficient and conceptually easier.
Where To Apply Rate Limiting
Which Endpoints Need It Most
You should not treat all endpoints the same. Some are more sensitive.
Common priorities:
- Authentication endpoints
/login,/register,/password-reset,/email/verify- Protect against brute force, password guessing, and abuse
- Heavy or expensive endpoints
- Reports, exports, complicated database queries
- Endpoints that trigger background jobs, emails, or third-party calls
- Public APIs or anonymous endpoints
/search,/public/data- Rate limit by IP to stop scraping or unintended DoS
- Write operations
POST /orders,POST /comments- Reduce abuse or spam and protect databases
Example design:
| Endpoint | Identifier | Limit |
|---|---|---|
POST /auth/login | IP + Username | 5 attempts / 10 minutes |
POST /auth/password-reset | IP + Email | 3 emails / hour |
GET /search | IP | 60 requests / minute |
POST /orders | User ID | 10 orders / minute |
GET /public/* | IP | 100 requests / minute |
HTTP Responses For Rate Limits
Status Codes
When you block a request due to rate limiting, you should use:
- 429 Too Many Requests
Example response body:
{
"detail": "Too Many Requests",
"message": "You have exceeded the limit of 100 requests per minute.",
"retry_after_seconds": 30
}Other codes like 403 or 503 are sometimes used, but 429 is the most appropriate and standard.
Useful Rate Limiting Headers
Many APIs expose rate limit information through HTTP headers.
Common headers:
| Header | Example value | Meaning |
|---|---|---|
X-RateLimit-Limit | 100 | Maximum requests allowed in the current window |
X-RateLimit-Remaining | 20 | Requests remaining in the current window |
X-RateLimit-Reset | 1693142400 | Unix timestamp when the limit resets |
Retry-After | 30 | Seconds until the client should retry |
Example response during normal use:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1693142400
{"data": "..."}Example when blocked:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1693142460
{"detail": "Too Many Requests"}These headers help clients behave politely and avoid hitting limits accidentally.
Implementation Approaches
Local In-Memory vs Distributed
You have to store counters or tokens somewhere.
1. In-Memory (Inside Each Application Instance)
- Use a dictionary or cache inside each application process.
Example (Python pseudo):
from collections import defaultdict
import time
REQUEST_COUNTS = defaultdict(list)
def is_allowed(ip, limit=100, window_seconds=60):
now = time.time()
window_start = now - window_seconds
# Remove old timestamps
timestamps = [t for t in REQUEST_COUNTS[ip] if t >= window_start]
REQUEST_COUNTS[ip] = timestamps
if len(timestamps) >= limit:
return False
REQUEST_COUNTS[ip].append(now)
return TruePros:
- Simple to implement
- No external dependency
Cons:
- Each server instance has its own memory
- Users can bypass limits by switching between instances
- Not suitable for multi-instance or distributed setups
2. Centralized Store (Redis, Database)
Use a shared store like Redis, which supports:
- Atomic increments
- Expiration
- Fast operations
This is the most common approach in production backends.
Simple fixed window with Redis:
def is_allowed(ip, limit=100, window_seconds=60):
window_id = int(time.time() // window_seconds)
key = f"rate:ip:{ip}:{window_id}"
count = redis.incr(key)
if count == 1:
redis.expire(key, window_seconds)
return count <= limitPros:
- Works across multiple instances
- Centralized view of usage
- Easy to tune
Cons:
- Extra infrastructure
- Need to handle Redis outages
Defensive Design Considerations
What Happens When The Rate Limiter Store Fails?
If Redis or your store is down, you must decide:
- Fail open: Allow all requests, ignore rate limiting
- Fail closed: Block all requests
In practice:
- For a small internal app, you might fail open.
- For a high security system, you may want to be stricter on login-related endpoints.
Sometimes you can:
- Use a backup in-memory limiter if Redis fails, to at least protect your system partially.
Trusted vs Untrusted Paths
You may want to:
- Apply strict limits on public internet traffic
- Relax limits for internal services inside your private network
- Use different limits for premium customers vs free-tier users
Rate Limiting And Security
Brute Force Protection
Brute force attacks try many passwords or tokens until they guess correctly.
Rate limiting is a strong defense when combined with:
- Proper password hashing
- Account lockouts or step-up challenges (captcha, email verification)
Example login policy:
- 5 failed attempts per IP per 10 minutes
- 10 failed attempts per username per hour
- If exceeded, require captcha or temporary lockout
This makes large-scale guessing attacks very slow or impractical.
Protection Against API Abuse
Public APIs are attractive targets for:
- Data scraping
- DDoS-like abuse
- Uncontrolled bots
Rate limiting per API key, user, and/or IP helps:
- Keep your infrastructure safe
- Enforce usage plans (for example, free vs paid tiers)
Fairness And Strategy
Multiple Limits Per Client
A realistic system rarely uses only one limit. You can combine several:
Example for a public API:
- Per IP: 100 requests per minute
- Per user account: 1,000 requests per hour
- Per API key: 10,000 requests per day
The idea is:
- IP limit: Blocks obvious abusive IPs
- User limit: Ensures fairness between users
- API key limit: Enforces business usage plans
If any of these is exceeded, the request is blocked.
Soft vs Hard Limits
- Hard limit: The user is immediately blocked when the limit is reached.
- Soft limit: After passing a soft limit, you may:
- Degrade service quality
- Add delays
- Show warnings
Example:
- Up to 100 requests / minute, full speed
- After 100, slow down responses or introduce artificial delay
- After 200, start returning 429
Practical Examples
Example: Login Rate Limiting With IP And Username
Goal: Stop brute force attacks on login.
Rules:
- Max 5 failed logins per IP per 10 minutes
- Max 10 failed logins per username per hour
Pseudocode:
def can_attempt_login(ip, username):
if not is_ip_allowed(ip, limit=5, window=600):
return False, "Too many attempts from this IP."
if not is_username_allowed(username, limit=10, window=3600):
return False, "Too many attempts for this account."
return True, None
def on_login_attempt(ip, username, password):
allowed, reason = can_attempt_login(ip, username)
if not allowed:
return error_429(reason)
if authenticate(username, password):
reset_fail_counters(ip, username) # optional
return success()
else:
increment_fail_counters(ip, username)
return error_invalid_credentials()This combines security (against attackers) and usability (legitimate users still have several chances).
Example: Public Search Endpoint With Token Bucket
Goal: Allow bursts but control average rate.
Rule:
- Per IP, allow average 10 requests / second, burst up to 20.
Parameters:
- Bucket capacity $B = 20$
- Refill rate $R = 10$ tokens per second
Behavior:
- If idle for a while, bucket fills to 20 tokens.
- User can then make up to 20 fast requests.
- After that, they must keep average usage around 10 per second.
This avoids constantly hitting 429 during short bursts but still protects your system from sustained floods.
Designing Reasonable Limits
How To Choose Numbers
Some basic guiding questions:
- How expensive is each request for your backend?
- Heavy database or CPU work requires stricter limits.
- How quickly should a normal user be able to use the feature?
- For UI actions, users rarely click more than a few times per second.
- Are you dealing with humans or automated systems?
- Human users: 1 to 10 requests / second is usually enough
- Machine clients: may need higher, but you can still cap them
- Are there business constraints, such as pricing tiers?
- Free tier: lower limits
- Paid tiers: higher limits
Example guidelines:
| Use case | Reasonable starting limit |
|---|---|
| Web page views per IP | 60 requests / minute |
| Login attempts per IP | 5 attempts / 10 minutes |
| Search API per API key | 10 requests / second, 1,000 / hour |
| File upload endpoint | 10 uploads / hour per user |
| Password reset emails | 3 emails / hour per email address |
These are just starting points. You must monitor usage and adjust.
Summary
Rate limiting is a key part of backend security and stability. The main ideas:
- Decide who you are limiting, using identifiers like IP, user ID, API key, or combinations.
- Choose a rate limiting algorithm:
- Fixed window for simplicity
- Sliding window or logs for fairness
- Token bucket to allow bursts with controlled averages
- Apply strict limits to sensitive endpoints, especially authentication and expensive operations.
- Use HTTP 429 and helpful headers like
X-RateLimit-*andRetry-After. - Implement the limiter using in-memory storage for simple apps, or Redis / a database for distributed systems.
- Combine multiple limits per user, IP, and API key to balance security, fairness, and user experience.
With well-designed rate limiting, you significantly reduce the risk of brute force attacks and overload, and you keep your backend more reliable for everyone.
Views: 8
KAHIBARO