Skip to content

Throttle every endpoint: SlowAPIMiddleware + tight cap on /api/exports#309

Merged
ptrlrd merged 1 commit into
mainfrom
feat/throttle-everything
May 20, 2026
Merged

Throttle every endpoint: SlowAPIMiddleware + tight cap on /api/exports#309
ptrlrd merged 1 commit into
mainfrom
feat/throttle-everything

Conversation

@ptrlrd
Copy link
Copy Markdown
Owner

@ptrlrd ptrlrd commented May 20, 2026

Summary

slowapi's default_limits=["300/minute"] setting on the main Limiter was a no-op without SlowAPIMiddleware — only routes carrying an explicit @limiter.limit(...) decorator got throttled. The audit:

Decorated Unthrottled
~15 routes (POST writes, /api/runs/* reads) ~70 routes (every entity GET, exports, news, mechanics, etc.)

So /api/cards, /api/relics, /api/exports/{lang}, etc. had zero rate limit. A scraper could hammer them as fast as the box could serve until they ran out of bandwidth.

Changes

  1. Add SlowAPIMiddleware — applies the limiter's default_limits to every request before the route runs. slowapi picks the most restrictive applicable limit per request, so routes with their own decorator keep their tighter limit unchanged.
  2. Tighten /api/exports/{lang} from the new 300/min floor to 10/hour — each request builds a 15-file ZIP with deflate compression (1-3 MB / 20-100ms CPU). Cheap legitimate use, expensive abuse. 10/hour covers "snapshot all 14 locales" with room to spare; anything more is scraping.

Smoke test

350 sequential GET /api/cards from one IP:
  200 OK:  300
  429:      50

Exactly the expected split. Middleware works.

After deploy

Every endpoint has a floor of 300/min/IP. Routes with their own @limiter.limit(...) keep their tighter override. The upcoming admin rate-limit dial (PR #308) lets you tune any slug on the fly without redeploying — but the floor is in place now.

slowapi's `default_limits` was a no-op without `SlowAPIMiddleware`
— only routes with an explicit `@limiter.limit(...)` decorator got
throttled. That left ~70 of ~85 endpoints (every entity GET route,
exports, news, mechanics pages, the new admin sketches) with zero
rate limiting. A scraper could hammer `/api/cards` or
`/api/exports/{lang}` until they ran out of bandwidth budget.

Adds `SlowAPIMiddleware` so the limiter's 300/min default applies
to every request. Routes that already have explicit decorators
(submit_run 3000/hr, list/leaderboard 120/min, shared 60/min, etc.)
keep their tighter limits — slowapi uses the most restrictive
applicable limit per request, so adding the middleware is purely
additive: nothing currently throttled gets looser.

Tightened `/api/exports/{lang}` from the new 300/min floor to
10/hour because each request builds a multi-file zip with deflate
compression on 15+ JSON files (1-3 MB output, 20-100ms CPU). A
legitimate "give me a snapshot of the eng locale" call is rare —
10/hour leaves enough headroom for "I'm trying all 14 languages"
and still slams the door on scrapers.

Smoke-tested locally: 350 sequential `/api/cards` requests from one
IP cleanly returned exactly 300 × 200 followed by 50 × 429.

After deploy, every endpoint is throttled at 300/min default. The
upcoming admin rate-limit dial (PR #308) lets you tune that on the
fly per slug without redeploying — but the floor is in place now.
@ptrlrd ptrlrd merged commit d6cc487 into main May 20, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant