Rate limiter: per-IP bucketing and low /api and /auth ceilings won't hold at scale #1
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
eofredj/patrimoine#1
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
backend/app/middleware/rate_limit.pylimits are tight enough to cause real, user-visible 429s once usage grows past a handful of concurrent users, independent of the nginx-layer limits infrontend/nginx.conf.template.Current backend middleware limits (per client IP, sliding 60s window):
/api/v1/*: 100 req/60s/auth/*and/api/v1/auth/*(except/api/v1/auth/me, carved out to the 100/60s bucket): 10 req/60snginx sits in front with its own, more generous, per-second-smoothed limits (10 r/s / burst 20 for
api, 5 r/s / burst 10 forauth— roughly 600/min and 300/min respectively), so in practice the backend middleware is the real bottleneck.Why this bites at scale
_get_client_ipusesX-Real-IP). Any users sharing an IP (office network, VPN, CGNAT) share one bucket and can lock each other out.uvicorncurrently runs with no--workersflag, so there's one bucket store today, but the module's own docstring already flags that adding workers or replicas later silently multiplies the effective limit unevenly (no shared store, e.g. Redis).Suggested directions (not mutually exclusive, needs a decision before implementing — this touches auth-adjacent security logic)
/api/v1/*from 100 to something closer to nginx's own ceiling (e.g. 300-600/60s), since nginx's docstring already states it's meant to be the authoritative layer and the backend middleware is defense-in-depth.How this was found
Surfaced while regenerating manual/landing-page screenshots against a local dev instance seeded with demo data — the seeding script hit 429s from normal API usage patterns that mimic real multi-view navigation, which is what prompted a closer look at the limiter's actual ceilings.
Addressed the first two directions in commit
bb31d11a:/api/v1/*from 100 to 400 req/60s, closer to nginx's own ceiling, so a normal user bouncing between a couple of chatty views no longer exhausts the budget alone.access_tokencookie (checked synchronously viavalidate_local_token, bucketed bysub). This fixes the shared-IP collision problem for the common local-auth case. API tokens, OIDC sessions, and/auth/*itself still fall back to IP — see the docstrings inrate_limit.pyfor why each of those can't be safely/cheaply verified at this layer.Authorization: Bearer ptr_...) by the raw, unverified header value, which a background security review flagged as a HIGH-severity bypass (any attacker could send a fresh fake token per request and get an unbounded new bucket every time). That branch was removed before landing — API tokens fall back to IP like before.Still open, not attempted here: Redis-backed (or similar shared) bucket store before ever scaling to multiple backend workers/replicas — today's in-memory buckets are per-process, so the module docstring's own caveat about effective-limit drift under multiple workers still applies. Leaving this open since it's a bigger infrastructure change than the other two.
Docs updated in the same commit:
docs/architecture/middleware.md,docs/security/overview.md. Backend test suite (2478 tests) passes; new/updated tests cover same-IP-different-user bucketing and the fallback-to-IP-on-invalid-token case.