Rate Limiting with SlowAPI

Learn rate limiting basic endpoints with SlowAPI in this FastAPI Backend Development tutorial — hands-on steps, troubleshooting, and what to study next.

Focus: rate limiting basic endpoints with slowapi

Sponsored

You've built a FastAPI endpoint that's fast, clean, and well-documented — but what happens when a single client (or a scraper on a rampage) hits it 1,000 times per second? Your database starts sweating, your response times crawl, and your hosting bill balloons. This is precisely the problem rate limiting solves: it caps how often a client can call your API, protecting your resources and keeping your service responsive for everyone. In this lesson, you'll master rate limiting basic endpoints with SlowAPI — the de facto middleware for FastAPI — and walk away with hands-on code you can apply immediately.

The problem this lesson solves

APIs are meant to be consumed, but unlimited consumption can turn your backend into a bottleneck. Without rate limiting, you risk:

  • Resource exhaustion: A single user can hog your CPU, memory, and database connections.
  • Abuse and scraping: Automated bots can drain your endpoints, steal data, or break your business logic.
  • Poor user experience: One slow consumer can degrade latency for all your users.
  • Unexpected costs: Cloud providers bill by requests and bandwidth — spikes hurt your wallet.

Here's a real scenario: you deploy a public API that accepts user-generated content. Overnight, a malicious script floods your /posts endpoint with thousands of requests, overwhelming your database. Your health checks start failing, and your users see 500 errors. A rate limiter would have stopped this cold, returning 429 Too Many Requests instead of letting those requests through.

Core concept / mental model

Think of a rate limiter as a bouncer at a club. The club has a strict capacity: only 30 people can enter per minute. The bouncer keeps a clipboard with timestamps of each entry. If you've already entered 30 times this minute, you're politely turned away — but you're welcome to come back next minute.

In technical terms, rate limiting involves two components:

  • Limit: The maximum number of requests allowed in a given time window (e.g., 5 requests per minute).
  • Window: The time period during which the count resets (e.g., 1 minute, 1 hour, 1 day).

SlowAPI implements this with an in-memory storage by default, which is perfect for single-process development and simple deployments. For distributed systems, you can plug in Redis or Memcached backends, but the mental model stays the same: count requests per identifier (IP, user, or API key) within a sliding window.

Key insight: Rate limiting is an external policy. It doesn't change your endpoint's logic; it wraps it with a guard that decides whether to let a request through or reject it with HTTP 429.

How it works step by step

Here's the flow when a client hits a rate-limited endpoint with SlowAPI:

  1. Request arrives at your FastAPI application.
  2. SlowAPI's middleware examines the request — it looks for the client's IP address (or a custom key you've defined).
  3. It checks the request count against the limit and window you've configured.
  4. If the count is below the limit, the request proceeds to your endpoint, and the count increments.
  5. If the count exceeds the limit, SlowAPI immediately returns 429 Too Many Requests with a Retry-After header (if configured), and your endpoint code is never executed.

This happens transparently — you don't add a single line to your route function. The beauty of middleware is that you can apply rate limiting to groups of routes or the whole app with just a few decorators.

Hands-on walkthrough

Let's get your hands dirty. First, install SlowAPI:

pip install slowapi

Now, create a simple FastAPI app with rate-limited endpoints. We'll define limits using strings like "5/minute" — they read like English, which is a big win for maintainability.

# app.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

# Create a limiter instance
limiter = Limiter(key_func=get_remote_address)

app = FastAPI(title="Rate Limited API")
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

That's the boilerplate. Now let's apply a basic limit to a single endpoint:

@app.get("/ping")
@limiter.limit("5/minute")
async def ping(request: Request):
    return {"message": "pong"}

Notice the request: Request parameter — it's mandatory when using the limiter decorator. SlowAPI needs to inspect the request to determine the client's IP, so your endpoint must accept it.

Start your server and test:

uvicorn app:app --reload

Fire off six quick requests with curl:

for i in {1..6}; do
  curl -s -o /dev/null -w "Request $i: %{http_code}\n" http://localhost:8000/ping
  sleep 0.5
done

Expected output (roughly):

Request 1: 200
Request 2: 200
Request 3: 200
Request 4: 200
Request 5: 200
Request 6: 429

The sixth request hits the 5-per-minute ceiling and gets a 429. That's your rate limiter working!

Apply limits to groups of routes

What if you want to protect several endpoints under the same mount path? Use the @limiter.shared_limit decorator. Here's how:

@app.get("/items/{item_id}")
@limiter.shared_limit("10/minute", scope="items")
async def get_item(request: Request, item_id: int):
    return {"item_id": item_id, "name": f"Item {item_id}"}


@app.post("/items")
@limiter.shared_limit("10/minute", scope="items")
async def create_item(request: Request):
    return {"message": "Item created"}

Now both endpoints combined count toward the same 10-per-minute budget, which is perfect for a cohesive API section.

Rate limit a whole application or route group

Instead of decorating every endpoint manually, you can apply a global limit to every route by passing default_limits to Limiter:

limiter = Limiter(
    key_func=get_remote_address,
    default_limits=["20/minute"]
)

This guards your entire API — every endpoint gets at least 20 requests per minute unless overridden. Override by applying a @limiter.limit decorator on a specific route with a more lenient or stricter limit.

Custom key functions

You don't have to identify clients by IP. For authenticated APIs, you can use the current user's ID. Here's an advanced example using a FastAPI dependency:

from fastapi import Depends
from slowapi import Limiter


def get_client_key(request: Request):
    # Imagine you have authentication middleware that sets request.user_id
    return request.headers.get("X-User-ID", "anonymous")


limiter = Limiter(key_func=get_client_key)
app = Limiter._make_key_func(get_client_key)

# Wait—don't do that! read below

Correction: The key_func must be a function that takes a Request and returns a str. Here's the proper custom key:

def get_user_id(request: Request) -> str:
    # When you have OAuth or JWT, extract from the request state
    return request.state.user_id if hasattr(request.state, "user_id") else "anonymous"

limiter = Limiter(key_func=get_user_id)

Then decorate endpoints as usual — every user gets their own quota, independent of IP.

Compare options / when to choose what

Approach Use Case Pros Cons
Per-IP limit (default) Public APIs, unauthenticated endpoints Simple, no setup Can block multiple users behind NAT, evadable with VPN
Per-user limit Authenticated APIs Fair to users, ideal for subscriptions Requires authentication infrastructure
Per-API-key limit SaaS products, developer portals Granular control, monetization ready More complex to manage keys
Global app limit Internal services, protection against traffic spikes Easy to set up, broad protection May be too restrictive for some endpoints

When to choose what?

  • Start with per-IP for public read-only endpoints.
  • Move to per-user as soon as you add authentication, to ensure fairness.
  • Use per-API-key if you're building a paid API platform.
  • Apply a global limit as a safety net, then override specific routes that need more headroom.

For storage, SlowAPI defaults to in-memory, which resets on app restart. If you run multiple workers or need persistence, switch to Redis:

from slowapi import Limiter
from slowapi.util import get_remote_address
import redis

storage_uri = "redis://localhost:6379"
limiter = Limiter(key_func=get_remote_address, storage_uri=storage_uri)

Troubleshooting & edge cases

Even with a working setup, you'll hit common pitfalls. Let's debug them:

  1. AssertionError: 'Request' is a required argument - You forgot to add request: Request to the endpoint signature. Add it, even if you don't use it — the decorator needs it.

  2. Limits not applying in production with multiple workers - In-memory storage is per-process. If you run 4 workers, each has its own counter, so the effective limit becomes 4x your configured value. Use storage_uri pointing to Redis or Memcached to share state.

  3. RateLimitExceeded handler not returning JSON - If you don't add the exception handler, SlowAPI returns a plain text response. Always add app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) for structured JSON errors.

  4. Choosing the wrong key function - Using get_remote_address behind a reverse proxy or load balancer may capture the proxy's IP, not the client's. Configure your proxy to set X-Forwarded-For and use a custom key function that respects it.

  5. Zero-length key issue - If your key function returns an empty string, SlowAPI might raise an error. Ensure your custom function always returns a non-empty string (e.g., fall back to "anonymous").

  6. Test suite hitting 429 - During tests, a lot of requests in a short time can exhaust limits. Disable rate limiting in test settings via environment variable, or use a higher limit.

What you learned & what's next

You've now grasped the core idea behind rate limiting basic endpoints with SlowAPI: what it solves, how the middleware works, and how to apply it to individual routes, groups, or the whole app. You can configure limits per IP or per user, choose the right storage backend, and debug common pitfalls. You've completed a hands-on exercise that proves the concept works.

This is just a slice of backend defense. Up next, you'll explore caching strategies to make your rate-limited endpoints even more performant, or dive into API versioning to evolve your endpoints without breaking existing clients. Either way, you're building production-ready FastAPI services, and rate limiting is your guard at the gate.

Practice recap

Build a new endpoint /api/search that allows 3 requests per 10 seconds per user, and another /api/admin limited to 1 request per hour. Use a custom key function that reads a header X-User-ID (fallback to "anonymous"). Test both by firing a burst of requests and observing the 429 status. Then, switch to a shared limit scope for both endpoints and retest the combined budget.

Common mistakes

  • Forgetting to add request: Request to a rate-limited endpoint causes an assertion error at startup — always include it even if unused.
  • Using in-memory storage with multiple workers or processes silently multiplies your effective limit, defeating the purpose of rate limiting.
  • Not configuring a RateLimitExceeded exception handler leaves your 429 responses as plain text, breaking API consistency.
  • Relying on get_remote_address behind a reverse proxy without forwarding client IPs can rate-limit the proxy's IP instead of actual users.

Variations

  1. Use @limiter.shared_limit to enforce a combined quota across multiple endpoints under the same scope.
  2. Set default_limits=[...] on the Limiter instance for a global app-level rate limit, then override per route with decorators.
  3. Switch the storage backend from in-memory to Redis with storage_uri to share limits across workers and persist through restarts.

Real-world use cases

  • A public JSON API for weather data caps users at 60 requests per minute per IP to prevent scraping and server overload.
  • A SaaS platform uses per-user rate limits (e.g., 1000 requests/hour) on its premium API to enforce plan quotas.
  • A microservice behind a gateway applies strict rate limits (e.g., 5 requests/second) on expensive endpoints like PDF generation to control CPU costs.

Key takeaways

  • Rate limiting protects backend resources by capping how often a client can call an endpoint, returning HTTP 429 when exceeded.
  • SlowAPI integrates via middleware and decorators — simply add @limiter.limit("5/minute") and a Request argument to any endpoint.
  • Choose your key function wisely: per-IP for public APIs, per-user for authenticated traffic, per-API-key for SaaS products.
  • For production with multiple workers or instances, configure Redis (or another shared storage) via storage_uri.
  • Always test that you've added the exception handler and that your key function never returns an empty string to avoid runtime errors.
  • Rate limiting is a policy layer — it doesn't alter your endpoint logic, making it easy to add, adjust, or remove without refactoring.

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.