Local-first, offline static analysis CLI that finds API routes missing rate-limiting protection — and prints ready-to-apply fixes.
AI assistants ship backends without rate limiting all the time: login, signup, password-reset, checkout and webhook endpoints go straight to production with nothing in front of them. That's brute-force territory for auth endpoints, billing-abuse territory for AI/payment endpoints, and spam-queue territory for everything else. Existing scanners are hosted, paid, and probe a live URL. RateGuard scans the code itself.
- 100% offline — no network calls, no telemetry, no API keys.
- AST-based detection (with regex fallback for unparseable files).
- Fast — static analysis only; runs as a pre-commit hook or on every PR.
- Free and MIT-licensed.
npx rateguard scanor install once:
npm install -g rateguard
rateguard scanNo config file required. Point it at a repo and it figures the rest out.
CRITICAL (2)
┌────────────────────────────┬───────┬─────────────────────────┬───────────┬───────────────...
│ File │ Line │ Route │ Framework │ Reason │
├────────────────────────────┼───────┼─────────────────────────┼───────────┼───────────────...
│ src/routes/auth.ts │ 12 │ POST /login │ express │ critical route │
│ src/routes/auth.ts │ 24 │ POST /password-reset │ express │ critical route │
MEDIUM (1)
...
2 critical, 1 medium, 0 low findings across 14 routes scanned in 6 files.
rateguard scan [path] [--format table|json] [--fail-on critical|medium|none] [--ignore <glob>]
rateguard fix [path] [--write] [--only critical|medium]
rateguard init-hook
rateguard --help
| Flag | Default | Meaning |
|---|---|---|
--format |
table |
table prints grouped, colorized findings; json prints { routesScanned, message, findings: [{file, line, method, path, framework, severity, protected, reason}] }. When no routes are detected, findings is empty and message explains the zero case so CI consumers don't have to guess from prose. |
--fail-on |
critical |
Exit non-zero when findings at or above this level exist. none always exits 0. |
--ignore |
— | Extra glob to skip; repeatable. node_modules, .next, dist, build, venv, __pycache__ are always skipped regardless. |
--write (fix) |
off | Apply the generated fixes. Files are backed up to .rateguard-backup/ first, and writes are temp-file-then-move. |
--only (fix) |
— | Restrict fixes to critical or medium findings. |
path defaults to the current directory. .gitignore is respected.
| Framework | Detects | Protection recognised |
|---|---|---|
| Express | app.METHOD(path, ...), router.METHOD(...) in *.ts/js |
express-rate-limit, rate-limiter-flexible, custom *limiter* middleware (route-level or app.use() before registration) |
| FastAPI | @app.METHOD(path) / @router.METHOD(path) decorators in *.py |
slowapi, fastapi-limiter, or Depends(<rate-limiter-ish>) |
| Next.js | Pages Router pages/api/** default export; App Router app/api/**/route.ts HTTP-verb exports |
@upstash/ratelimit (route file) or a middleware.ts whose matcher covers the route |
| Supabase Edge Functions | Deno.serve(...) / serve(...) in supabase/functions/*/index.ts |
In-function counter logic only (Supabase has no built-in per-function limiter; unprotected functions are always flagged) |
A companion action runs on pull_request and:
- Lists the PR's changed files via the GitHub API.
- Runs
rateguard scan --format jsonon the repo. - Posts (or updates) one PR comment with a markdown table of findings in the changed files.
- Fails the check if any critical finding is in a changed file.
Add it to your repo:
# .github/workflows/rateguard.yml
name: RateGuard
on: [pull_request]
jobs:
rateguard:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci && npm run build
- uses: ./action- Each discovered route is classified critical (login/signup/password-reset/OTP/payment/webhook/admin), medium (mutating
POST/PUT/DELETE/PATCH, search, upload), or low (public read-onlyGET). - Static analysis tracks per-file: known rate-limit imports (
express-rate-limit,slowapi,@upstash/ratelimit, …), custom identifiers matching*rateLimit* | *limiter* | throttle | slowdown,app.use(limiter)ordering (Express),middleware.tsmatcher coverage (Next.js),Depends(limiter)(FastAPI), and in-function counters (Supabase). - Global middleware applied before route registration counts as coverage and is reported as such — protected routes are never double-flagged.
- Unparseable files fall back to regex route discovery so a single broken file never hides routes.
FastAPI scanning uses @lezer/python, a pure-JS tolerant Python parser — no Python runtime needed on the machine running the scan. It's a CST, not a full Python AST, so detection focuses on the decorator + function-definition shapes the FastAPI docs use. Unparseable files fall back to regex.
- Next.js route definitions come in more than one shape. Plain
export async function GET/POSTis the common case, but real apps also use re-exported handlers (export { handler as GET, handler as POST }— tRPC'sfetchRequestHandler) and destructured factory exports (export const { GET, POST, PUT } = serve({...})— Inngest). All three are detected; all emit the correct URL path and per-verb severity. Anything even more exotic (e.g. a handler built dynamically) falls back to regex per-file. - Two kinds of "limiter" exist. The term is overloaded: some repos use
rate-limiter-flexible'sRateLimiterPrismafor business-logic quotas (credit consumption, per-user thresholds — e.g. a "usage" module that charges per generation). These are imported into a service module, never applied as HTTP middleware or route guards, and RateGuard correctly does NOT count them as protecting any route. Only a limiter wired into the actual request path (Express middleware, FastAPIDepends/decorator, Nextmiddleware.tsmatcher, or an in-function counter for Supabase) counts as coverage. - FastAPI parsing is CST-based (
@lezer/python, see earlier note). A broken Python file still yields routes via the regex fallback, but exotic decorator stacking could theoretically be missed; the fallback covers it. - Global-middleware coverage is order-sensitive by design — Express
app.use(limiter)only protects routes registered after it (which matches how the middleware actually behaves).
See CONTRIBUTING.md.
MIT © 2026 RateGuard contributors