Introduction

Welcome to the very first lesson of the Implementing Rate Limiting course! 🚀

In this lesson, we will explore rate limiting, a crucial technique for enhancing the security of your API. Let's dive in! 🎉

What is Rate Limiting?

Rate limiting is a strategy that controls how many requests a client (identified by IP address, API key, or other identifiers) can make to your API within a specified time period. When the limit is reached, subsequent requests are blocked until the time period resets.

For example, you might configure your API to allow each client:

  • No more than 5 requests per minute.
  • No more than 100 requests per hour.
  • No more than 1000 requests per day.
Why Implement Rate Limiting?

Rate limiting serves several important purposes:

  • Prevents server overload – Protects your server resources from being overwhelmed.
  • Defends against DoS attacks – Makes it harder for attackers to flood your service.
  • Ensures fair usage – Prevents a single client from monopolizing your API.
  • Manages traffic spikes – Helps maintain consistent performance during high-traffic periods.
  • Reduces costs – Limits resource consumption for services with usage-based pricing.
Rate Limiters as FastAPI Middleware

SlowAPI is a popular Python library specifically designed to add rate limiting to FastAPI and Starlette applications. It's built on top of the well-established Flask-Limiter library, adapting its proven rate-limiting capabilities for async frameworks. SlowAPI integrates seamlessly with FastAPI's middleware system to provide both global and per-route rate limiting.

Rate limiters in FastAPI work as middleware components:

  1. The SlowAPI limiter creates middleware that sits between client requests and your route handlers
  2. When requests arrive, this middleware checks if the client has exceeded their limit
  3. If the limit is exceeded, it responds with a 429 error before the request reaches your routes
  4. If within limits, it allows the request to continue to your route handlers

This middleware pattern lets rate limiters intercept and filter requests before they reach your application logic.

Current API Routes Setup

Let's look at a simplified version of our current route setup in backend/main.py:

# backend/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .routes import auth, snippets, admin

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Include routers
app.include_router(auth.router, prefix="/api/auth")
app.include_router(snippets.router, prefix="/api/snippets")
app.include_router(admin.router, prefix="/api/admin")

Our snippet router includes a test endpoint we can use to demonstrate rate limiting:

# Simplified version of backend/routes/snippets.py
from fastapi import APIRouter, Request

router = APIRouter()

# Test endpoint for rate limiting
@router.get("/test-rate-limit")
async def test_rate_limit(request: Request):
    return {"message": "This endpoint is just for testing rate limiting"}

# Other endpoints would be here

Currently, there is no rate limiting applied to any of these endpoints, making our API vulnerable to request flooding.

Exploiting the Vulnerability

An attacker could target these endpoints with repeated calls:

# Simulate an attack using our test endpoint
for i in {1..100}; do
  curl http://localhost:3000/api/snippets/test-rate-limit
done

This could overwhelm our server, especially for resource-intensive operations.

Implementing a Global Rate Limiter, Step 1: Import the rate limiter packages

To protect our API, we'll add rate limiting at the global level in our main.py file.

First, we need to import the SlowAPI packages:

from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
Step 2: Configure the rate limiter

Next, we'll create a rate limiter with these important configuration options:

# Configure global API rate limiter
limiter = Limiter(key_func=get_remote_address, default_limits=["5/30seconds"])
app.state.limiter = limiter

The configuration means:

  • key_func=get_remote_address: Track requests by IP address
  • default_limits=["5/30seconds"]: Allow maximum 5 requests per 30 seconds per client
  • app.state.limiter = limiter: Attach the limiter to the FastAPI app state
Step 3: Create and register the error handler, then add middleware

Now, we'll create a custom error handler function, register it with FastAPI, and then add the SlowAPI middleware:

# Custom error handler for rate limit exceeded
def custom_rate_limit_handler(request: Request, exc: RateLimitExceeded):
    from fastapi.responses import JSONResponse
    return JSONResponse(
        status_code=429,
        content="Too many requests, please try again later."
    )

# Register the custom error handler
app.add_exception_handler(RateLimitExceeded, custom_rate_limit_handler)

# Add slowapi middleware to enable automatic rate limiting
app.add_middleware(SlowAPIMiddleware)

Important notes:

  • The handler function is synchronous (not async) - SlowAPI requires this
  • We use app.add_exception_handler() to register the handler, not a decorator
  • The exception handler must be registered before adding the SlowAPIMiddleware
  • The middleware automatically applies the default rate limits to all routes
Complete Implementation

Here's how our complete main.py file looks with global rate limiting:

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from .routes import auth, snippets, admin

app = FastAPI()

# Configure global API rate limiter: 5 requests per 30 seconds per IP
limiter = Limiter(key_func=get_remote_address, default_limits=["5/30seconds"])
app.state.limiter = limiter

# Custom error handler for rate limit exceeded
def custom_rate_limit_handler(request: Request, exc: RateLimitExceeded):
    return JSONResponse(
        status_code=429,
        content="Too many requests, please try again later."
    )

# Register the custom error handler
app.add_exception_handler(RateLimitExceeded, custom_rate_limit_handler)

# Add slowapi middleware to enable automatic rate limiting
app.add_middleware(SlowAPIMiddleware)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Include routers
app.include_router(auth.router, prefix="/api/auth")
app.include_router(snippets.router, prefix="/api/snippets")
app.include_router(admin.router, prefix="/api/admin")
Testing the Rate Limiter

We can test our rate limiter implementation using this Python test script:

# test_global_limit.py
import urllib.request
import urllib.error
import time

def test_rate_limit():
    print("Testing global rate limit...")
    
    print("\nTesting rate limit with test endpoint:")
    
    for i in range(1, 11):
        try:
            with urllib.request.urlopen('http://localhost:3000/api/snippets/test-rate-limit') as response:
                if response.getcode() == 200:
                    print(f"Request {i}: {response.getcode()} Success")
                else:
                    print(f"Request {i}: {response.getcode()} {response.read().decode()}")
        except urllib.error.HTTPError as e:
            print(f"Request {i}: {e.code} {e.read().decode()}")
        except Exception as e:
            print(f"Request {i} failed: {str(e)}")
        
        # Add a small delay between requests
        time.sleep(0.01)

if __name__ == "__main__":
    test_rate_limit()

When you run this script, you'll see output similar to this:

Testing global rate limit...

Testing rate limit with test endpoint:
Request 1: 200 Success
Request 2: 200 Success
Request 3: 200 Success
Request 4: 200 Success
Request 5: 200 Success
Request 6: 429 "Too many requests, please try again later."
Request 7: 429 "Too many requests, please try again later."
Request 8: 429 "Too many requests, please try again later."
Request 9: 429 "Too many requests, please try again later."
Request 10: 429 "Too many requests, please try again later."

This confirms our rate limiter is working properly - allowing the first 5 requests and blocking subsequent ones.

Note: Your actual output may vary slightly depending on timing factors such as system delays or longer gaps between requests. The key pattern to observe is that after approximately 5 requests, you should start seeing 429 responses.

Conclusion and Next Steps

In this lesson, we learned how to implement a global rate limiter to protect all of our API routes at once. This approach provides a baseline level of protection against request flooding.

By adding just a few lines of code to our main FastAPI application, we've significantly improved the security of our application against potential DoS attacks.

In future lessons, we'll explore more advanced rate limiting strategies, such as:

  • Setting different limits for different routes
  • Creating specialized limiters for authentication endpoints
  • Implementing more sophisticated rate-limiting algorithms

Remember that rate limiting is just one aspect of API security, but it's an essential first step in building a robust, secure application.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal