Correct, an in-memory Map on serverless is a counter per instance, and instances come and go. And the Referer header is set by the caller, so a script just sends your domain.
Order of fixes:
- Auth first. In the route, get the user from the session and return 401 if there's none. That alone kills anonymous abuse.
- Limit per user, not per IP. Use a shared store. The common option is Upstash Redis with
@upstash/ratelimit(one new dependency, free tier is plenty):
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(20, '1 h'),
})
const { success } = await ratelimit.limit(user.id)
if (!success) return new Response('Too many requests', { status: 429 })- Keep the provider spend limit as the last line of defense.
If you already use Supabase/Postgres you can also count rows in a generations table for the last hour instead of adding Redis. Slower, but zero new services.