Add Rate Limiting to FastAPI
Add rate limiting to FastAPI endpoints — Secure development tutorial, lesson 60. Hands-on steps, troubleshooting, and what to study next.
Focus: add rate limiting to fastapi endpoints
Your FastAPI endpoint is fast, elegant, and production-ready — until an attacker (or a runaway script) hammers it with thousands of requests per second. Without rate limiting, you're exposing your API to brute-force attacks, resource exhaustion, and inflated cloud bills. In this lesson, you'll learn how to add rate limiting to FastAPI endpoints, turning a fragile public surface into a resilient, controlled gateway. By the end, you'll not only protect your services but also understand the trade-offs between in-memory and distributed limits, and how to debug the inevitable edge cases.
The problem this lesson solves
APIs are the front door to your application, and like any door, they need a lock. Rate limiting is that lock — it caps how many requests a client can make in a given time window. Without it, you face three real threats:
- Brute-force attacks: Login endpoints become a playground for credential stuffing. An attacker tries thousands of passwords per minute, and your server happily processes each one.
- Resource exhaustion: A single client can spawn unlimited connections, exhausting database connections, memory, or CPU. This can cause a denial-of-service (DoS) condition, crashing your app or slowing it down for everyone.
- Cost overruns: Every request may trigger a paid API call (like OpenAI or a cloud storage service). Unbounded usage translates directly into a surprise invoice.
Pro tip: Rate limiting isn't just about security — it's also a business policy. You might allow free-tier users 10 requests per minute and premium users 1,000. Implementing limits early makes future monetization painless.
Core concept / mental model
Think of rate limiting as a turnstile at a stadium entrance. Each fan (client) gets a ticket (request). The turnstile only lets one person through per second. If they try to push through faster, they're blocked until the next slot opens.
In technical terms, rate limiting uses a token bucket or sliding window algorithm:
- Token bucket: A bucket holds a fixed number of tokens. Each request consumes one token. Tokens refill at a fixed rate. If the bucket is empty, the request is rejected. This allows bursts up to the bucket size but enforces a steady average rate.
- Sliding window: Keeps track of request timestamps in the current time window (e.g., last 60 seconds). If the count exceeds the limit, reject. This is more accurate but uses more memory.
FastAPI itself doesn't include rate limiting, but it integrates with middleware and dependencies. You'll typically use a library like slowapi (which wraps limits) or implement your own middleware. The concept stays the same: track requests per client, enforce a ceiling, respond with 429 Too Many Requests when exceeded.
How it works step by step
Here's the logical flow of adding rate limiting to a FastAPI endpoint:
- Install a rate limiting library —
slowapiis standard, but you could usestarry,limits, or write a custom decorator. - Configure the limiter — Define a global limits instance with a storage backend (in-memory, Redis, etc.) and a default policy.
- Attach the limiter to your FastAPI app — Add
lifespanor use the app state to make the limiter accessible. - Decorate endpoints — Use
@limiter.limit("5/minute")to apply a policy. - Handle 429 responses — Set a custom exception handler to return a clean JSON error instead of a bare HTML error.
- Test thoroughly — Simulate burst requests to verify the limit kicks in.
Cause and effect: If a client exceeds the limit, the middleware intercepts the request, increments the counter, and returns a 429 before your endpoint code runs. This prevents resource usage at the business logic level — the request never reaches your database or computation.
Hands-on walkthrough
Let's build a working example. We'll create a small FastAPI app with rate limiting on a public status endpoint and a protected login endpoint.
First, install the dependencies:
pip install fastapi uvicorn slowapi
Now create main.py:
from fastapi import FastAPI, Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
app = FastAPI()
# Use client IP as the default key
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.get("/")
@limiter.limit("10/minute")
async def root(request: Request):
return {"message": "Hello, world!"}
@app.post("/login")
@limiter.limit("5/minute")
async def login(request: Request):
# Simulate credentials check
return {"token": "fake-jwt-token"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Run the app:
uvicorn main:app --reload
Test it with curl or a browser. If you hit http://127.0.0.1:8000 more than 10 times in a minute, you'll get:
{"error": "Rate limit exceeded: 10 per 1 minute"}
But there's a catch: when using @limiter.limit, your endpoint must accept a request: Request parameter, even if you don't use it. That's how slowapi identifies the client.
For a more granular approach, you can limit per user (if authenticated) instead of per IP:
from fastapi import Depends
from slowapi.util import get_remote_address
def get_user_id(request: Request) -> str:
return request.headers.get("X-User-ID", get_remote_address(request))
limiter = Limiter(key_func=get_user_id)
This lets you tie limits to user accounts rather than IP addresses, which is more accurate for mobile users behind NAT.
Compare options / when to choose what
| Approach | Library / Method | Key Characteristic | When to Use |
|---|---|---|---|
| In-memory (slowapi default) | slowapi with memory storage |
No external dependency, resets on restart | Single-node apps, development, or very small deployments |
| Redis-backed | slowapi with limits using Redis |
Distributed, persists across restarts, handles multiple instances | Production with multiple replicas, high-traffic APIs |
| Custom middleware | Hand-written BaseHTTPMiddleware |
Full control, no library dependency | When you need custom logic (e.g., per-route rules) or want to avoid adding dependencies |
| API Gateway / Reverse proxy | Nginx limit_req, Cloudflare, Kong |
Offloads enforcement from app | When you want edge-level protection and central management |
Key points:
- Redis is non-negotiable for multi-instance deployments — otherwise each instance has its own counter, and a user can hit 10x the limit in a cluster of 10.
- Reverse proxies add overhead but can be simpler for uniform policies across all microservices.
- Custom middleware gives you ultimate flexibility but is more code to maintain and easy to get wrong.
Here's a Redis-backed setup with slowapi — install redis and limits first (pip install redis limits):
from slowapi import Limiter
from slowapi.util import get_remote_address
from limits.storage import RedisStorage
storage = RedisStorage("redis://localhost:6379")
limiter = Limiter(key_func=get_remote_address, storage_uri="redis://localhost:6379")
# Alternatively: limiter = Limiter(key_func=get_remote_address, storage_uri="memory://")
Now restart your app — limits persist across restarts, and multiple instances share the same counter.
Troubleshooting & edge cases
Rate limiting seems simple, but real-world issues abound. Here are the pitfalls to watch for:
- Endpoint not enforcing the limit: You forgot the
Requestparameter, or you didn't decorate the function. FastAPI will throw aTypeErrorifrequestis missing. Always check the console logs. - Limits reset on every restart: In-memory storage loses counters. If you need persistence, use Redis.
- Client IP is wrong behind a proxy:
get_remote_addressreturns the proxy's IP if you don't set theX-Forwarded-Forheader properly. Configure your proxy to pass the real client IP, or use a custom key function. - Slowapi returns 404 on 429: If you didn't add the exception handler, FastAPI returns a generic error. Add
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)as shown above. - Headers missing: Clients often expect
X-RateLimit-LimitandX-RateLimit-Remainingheaders.slowapiadds them by default, but if you custom-handle the exception, you might lose them. Include them manually in your 429 response. - Bursty traffic: The token bucket allows short bursts, which is fine. But if you need hard limits per window, use sliding window algorithm (available in
limitslibrary).
Example of a custom 429 handler with headers:
from fastapi.responses import JSONResponse
async def custom_rate_limit_handler(request: Request, exc: RateLimitExceeded):
response = JSONResponse(
status_code=429,
content={"error": "Too many requests", "retry_after": exc.retry_after},
)
response.headers["X-RateLimit-Limit"] = str(exc.limit.limit)
response.headers["X-RateLimit-Remaining"] = str(exc.limit.remaining)
return response
app.add_exception_handler(RateLimitExceeded, custom_rate_limit_handler)
What you learned & what's next
You now understand how to add rate limiting to FastAPI endpoints, covering both the core concept (token bucket / sliding window) and practical implementation with slowapi. You've seen how to configure it, apply it per endpoint, and handle errors gracefully. You've also compared in-memory vs. Redis storage and learned to troubleshoot common pitfalls like proxy IPs and missing parameters.
Key takeaways:
- Rate limiting protects APIs from abuse and cost overruns.
- slowapi is the go-to library; always include a Request parameter in decorated endpoints.
- Use Redis for multi-instance deployments; in-memory is fine for single-node demos.
- Always add a custom 429 handler to return clean JSON.
- Headers like X-RateLimit-Remaining help clients self-throttle.
In the next lesson, you'll build on this foundation by exploring authentication and authorization — securing your endpoints with OAuth2 and JWT. Rate limiting will be your first line of defense; auth will be your second.
Practice recap
Practice: Extend the example by adding a /dashboard endpoint with a stricter limit (e.g., 2/minute) and test it with curl. Then, switch to Redis storage (install redis and run a local Redis server) and verify that limits persist across a restart. Finally, add custom headers like X-RateLimit-Remaining to your 429 response using the custom exception handler.
Common mistakes
- Forgetting to add a
Requestparameter to an endpoint decorated with@limiter.limit— FastAPI will throw aTypeErrorand the endpoint won't work. - Using default in-memory storage in a multi-instance deployment — each instance keeps its own counter, effectively multiplying the allowed limit.
- Not adding the
RateLimitExceededexception handler — clients get a bare 500 or 404 instead of a clean 429 JSON. - Ignoring proxy headers —
get_remote_addressreturns the proxy IP unless you configureX-Forwarded-Forand theuwsgiorproxymiddleware correctly.
Variations
- Use
starrylibrary for a lighter-weight, declarative rate limiting approach (similar toslowapibut with different API). - Implement a custom middleware using
BaseHTTPMiddlewarefor complete control over rate limiting logic — useful if you want to avoid third-party dependencies. - Offload rate limiting to an API gateway (e.g., Nginx
limit_reqor Cloudflare) to enforce limits at the edge, before requests reach your app.
Real-world use cases
- A public API for a weather service that allows 100 free requests/hour per IP to prevent abuse and upsell premium tiers.
- An authentication endpoint in a banking app that limits login attempts to 5 per minute per user to block credential-stuffing attacks.
- A social media platform limiting tweet posting to 10 per minute per user to prevent spam and excessive load on the database.
Key takeaways
- Rate limiting caps requests per time window to prevent brute force, resource exhaustion, and cost overruns.
slowapiis the standard FastAPI integration; it uses thelimitslibrary for token bucket / sliding window algorithms.- Always include a
Requestparameter in decorated endpoints and add a custom 429 handler. - Choose Redis-backed storage for production multi-instance setups; in-memory is fine for single-node testing.
- Consider edge-level rate limiting (Nginx, Cloudflare) when you need uniform policies across all services.
- Troubleshoot proxy IPs and custom headers early to avoid incorrect client identification.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.