Quick Answer
Rate limiting controls how many requests a client can make to your API within a time window. Without it, any user - or bot - can hammer your endpoints thousands of times per second. AI tools like Bolt, Lovable, and Cursor almost never include rate limiting middleware in generated code, leaving authentication routes, contact forms, and data endpoints completely unprotected.
Why AI-Generated APIs Ship Without Rate Limiting
Ask any AI coding tool to build a Next.js API route or an Express server, and it will produce functional code quickly. What it almost never includes is rate limiting. The reason is pragmatic: AI tools optimize for getting something that runs, not something that survives production traffic.
Rate limiting is infrastructure - it sits outside the happy path that AI is trained to produce. When generating a login endpoint, the model thinks about accepting credentials, checking the database, returning a token. It doesn't model the threat of an attacker running 50,000 password attempts against that same endpoint.
According to the OWASP API Security Top 10, Unrestricted Resource Consumption (API4) - which includes missing rate limiting - is one of the most commonly exploited API vulnerabilities. Akamai's 2023 State of the Internet report found that API attacks grew by 137% year-over-year, with credential stuffing and brute force attacks leading the way. Cloudflare's 2024 DDoS Threat Report noted that over 8 million DDoS attacks were mitigated in a single quarter, many targeting unprotected API endpoints.
What Can Go Wrong Without Rate Limiting
The consequences of missing rate limits vary by endpoint type. Login and registration routes allow unlimited brute force attempts. Password reset routes can be abused to flood a user's inbox or enumerate valid email addresses. Contact forms and email-sending endpoints let attackers use your server as a spam relay. AI-powered routes with per-call costs (OpenAI, Anthropic) can drain your budget in minutes.
For Supabase-backed apps built with Lovable or Bolt, the database itself becomes the target. Without rate limiting on your server layer, an attacker can send thousands of queries per second through your API, hitting Supabase row limits and spiking your usage bill simultaneously.
| Endpoint Type | Risk Without Rate Limiting | Suggested Limit |
|---|---|---|
| Login / Auth | Brute force, credential stuffing | 5 requests / 15 min per IP |
| Password Reset | Email flood, account enumeration | 3 requests / hour per IP |
| Registration | Fake account creation, spam | 10 requests / hour per IP |
| AI / LLM Proxy | Cost exhaustion | 20 requests / min per user |
| Data / Search | Scraping, DoS | 100 requests / min per IP |
| Contact / Email Send | Spam relay, abuse | 5 requests / hour per IP |
What AI Code Typically Generates
Here is a typical login route from a Bolt or Cursor-generated Next.js app. It works perfectly - and is completely open to abuse:
// ❌ BAD - No rate limiting on auth endpoint
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { supabase } from '@/lib/supabase';
export async function POST(req: NextRequest) {
const { email, password } = await req.json();
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
return NextResponse.json({ error: error.message }, { status: 401 });
}
return NextResponse.json({ session: data.session });
}
// No rate limiting. No IP tracking. No lockout.
// An attacker can try millions of passwords with no resistance.
How to Add Rate Limiting in Minutes
For Next.js apps, the upstash/ratelimit library paired with Upstash Redis is the most common production solution. It's free at low volumes and integrates with Vercel Edge Functions. For Express apps, express-rate-limit is the standard.
// ✅ GOOD - Rate limiting with Upstash + next-rate-limit
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { supabase } from '@/lib/supabase';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '15 m'), // 5 attempts per 15 minutes
analytics: true,
});
export async function POST(req: NextRequest) {
// Use IP address as rate limit identifier
const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1';
const { success, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many login attempts. Try again later.' },
{
status: 429,
headers: {
'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
'X-RateLimit-Remaining': String(remaining),
},
}
);
}
const { email, password } = await req.json();
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
return NextResponse.json({ error: error.message }, { status: 401 });
}
return NextResponse.json({ session: data.session });
}
For Express apps, the setup is even simpler:
// ✅ GOOD - express-rate-limit for Node/Express apps
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 requests per window
standardHeaders: true, // Returns rate limit info in headers
legacyHeaders: false,
message: { error: 'Too many login attempts. Try again later.' },
});
app.post('/api/auth/login', loginLimiter, handleLogin);
How to Find Unprotected Endpoints in Your Codebase
Manually auditing every API route is tedious, especially in a Bolt or Lovable project that may have generated dozens of endpoints you didn't explicitly write. The pattern to look for is any export async function POST or app.post() that doesn't call a rate limiting function before processing the request.
Tools like VibeDoctor (vibedoctor.io) automatically scan your codebase for API endpoints without rate limiting middleware and flag the specific file paths and line numbers. Free to sign up.
Beyond automated scanning, manually prioritize these endpoint types first: anything that authenticates a user, sends an email, creates a record, or calls a third-party API with per-request billing.
Rate Limiting at the Infrastructure Layer
Code-level rate limiting is important, but it's not the only layer. If you're deploying on Vercel, you can add rate limiting rules at the edge using Vercel's Firewall (available on Pro plans). Cloudflare's free tier provides basic DDoS protection and rate limiting rules without touching your code.
For Supabase apps, Supabase itself has built-in rate limits on its auth endpoints. But those limits apply to Supabase's APIs - your custom Next.js API routes that proxy data through Supabase still need their own limits. Don't assume the database layer protects your application layer.
FAQ
Does Supabase auth have built-in rate limiting?
Yes, Supabase applies rate limits to its own auth API endpoints. However, if you're building custom API routes in Next.js that call Supabase on the backend, those routes are not rate-limited by Supabase - you need to add your own middleware.
Do I need Redis for rate limiting?
Not necessarily. express-rate-limit uses in-memory storage by default, which works for single-server deployments. For serverless functions (Vercel, Netlify Edge) or multi-server setups, you need a shared store like Redis - Upstash Redis is free at low volumes and integrates easily.
What HTTP status code should rate limited responses return?
429 Too Many Requests is the correct status code. Include a Retry-After header with the number of seconds until the window resets so clients can handle it gracefully.
Should I rate limit by IP or by user?
Both, where possible. IP-based limiting blocks anonymous attackers before they authenticate. User-based limiting prevents authenticated users from abusing your API. For login endpoints specifically, always rate limit by IP since the user isn't authenticated yet.
Can Vercel or Cloudflare replace code-level rate limiting?
Infrastructure-level limits (Cloudflare, Vercel Firewall) are a valuable additional layer but should not replace application-level rate limiting. Infrastructure limits are coarser and harder to tune per-endpoint. Defense in depth means implementing both.