How to Add an API Rate Limiter With Redis
An API rate limiter caps how many requests a single client can make in a time window - and returns 429 with a Retry-After header when they exceed it. Using Redis as the counter makes it work across every replica. Here's how to build one.
Why Redis, not an in-process counter
An in-memory dict works for a single-process server, but the moment you run two replicas each process holds its own counter, so a client can hit 5 req/min per replica - 10x the intended limit. Redis is the shared counter that every replica reads and writes atomically, keeping the window accurate regardless of how many instances are running.
The fixed-window pattern
The simplest correct approach: one Redis key per client, incremented on each request, with a 60-second TTL that auto-resets the window.
import redis
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
LIMIT = 5
WINDOW_SEC = 60
def check_rate_limit(client_id: str):
key = f"ratelimit:{client_id}"
count = r.incr(key) # atomic increment
if count == 1:
r.expire(key, WINDOW_SEC) # set TTL on first request
if count > LIMIT:
ttl = max(r.ttl(key), 1)
raise HTTPException(
status_code=429,
detail={"error": "rate limit exceeded", "retry_after": ttl},
headers={"Retry-After": str(ttl)},
)
@app.get("/api/search")
def search(q: str, request: Request):
client_id = (
request.headers.get("X-Client-Id")
or (request.client.host if request.client else "anonymous")
)
check_rate_limit(client_id)
return {"query": q, "results": []}
What each line does
r.incr(key)- atomically increments the counter and returns the new value. Redis guarantees this is race-free even with concurrent requests.r.expire(key, WINDOW_SEC)oncount == 1- sets the TTL only on the first hit, so the window clock starts when the client first appears. Subsequent increments don't reset the clock.r.ttl(key)- returns seconds remaining on the key, which becomes theRetry-Aftervalue. This tells the client exactly how long to wait before the counter resets to zero.
Testing the limiter
# Start the server
uvicorn main:app --host 0.0.0.0 --port 8000
# Fire 6 requests as the same client - 5 should pass, 6th should 429
for i in 1 2 3 4 5 6; do
curl -s -o /dev/null -w "%{http_code}\n" \
-H "X-Client-Id: alice" "http://localhost:8000/api/search?q=test"
done
# Inspect the counter and TTL in Redis
redis-cli GET ratelimit:alice
redis-cli TTL ratelimit:alice
Limits of the fixed-window approach
Fixed-window has one edge case: a client can fire 5 requests at 0:59 and 5 more at 1:01 - technically 10 in 2 seconds, both windows allow it. For stricter burst control, a sliding-window (using a Redis sorted set of timestamps) or a token bucket (Lua script, refills continuously) eliminates this seam. For most APIs the fixed-window is good enough and far simpler to operate.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Implementing a fixed-window rate limiter with Redis INCR + EXPIRE
- Identifying clients by header (X-Client-Id) with an IP fallback
- Returning 429 with a Retry-After header from a FastAPI endpoint
FAQ
How do I add rate limiting to a FastAPI endpoint?
Use Redis as a shared counter: INCR a key per client on each request, set a TTL on the first hit (the window length), and raise an HTTPException with status 429 and a Retry-After header when the count exceeds the limit.
Why use Redis for rate limiting instead of an in-memory counter?
An in-memory counter is per-process - if you run two replicas each has its own counter, effectively doubling the limit. Redis is shared, so every replica increments the same key and the limit holds correctly across all instances.
What is the Retry-After header in a 429 response?
Retry-After tells the client how many seconds to wait before retrying. Set it to r.ttl(key) - the remaining lifetime of the rate-limit counter in Redis - so the client knows the exact moment their window resets.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →