- Refactor Redis backend connection handling and pool management - Update algorithm implementations with improved type annotations - Enhance config loader validation with stricter Pydantic schemas - Improve decorator and middleware error handling - Expand example scripts with better docstrings and usage patterns - Add new 00_basic_usage.py example for quick start - Reorganize examples directory structure - Fix type annotation inconsistencies across core modules - Update dependencies in pyproject.toml
221 lines
6.2 KiB
ReStructuredText
221 lines
6.2 KiB
ReStructuredText
Quickstart
|
|
==========
|
|
|
|
Let's get rate limiting working in your FastAPI app. This guide covers the basics —
|
|
you'll have something running in under five minutes.
|
|
|
|
Your First Rate Limit
|
|
---------------------
|
|
|
|
The simplest way to add rate limiting is with the ``@rate_limit`` decorator:
|
|
|
|
.. code-block:: python
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi_traffic import rate_limit
|
|
|
|
app = FastAPI()
|
|
|
|
@app.get("/api/hello")
|
|
@rate_limit(10, 60) # 10 requests per 60 seconds
|
|
async def hello(request: Request):
|
|
return {"message": "Hello, World!"}
|
|
|
|
That's the whole thing. Let's break down what's happening:
|
|
|
|
1. The decorator takes two arguments: ``limit`` (max requests) and ``window_size`` (in seconds)
|
|
2. Each client is identified by their IP address by default
|
|
3. When a client exceeds the limit, they get a 429 response with a ``Retry-After`` header
|
|
|
|
.. note::
|
|
|
|
The ``request: Request`` parameter is required. FastAPI Traffic needs access to the
|
|
request to identify the client and track their usage.
|
|
|
|
Testing It Out
|
|
--------------
|
|
|
|
Fire up your app and hit the endpoint a few times:
|
|
|
|
.. code-block:: bash
|
|
|
|
# Start your app
|
|
uvicorn main:app --reload
|
|
|
|
# In another terminal, make some requests
|
|
curl -i http://localhost:8000/api/hello
|
|
|
|
You'll see headers like these in the response:
|
|
|
|
.. code-block:: http
|
|
|
|
HTTP/1.1 200 OK
|
|
X-RateLimit-Limit: 10
|
|
X-RateLimit-Remaining: 9
|
|
X-RateLimit-Reset: 1709834400
|
|
|
|
After 10 requests, you'll get:
|
|
|
|
.. code-block:: http
|
|
|
|
HTTP/1.1 429 Too Many Requests
|
|
Retry-After: 45
|
|
X-RateLimit-Limit: 10
|
|
X-RateLimit-Remaining: 0
|
|
|
|
Choosing an Algorithm
|
|
---------------------
|
|
|
|
Different situations call for different rate limiting strategies. Here's a quick guide:
|
|
|
|
.. code-block:: python
|
|
|
|
from fastapi_traffic import rate_limit, Algorithm
|
|
|
|
# Token Bucket - great for APIs that need burst handling
|
|
# Allows short bursts of traffic, then smooths out
|
|
@app.get("/api/burst-friendly")
|
|
@rate_limit(100, 60, algorithm=Algorithm.TOKEN_BUCKET, burst_size=20)
|
|
async def burst_endpoint(request: Request):
|
|
return {"status": "ok"}
|
|
|
|
# Sliding Window - most accurate, but uses more memory
|
|
# Perfect when you need precise rate limiting
|
|
@app.get("/api/precise")
|
|
@rate_limit(100, 60, algorithm=Algorithm.SLIDING_WINDOW)
|
|
async def precise_endpoint(request: Request):
|
|
return {"status": "ok"}
|
|
|
|
# Fixed Window - simple and efficient
|
|
# Good for most use cases, slight edge case at window boundaries
|
|
@app.get("/api/simple")
|
|
@rate_limit(100, 60, algorithm=Algorithm.FIXED_WINDOW)
|
|
async def simple_endpoint(request: Request):
|
|
return {"status": "ok"}
|
|
|
|
See :doc:`/user-guide/algorithms` for a deep dive into each algorithm.
|
|
|
|
Rate Limiting by API Key
|
|
------------------------
|
|
|
|
IP-based limiting is fine for public endpoints, but for authenticated APIs you
|
|
probably want to limit by API key:
|
|
|
|
.. code-block:: python
|
|
|
|
def get_api_key(request: Request) -> str:
|
|
"""Extract API key from header, fall back to IP."""
|
|
api_key = request.headers.get("X-API-Key")
|
|
if api_key:
|
|
return f"key:{api_key}"
|
|
# Fall back to IP for unauthenticated requests
|
|
return request.client.host if request.client else "unknown"
|
|
|
|
@app.get("/api/data")
|
|
@rate_limit(1000, 3600, key_extractor=get_api_key) # 1000/hour per API key
|
|
async def get_data(request: Request):
|
|
return {"data": "sensitive stuff"}
|
|
|
|
Global Rate Limiting with Middleware
|
|
------------------------------------
|
|
|
|
Sometimes you want a blanket rate limit across your entire API. That's what
|
|
middleware is for:
|
|
|
|
.. code-block:: python
|
|
|
|
from fastapi_traffic.middleware import RateLimitMiddleware
|
|
|
|
app = FastAPI()
|
|
|
|
app.add_middleware(
|
|
RateLimitMiddleware,
|
|
limit=1000,
|
|
window_size=60,
|
|
exempt_paths={"/health", "/docs", "/openapi.json"},
|
|
)
|
|
|
|
# All endpoints now have a shared 1000 req/min limit
|
|
@app.get("/api/users")
|
|
async def get_users():
|
|
return {"users": []}
|
|
|
|
@app.get("/api/posts")
|
|
async def get_posts():
|
|
return {"posts": []}
|
|
|
|
Using a Persistent Backend
|
|
--------------------------
|
|
|
|
The default memory backend works great for development, but it doesn't survive
|
|
restarts and doesn't work across multiple processes. For production, use SQLite
|
|
or Redis:
|
|
|
|
**SQLite** — Good for single-node deployments:
|
|
|
|
.. code-block:: python
|
|
|
|
from fastapi_traffic import RateLimiter, SQLiteBackend
|
|
from fastapi_traffic.core.limiter import set_limiter
|
|
|
|
# Set up persistent storage
|
|
backend = SQLiteBackend("rate_limits.db")
|
|
limiter = RateLimiter(backend)
|
|
set_limiter(limiter)
|
|
|
|
@app.on_event("startup")
|
|
async def startup():
|
|
await limiter.initialize()
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown():
|
|
await limiter.close()
|
|
|
|
**Redis** — Required for distributed systems:
|
|
|
|
.. code-block:: python
|
|
|
|
from fastapi_traffic import RateLimiter
|
|
from fastapi_traffic.backends.redis import RedisBackend
|
|
from fastapi_traffic.core.limiter import set_limiter
|
|
|
|
@app.on_event("startup")
|
|
async def startup():
|
|
backend = await RedisBackend.from_url("redis://localhost:6379/0")
|
|
limiter = RateLimiter(backend)
|
|
set_limiter(limiter)
|
|
await limiter.initialize()
|
|
|
|
Handling Rate Limit Errors
|
|
--------------------------
|
|
|
|
By default, exceeding the rate limit raises a ``RateLimitExceeded`` exception that
|
|
returns a 429 response. You can customize this:
|
|
|
|
.. code-block:: python
|
|
|
|
from fastapi import Request
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi_traffic import RateLimitExceeded
|
|
|
|
@app.exception_handler(RateLimitExceeded)
|
|
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
|
|
return JSONResponse(
|
|
status_code=429,
|
|
content={
|
|
"error": "slow_down",
|
|
"message": "You're making too many requests. Take a breather.",
|
|
"retry_after": exc.retry_after,
|
|
},
|
|
)
|
|
|
|
What's Next?
|
|
------------
|
|
|
|
You've got the basics down. Here's where to go from here:
|
|
|
|
- :doc:`/user-guide/algorithms` — Understand when to use each algorithm
|
|
- :doc:`/user-guide/backends` — Learn about storage options
|
|
- :doc:`/user-guide/key-extractors` — Advanced client identification
|
|
- :doc:`/user-guide/configuration` — Load settings from files and environment variables
|