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

Sponsored

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:

  1. Install a rate limiting libraryslowapi is standard, but you could use starry, limits, or write a custom decorator.
  2. Configure the limiter — Define a global limits instance with a storage backend (in-memory, Redis, etc.) and a default policy.
  3. Attach the limiter to your FastAPI app — Add lifespan or use the app state to make the limiter accessible.
  4. Decorate endpoints — Use @limiter.limit("5/minute") to apply a policy.
  5. Handle 429 responses — Set a custom exception handler to return a clean JSON error instead of a bare HTML error.
  6. 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 Request parameter, or you didn't decorate the function. FastAPI will throw a TypeError if request is 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_address returns the proxy's IP if you don't set the X-Forwarded-For header 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-Limit and X-RateLimit-Remaining headers. slowapi adds 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 limits library).

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 Request parameter to an endpoint decorated with @limiter.limit — FastAPI will throw a TypeError and 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 RateLimitExceeded exception handler — clients get a bare 500 or 404 instead of a clean 429 JSON.
  • Ignoring proxy headers — get_remote_address returns the proxy IP unless you configure X-Forwarded-For and the uwsgi or proxy middleware correctly.

Variations

  1. Use starry library for a lighter-weight, declarative rate limiting approach (similar to slowapi but with different API).
  2. Implement a custom middleware using BaseHTTPMiddleware for complete control over rate limiting logic — useful if you want to avoid third-party dependencies.
  3. Offload rate limiting to an API gateway (e.g., Nginx limit_req or 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.
  • slowapi is the standard FastAPI integration; it uses the limits library for token bucket / sliding window algorithms.
  • Always include a Request parameter 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.