traceRAG-backend / ratelimit.py
Karush2807
TraceRAG backend + demo graphs
db284b9
Raw
History Blame Contribute Delete
2.41 kB
"""Per-user rate limiting for the LLM (billed) endpoints, built on slowapi.
slowapi wraps the `limits` library. A limit like "10/minute" is a FIXED-WINDOW
counter by default: one count per (key, 60s window). It's cheap and perfect for
"stop a single user from hammering a paid API", with one caveat β€” the window
boundary allows a short burst (10 at :59 + 10 at 1:01 = 20 in ~2s). For a smooth
limit with no boundary burst, switch to the moving-window strategy (see below).
The interesting design choice here is the KEY: we rate-limit per authenticated
USER, not per IP. The default slowapi key_func (get_remote_address) keys by IP,
which would make a whole office behind one NAT share a single bucket. Instead our
key_func reads request.state.user_id β€” which get_current_user populates during
dependency resolution, before this key_func is ever called (see auth.py).
"""
from __future__ import annotations
from slowapi import Limiter
from slowapi.util import get_remote_address
from starlette.requests import Request
def _user_key(request: Request) -> str:
"""Bucket key = the authenticated user id, falling back to client IP.
By the time slowapi calls this, FastAPI has already run get_current_user
(a dependency on the limited routes), so request.state.user_id is set. The
IP fallback only matters for routes that have no auth dependency β€” it keeps
the key_func total so slowapi never crashes on a missing attribute.
"""
user_id = getattr(request.state, "user_id", None)
if user_id:
return f"user:{user_id}"
return f"ip:{get_remote_address(request)}"
# The process-wide limiter.
#
# storage_uri: omitted β†’ defaults to IN-MEMORY storage. That's correct for a
# single uvicorn worker. With --workers N each process keeps its OWN counter, so
# the effective limit becomes N Γ— the configured rate. When you scale to
# multi-worker / multi-instance, point all of them at a shared store:
#
# limiter = Limiter(
# key_func=_user_key,
# storage_uri="redis://localhost:6379", # shared counter across workers
# strategy="moving-window", # smooth limit, no boundary burst
# )
#
# strategy: omitted β†’ "fixed-window" (the simple counter described above).
limiter = Limiter(key_func=_user_key)
# Single source of truth for the LLM-endpoint rate, applied via @limiter.limit().
LLM_RATE_LIMIT = "10/minute"