KAHIBARO
Discord Login Register

15.9. Rate Limiting

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:

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:

  1. Who is limited
  2. How many requests they can make in what time window

Common examples:

Who is limitedLimit ExampleMeaning
IP address100 requests per 1 minuteEach IP can make 100 requests every 60 seconds
User account (user ID)10 login attempts per 15 minsEach user can try to log in 10 times in 15 mins
API key1,000 API calls per hourEach key can call the API 1,000 times per hour
Entire system (global)500 requests per second totalSystem will only process 500 RPS globally

Time Windows

Common time windows:

You can combine windows, for example:

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:

IdentifierWhen to useProsCons
IP addressUnauthenticated or public endpointsEasy to get, no login requiredShared IPs, NAT, VPNs, mobile networks
User IDAuthenticated usersStable per user, fairer than IPNeeds authentication
API keyPublic APIs, 3rd-party consumersDirectly tied to a consumer or applicationNeeds key management
Session IDWeb apps with cookies/sessionsTreat each session as separate clientSession rotation can complicate counts
Device IDMobile appsPer-device controlRequires device ID logic

In practice, you often combine identifiers, for example:

Simple Rate Limiting Strategies

Fixed Window

Fixed window rate limiting is the simplest approach.

Example rule:

You divide time into fixed windows, for example:

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:

  1. Compute window key, for example, current_minute = floor(current_time / 60)
  2. Identify client, for example, IP address
  3. Key in storage: rate:ip:<ip>:<current_minute>
  4. Increment counter
  5. 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:

python
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

ProsCons
Very simple conceptuallyWindows reset suddenly at boundary
Easy to implement with RedisShort bursts across boundary can slip by
Fast and cheapLess fair close to window reset

Boundary problem example:

Sliding Window

Sliding window methods make rate limiting smoother by considering a moving time window instead of strict 1-minute blocks.

Two common variants:

  1. Sliding window counter (approximate)
  2. 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

$$
\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:

  1. Remove timestamps older than now - window_size
  2. Count remaining timestamps
  3. If count >= limit, block
  4. Else, record current timestamp and allow

Storage example in Redis, key per user like: rate:user:123 with scores as timestamps.

Pros:

Cons:

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.”

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:

  1. Bucket capacity $B$
  2. Refill rate $R$ tokens per second

Example:

Behaviour:

Simple Token Bucket Formula

We often store:

On each request at time $t$:

  1. Calculate time difference: $\Delta t = t - t_{last}$
  2. Add new tokens:

$$
T = \min(B, T + \Delta t \cdot R)
$$

  1. Set $t_{last} = t$
  2. If $T \ge 1$, then:
    • Allow request
    • Decrease $T$ by 1
  3. 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:

  1. Queue based: Requests are queued and processed at a constant rate.
  2. 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:

  1. Authentication endpoints
    • /login, /register, /password-reset, /email/verify
    • Protect against brute force, password guessing, and abuse
  2. Heavy or expensive endpoints
    • Reports, exports, complicated database queries
    • Endpoints that trigger background jobs, emails, or third-party calls
  3. Public APIs or anonymous endpoints
    • /search, /public/data
    • Rate limit by IP to stop scraping or unintended DoS
  4. Write operations
    • POST /orders, POST /comments
    • Reduce abuse or spam and protect databases

Example design:


EndpointIdentifierLimit
POST /auth/loginIP + Username5 attempts / 10 minutes
POST /auth/password-resetIP + Email3 emails / hour
GET /searchIP60 requests / minute
POST /ordersUser ID10 orders / minute
GET /public/*IP100 requests / minute

HTTP Responses For Rate Limits

Status Codes

When you block a request due to rate limiting, you should use:

Example response body:

json
{
  "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:

HeaderExample valueMeaning
X-RateLimit-Limit100Maximum requests allowed in the current window
X-RateLimit-Remaining20Requests remaining in the current window
X-RateLimit-Reset1693142400Unix timestamp when the limit resets
Retry-After30Seconds until the client should retry

Example response during normal use:

http
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1693142400
{"data": "..."}

Example when blocked:

http
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)

Example (Python pseudo):

python
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 True

Pros:

Cons:

2. Centralized Store (Redis, Database)

Use a shared store like Redis, which supports:

This is the most common approach in production backends.

Simple fixed window with Redis:

python
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 <= limit

Pros:

Cons:

Defensive Design Considerations

What Happens When The Rate Limiter Store Fails?

If Redis or your store is down, you must decide:

In practice:

Sometimes you can:

Trusted vs Untrusted Paths

You may want to:

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:

Example login policy:

This makes large-scale guessing attacks very slow or impractical.

Protection Against API Abuse

Public APIs are attractive targets for:

Rate limiting per API key, user, and/or IP helps:

Fairness And Strategy

Multiple Limits Per Client

A realistic system rarely uses only one limit. You can combine several:

Example for a public API:

The idea is:

If any of these is exceeded, the request is blocked.

Soft vs Hard Limits

Example:

Practical Examples

Example: Login Rate Limiting With IP And Username

Goal: Stop brute force attacks on login.

Rules:

Pseudocode:

python
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:

Parameters:

Behavior:

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:

  1. How expensive is each request for your backend?
    • Heavy database or CPU work requires stricter limits.
  2. 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.
  3. 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
  4. Are there business constraints, such as pricing tiers?
    • Free tier: lower limits
    • Paid tiers: higher limits

Example guidelines:

Use caseReasonable starting limit
Web page views per IP60 requests / minute
Login attempts per IP5 attempts / 10 minutes
Search API per API key10 requests / second, 1,000 / hour
File upload endpoint10 uploads / hour per user
Password reset emails3 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:

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

Comments

Please login to add a comment.

Don't have an account? Register now!