---
title: "No rate limiting on auth or paid API routes"
slug: no-rate-limiting
severity: high
category: Access control
summary: "Login, password reset, and any route that calls a paid API will accept as many requests as an attacker sends, until the bill or the user list gives out."
platforms:
  - Next.js
  - Vercel
  - Supabase
  - Stripe
updated: "2026-10-13"
related:
  - unbounded-llm-spend
  - weak-password-reset
  - unverified-stripe-webhooks
---

A route handler runs once per request. Nothing in a default Next.js app slows the next one down. That is a problem in two places.

Auth routes: login, signup, magic links, and password reset. A script can try passwords or flood someone's inbox with reset mail.

Paid routes: anything that calls Stripe, a model provider, SMS, or email. An open endpoint is a meter the caller does not pay.

Rate limiting does not replace authentication. It caps how fast a caller can be wrong or expensive. Put the counter on the server, keyed by user id when you have one and by IP when you do not. A counter in `localStorage` does nothing.

## How to check in 5 minutes

1. List routes that sign people in, send email, charge a card, or call an LLM. Search for `signIn`, `resetPassword`, `generateText`, `openai`, and `messages.create`.
2. For each, see if the handler increments a counter or calls a limiter before the expensive work. If the only limit is "the UI disables the button," there is no limit.
3. Send ten quick requests to a local auth route you own. If all ten run the same work, you have no server-side limit.
4. Check the model provider's dashboard for a monthly spend cap. An app-level limit and a provider-level cap are both useful. One does not replace the other. See [unbounded model spend](/issues/unbounded-llm-spend).

## How to fix

### Next.js

Before the expensive call, reject when this user or IP has exceeded a small budget for the window. Store the count in Redis, your database, or a rate-limit service. Memory on one serverless instance is a weak place to store it, because the next instance will not see the count.

Start tight on password attempts and reset emails. Loosen the numbers if real users get locked out. There is no universal correct number. Pick one you can afford if every account hits it.

Return `429` with a short message. Do not explain which passwords were close.

### Vercel

Vercel documents a firewall in front of deployments. Which rules are included on which plan changes, so check the current Vercel docs before you rely on it as the only limit. A firewall rule is still worth having for obvious floods. Keep a limit in the handler for the routes that cost money per call, because a firewall does not know that this POST triggers a model.

### Supabase

Supabase Auth has its own rate limits on auth endpoints. Those limits are not a promise about routes you wrote yourself. If your app proxies login or sends its own mail, limit that route. RLS does not rate-limit anything.

### Stripe

Stripe limits their API. That does not stop your route from creating a hundred Checkout sessions because a loop asked it to. Limit session creation per user.

## Agent prompt

```text
You are adding server-side rate limits. Do not add features.

Find handlers for login, signup, password reset, magic links, and any route that calls a paid API (LLM, SMS, email, Stripe).

For each:
1. Add a limit keyed by the signed-in user id, or by IP if there is no user.
2. Store the counter somewhere shared across server instances, not in a module variable and not in the browser.
3. Return 429 before doing the expensive work.
4. Do not pick a huge limit to avoid ever triggering it. Use a small starting number and name the constant so I can change it.
5. Do not remove the auth check. A rate limit is not authorization.

Tell me which routes you limited, the key, and the number you chose.
```

## FAQ

### Is a CAPTCHA the same thing?

No. A CAPTCHA tries to slow anonymous scripts. You still want a server-side cap, especially after the user is signed in and the route spends money.

### Can I rate-limit in the React component?

No. The attacker will not use your component. They will call the URL.

## Sources

- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
- [Stripe webhooks](https://docs.stripe.com/webhooks)
