Is Your FastAPI Ready to Handle a Flood of Requests the Smart Way?

Fortifying Your FastAPI with Redis-Powered Rate Limiting

Is Your FastAPI Ready to Handle a Flood of Requests the Smart Way?

Rate limiting is something every API needs to keep things running smoothly. It helps ensure that an application isn’t bogged down by too many requests at once. For those building with FastAPI, integrating rate limiting is not just a nice-to-have but a necessity. In this piece, let’s break down how to get this up and running using Redis, which is super-fast and reliable.

So, why even bother with rate limiting? For starters, it’s a great way to prevent abuse. Picture this: a malicious user floods your API with tons of requests. Without rate limiting in place, this could lead to serious problems, including a DoS attack. Plus, by controlling the number of requests, you’re optimizing performance and keeping your service quick for everyone else. And let’s not forget about costs—more requests mean higher infrastructure costs. Rate limiting helps keep those in check too.

Redis is a fantastic choice for implementing rate limiting. Since it operates in memory, its read and write speeds are blazingly fast. This speed is crucial when real-time rate limiting is the goal. Moreover, when your app runs on multiple instances, Redis provides a centralized storage solution. This ensures your rate limiting remains consistent across all those instances.

Getting started with Redis and FastAPI is pretty straightforward. First off, you’ll need a running Redis server. Using Docker can simplify this:

docker-compose up redis -d

Once that’s sorted, install the required Python package. Here, we’ll go with fastapi_redis_rate_limiter:

pip install fastapi_redis_rate_limiter

Next, you integrate the rate limiter with your FastAPI app. Here’s a basic example:

from fastapi import FastAPI
from fastapi_redis_rate_limiter import RedisRateLimiterMiddleware, RedisClient

app = FastAPI()

# Initialize the Redis client
redis_client = RedisClient(host="localhost", port=6379, db=0)

# Apply the rate limiter middleware to the app
app.add_middleware(RedisRateLimiterMiddleware, redis_client=redis_client, limit=40, window=60)

@app.get("/limited")
async def limited():
    return {"message": "This is a protected endpoint."}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

In this setup, the Redis client hooks into the FastAPI app, and the middleware enforces a limit of 40 requests per 60 seconds.

Of course, you’ll want to tweak these limits based on your application’s specific needs. For instance, what if you want to limit requests based on the user rather than IP? This is where customizability shines. Using slowapi, you can implement user-based rate limiting, and store the state in Redis.

Here’s an example of how to do it:

from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.middleware import SlowAPIMiddleware
from fastapi import FastAPI, Request
from redis import Redis
import uvicorn

# Initialize Redis and SlowAPI Limiter
redis = Redis(host='localhost', port=6379)
limiter = Limiter(key_func=get_remote_address, storage_uri="redis://localhost:6379")

app = FastAPI()
app.state.limiter = limiter
app.add_middleware(SlowAPIMiddleware)

@app.get("/")
@limiter.limit("5/minute")
async def root(request: Request):
    return {"message": "Hello, World!"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

This example limits requests to 5 per minute. The get_remote_address function identifies the client’s IP, and the limiter uses Redis to keep track of the state.

Sometimes just the basic rate limiting isn’t enough. For more advanced needs, consider distributed rate limiting if your app runs on multiple servers. Here’s another example, emphasizing consistent limits across instances, using a centralized Redis store:

from fastapi import FastAPI, Request
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.middleware import SlowAPIMiddleware
from redis import Redis
import uvicorn

# Initialize Redis and SlowAPI Limiter
redis = Redis(host='localhost', port=6379)
limiter = Limiter(key_func=get_remote_address, storage_uri="redis://localhost:6379")

app = FastAPI()
app.state.limiter = limiter
app.add_middleware(SlowAPIMiddleware)

@app.get("/")
@limiter.limit("5/minute")
async def root(request: Request):
    return {"message": "Hello, World!"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

This showcases a centralized setup ensuring consistent rate limits no matter which instance the request hits.

Need user-based limits? Go beyond IP-based rate limiting with a custom middleware. Here’s a glimpse into setting per-user rate limits:

import hashlib
from datetime import datetime, timedelta
from typing import Callable, TypeVar
import uvicorn
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic_settings import BaseSettings, SettingsConfigDict
from redis.asyncio import Redis

F = TypeVar("F", bound=Callable[..., Any])

class Settings(BaseSettings):
    redis_password: str
    redis_host: str = "127.0.0.1"
    redis_port: int = 6379
    user_rate_limit_per_minute: int = 3

    model_config = SettingsConfigDict(env_file=".env", extra="ignore")

settings = Settings()

redis_client = Redis(
    host=settings.redis_host,
    port=settings.redis_port,
    db=0,
    decode_responses=True,
    password=settings.redis_password,
)

app = FastAPI(
    title="FastAPI Rate Limiting",
    description="Rate limiting users using Redis middleware",
)

async def rate_limit_user(user: str, rate_limit: int):
    username_hash = hashlib.sha256(bytes(user, "utf-8")).hexdigest()
    now = datetime.utcnow()
    current_minute = now.strftime("%Y-%m-%dT%H:%M")
    redis_key = f"rate_limit_{username_hash}_{current_minute}"
    current_count = await redis_client.incr(redis_key)

    if current_count == 1:
        await redis_client.expireat(name=redis_key, when=now + timedelta(minutes=1))

    if current_count > rate_limit:
        return JSONResponse(
            status_code=429,
            content={"detail": "User Rate Limit Exceeded"},
            headers={
                "Retry-After": f"{60 - now.second}",
                "X-Rate-Limit": f"{rate_limit}",
            },
        )

    return None

# Plug this into the API

In this setup, each user’s requests get hashed and checked against Redis, imposing limits as needed.

To keep things running silky smooth, consider performance optimizations. First off, use efficient algorithms. The Token Bucket algorithm, for example, handles burst traffic better than a Fixed Window Counter. Utilize asynchronous I/O in FastAPI to ensure that rate limiting checks don’t block other requests. Here’s a quick snippet:

@app.get("/async_endpoint")
@limiter.limit("10/minute")
async def async_endpoint():
    await asyncio.sleep(1)
    return {"message": "Asynchronous endpoint"}

Lastly, leverage cache headers to reduce backend load. Cache responses on the client side to minimize repetitive requests.

To wrap things up, implementing rate limiting in FastAPI with Redis isn’t too hard but packs a big punch in keeping your app responsive and cost-effective. By choosing the right tools and techniques, from basic to advanced setups, you can tailor rate limiting to fit your exact needs. So go ahead, fortify your API and keep things running like a charm!