API ABUSE & BOT PROTECTION FOR SAAS APPS: RATE LIMITING GUIDE (2026)
AI-generated APIs ship fast and rarely ship rate limits, authz, or abuse controls. Protect the endpoints attackers actually hammer once your app is public.
PROBE YOUR API FOR ABUSE GAPS
Enter your app URL — we exercise public endpoints for auth gaps, IDOR, and unrestricted write paths.
The API Abuse Problem for SaaS Startups
SaaS APIs face four primary attack vectors. Credential stuffing uses leaked password databases to try thousands of login combinations per minute. Scraping bots extract your data to build competing products. API key theft happens when keys are exposed in client-side code or public repositories. Cost exploitation targets AI-powered endpoints where each API call costs real money — an attacker can run up thousands of dollars in OpenAI or Anthropic charges in hours.
Abuse sits next to authorization in the priority stack. A perfectly authorized API with no rate limits still loses to stuffing and bill attacks. An aggressively rate-limited API with BOLA still leaks every tenant’s data — slowly. Ship both; test both.
Startups are particularly vulnerable because they often ship without rate limiting, use permissive CORS, and expose API keys in frontend code generated by AI tools like Lovable, Bolt.new, or Cursor. Auth might be a React guard with no throttle on /auth/v1/token or /api/login. AI wrappers often proxy the provider key from a serverless function with no per-user budget.
Abuse control is not optional polish. It is how you keep the product online, the bill predictable, and account takeover expensive for attackers.
Threat model: what gets hammered first
| Surface | Abuse mode | First control |
|---|---|---|
POST /login, password grant |
Stuffing, spray | Per-account + per-IP limits, Turnstile, generic errors |
POST /signup, OTP, magic link |
Flood, SMS/email cost | Tight IP limits, CAPTCHA, provider budgets |
| Public read APIs | Scraping | Auth or signed URLs, rate limits, pagination caps |
| Search / export | Bulk extraction | Stricter limits, async jobs, authz |
| AI generation routes | Token bill attack | User budgets, low RPM, hard provider caps |
| Webhooks | Replay, forgery | Signatures, idempotency (not just rate limits) |
| Admin routes | Privilege probe | Authz + no public discoverability + alerts |
IDOR and missing auth are still primary bugs — rate limits do not fix BOLA. Fix authorization first (authorization patterns), then layer abuse controls.
Rate Limiting Strategies
Algorithms
- Token bucket — Allows short bursts up to capacity, then refills. Good UX for bursty UIs; can be abused if burst is huge.
- Fixed window — Simple counts per clock window; boundary bursts (end of window + start of next) are a known weakness.
- Sliding window — Smoother; counts requests over a rolling period. Preferred default for most SaaS APIs.
For most apps, sliding window per user (authenticated) and per IP (pre-auth) is the right starting point.
Identity keys
Authenticated: rate:{route}:{userId}
Pre-auth: rate:{route}:ip:{ip}
Global safety: rate:{route}:global
Never rate-limit only by IP for authenticated heavy routes (NAT false positives), and never only by user for login (attackers spray many users from one IP).
express-rate-limit (single Node process)
Useful for simple VMs or local. Not sufficient alone on multi-instance/serverless without a store.
npm install express-rate-limit
import rateLimit from "express-rate-limit";
import express from "express";
const app = express();
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20, // per IP in this window
standardHeaders: true, // RateLimit-* headers
legacyHeaders: false,
message: { error: "Too many login attempts. Try again later." },
});
app.use("/api/login", loginLimiter);
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
standardHeaders: true,
legacyHeaders: false,
});
app.use("/api/", apiLimiter);
For production multi-instance, attach a Redis store (see rate-limit-redis compatible stores) or switch to Upstash as below.
Upstash ratelimit (serverless / Vercel / Edge)
npm install @upstash/ratelimit @upstash/redis
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv(); // UPSTASH_REDIS_REST_URL / TOKEN
export const loginRatelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, "15 m"),
analytics: true,
prefix: "rl:login",
});
export const aiRatelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, "1 m"),
prefix: "rl:ai",
});
// Next.js App Router example
export async function POST(req: Request) {
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
const { success, limit, remaining, reset } = await loginRatelimit.limit(ip);
if (!success) {
return new Response(JSON.stringify({ error: "Too many requests" }), {
status: 429,
headers: {
"Content-Type": "application/json",
"Retry-After": Math.max(1, Math.ceil((reset - Date.now()) / 1000)).toString(),
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
},
});
}
// ... authenticate
return Response.json({ ok: true });
}
Authenticated AI route — key by user id:
export async function POST(req: Request) {
const userId = await requireUserId(req); // your auth
const { success } = await aiRatelimit.limit(`user:${userId}`);
if (!success) {
return new Response(JSON.stringify({ error: "Rate limit exceeded" }), {
status: 429,
});
}
// call model provider...
}
Supabase Edge Functions can use the same Upstash REST client: check limit before expensive work; return 429 with Retry-After.
Response contract
Always return 429 when limited. Include Retry-After when you can. Document limits for legitimate API customers. Do not silently drop.
Bot Detection and Prevention
Cloudflare Turnstile (forms)
Turnstile is privacy-friendly bot protection for browser forms (login, signup, reset). It does not secure pure API tokens.
Sketch:
- Create a Turnstile site key + secret in Cloudflare.
- Render the widget on the form (official React component or script).
- Send
cf-turnstile-responsetoken to your server. - Verify server-side with Cloudflare’s siteverify API before processing login/signup.
async function verifyTurnstile(token: string, ip: string) {
const body = new URLSearchParams({
secret: process.env.TURNSTILE_SECRET!,
response: token,
remoteip: ip,
});
const res = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{ method: "POST", body }
);
const data = await res.json();
return data.success === true;
}
Fail closed: if verification fails or secret missing in prod, reject the request.
Behavioral signals
- Requests without expected browser headers on cookie-session apps.
- Impossible timing (full signup flow in <100ms repeatedly).
- Headless UA patterns (support real API clients with keys separately).
- Optional fingerprinting (FingerprintJS etc.) as a signal, not sole auth.
Edge WAF
Put Cloudflare/similar in front of public apps: bot fight modes, geo rules if appropriate, and caching for public GETs. Still keep app-layer limits — WAF is not object-level authz.
Protecting AI-Powered API Endpoints
AI API proxying is the highest-cost attack surface for modern SaaS. If your app wraps OpenAI, Anthropic, or similar, every abused request costs you money.
Token budgets
// Pseudocode — store usage in Redis/Postgres
async function assertTokenBudget(userId: string, estimatedTokens: number) {
const dayKey = `tokens:${userId}:${new Date().toISOString().slice(0, 10)}`;
const used = Number((await redis.get(dayKey)) ?? 0);
const dailyCap = await getPlanCap(userId); // e.g. 100_000 free tier
if (used + estimatedTokens > dailyCap) {
const err = new Error("Daily AI quota exceeded");
// @ts-ignore
err.status = 402; // or 429
throw err;
}
}
async function commitTokens(userId: string, actualTokens: number) {
const dayKey = `tokens:${userId}:${new Date().toISOString().slice(0, 10)}`;
await redis.incrby(dayKey, actualTokens);
await redis.expire(dayKey, 60 * 60 * 48);
}
Provider hard caps
Set account-level spending limits and email alerts at 50%, 80%, and 95% on OpenAI/Anthropic/etc. App quotas fail; billing caps are the last fuse.
Prompt and input controls
- Max input length; reject absurd payloads.
- Separate system prompts from user content; do not concatenate untrusted HTML into admin tools (LLM HTML risks).
- Rate limit generation harder than reads (e.g. 10/min generate vs 60/min read).
- Auth required — never leave a public
/api/chatwith your key.
Key handling
Provider secrets only on server/Edge with restricted env. Client gets your app session, not sk-.... Scan for leaks: token leak checker.
Auth Endpoint Hardening
Login is the most attacked surface on most SaaS apps.
Progressive defense
- Soft limit — 5 failures / account / 15 minutes → Turnstile required.
- Hard limit — continued failures → temporary lock or exponential backoff.
- IP fan-out — one IP failing on many accounts → block/challenge that IP.
- MFA — for admin and high-value accounts.
- Generic errors —
"Invalid email or password"(no account enumeration).
async function recordFailedLogin(email: string, ip: string) {
await redis.incr(`fail:user:${email}`);
await redis.expire(`fail:user:${email}`, 15 * 60);
await redis.incr(`fail:ip:${ip}`);
await redis.expire(`fail:ip:${ip}`, 60 * 60);
const ipFails = Number(await redis.get(`fail:ip:${ip}`));
if (ipFails > 30) {
await redis.set(`block:ip:${ip}`, "1", { ex: 60 * 60 });
}
}
Password reset and OTP endpoints need the same family of controls — they are cost and takeover vectors. Patterns: auth flows.
Session and token abuse
- Short-lived access tokens; rotate refresh tokens.
- Bind admin sessions tightly; step-up auth for sensitive actions.
- Revoke on password change.
Deep dive: authentication implementation and API security guide.
Monitoring and alerting
What to log
Structured logs per request:
{
"ts": "2026-08-12T12:00:00Z",
"route": "/api/ai/complete",
"method": "POST",
"userId": "usr_123",
"ip": "203.0.113.10",
"status": 429,
"remaining": 0,
"tokens": 0,
"ua": "..."
}
Alerts worth having
- Spike in
401/403on admin routes. - Spike in
429(may be attack or mis-tuned limit). - Single IP → many user ids on auth routes.
- AI token burn rate vs baseline.
- Sudden 5xx after deploy (bad limit config or Redis down — fail policy matters).
Fail open vs fail closed
If Redis is down, decide explicitly:
- Auth login limits fail closed (reject) can lock everyone out — use careful degrade.
- AI expensive routes fail closed (reject) protects the bill.
- Document the choice; test chaos on the rate-limit store.
CORS, caching, and adjacent controls
Permissive CORS with credentials turns browser sessions into cross-site API abuse helpers. Tighten origins (CORS pattern). Disable caching of authenticated responses. Pagination max page sizes stop “export the DB via API.”
Per-route suggested starting limits
Tune to your UX; these are conservative starts for small SaaS:
| Route class | Authenticated | Pre-auth / IP |
|---|---|---|
| Login password grant | 5 fails / user / 15m | 20 fails / IP / hour |
| Signup | 3 / IP / hour + Turnstile | same |
| Password reset request | 3 / email / hour | 10 / IP / hour |
| Read API (CRUD GET) | 60–120 / min / user | low or auth-only |
| Write API | 30 / min / user | auth-only |
| AI generation | 5–10 / min / user + daily tokens | none public |
| File upload | 10 / hour / user + size cap | auth-only |
| Export | 2 / hour / user | auth-only |
Log every 429 with route and key type so you can spot attackers vs clumsy clients.
Quick Implementation Checklist
Add Cloudflare Turnstile to auth pages
Free bot protection for login, signup, and password reset forms. Verify server-side; fail closed in production.
Implement rate limiting with Upstash Redis
Serverless-friendly sliding window with per-user and per-IP keys. Return 429 + Retry-After.
Set token budget limits on AI endpoints
Cap per-user AI usage; track tokens; enforce provider account caps and alerts.
Enable request logging and monitoring
Log route, user, IP, status; alert on anomalies and token burn.
Configure billing alerts
50% / 80% / 95% thresholds on model providers and SMS/email providers.
Harden auth endpoints
Attempt limits, spray detection, generic errors, MFA for privileged roles.
Fix authz before celebrating limits
Rate limits on an open IDOR still leak data — slower. Run API security testing and a live vibe code scanner pass.
Example: combining limits on an Express API
import express from "express";
import rateLimit from "express-rate-limit";
import { Redis } from "@upstash/redis";
import { Ratelimit } from "@upstash/ratelimit";
const app = express();
app.use(express.json({ limit: "32kb" })); // payload cap
const redis = Redis.fromEnv();
const aiLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, "1 m"),
prefix: "rl:ai",
});
const ipLimiter = rateLimit({
windowMs: 60_000,
max: 120,
standardHeaders: true,
legacyHeaders: false,
});
app.use("/api/", ipLimiter);
app.post("/api/ai/complete", async (req, res) => {
const userId = req.user?.id; // from your session middleware
if (!userId) return res.status(401).json({ error: "Unauthorized" });
const { success } = await aiLimit.limit(userId);
if (!success) return res.status(429).json({ error: "Too many requests" });
// assertTokenBudget(userId, estimate); then call provider
res.json({ ok: true });
});
Wire real session middleware, budgets, and provider calls; keep the order: auth → rate limit → budget → expensive work.
Scraping and bulk export defenses
Authenticated APIs still get harvested by legitimate-looking sessions:
- Pagination caps — max
limit=100; rejectlimit=100000. - Stable sort + cursors — make parallel range scans harder; detect cursor abuse.
- Field minimization — list endpoints return summaries; detail endpoints rate-limit harder.
- Export as async job — generate file server-side, store temporarily, notify user; never stream entire tenant DB in one sync response without strict authz and audit.
- Watermarking / canaries — optional for high-value datasets; helps attribution after a leak.
Scrapers rotate IPs; per-user limits matter more than IP alone for authenticated scrape.
GraphQL-specific abuse
If AI generated a GraphQL gateway:
- Depth and complexity limits (nested friends-of-friends bombs).
- Disable or protect introspection in production.
- Persist queries / allowlists for mobile clients when possible.
- Per-operation rate limits, not only global HTTP limits.
A single GraphQL endpoint is a multiplexed abuse surface — treat it like many REST routes under one URL.
Webhooks vs user APIs
Do not apply the same rate limit mental model:
| Surface | Primary controls |
|---|---|
| User login | Per-account + per-IP + Turnstile |
| User AI route | Per-user RPM + token budget |
| Stripe webhook | Signature, idempotency, raw body — rate limit secondary |
| Partner webhooks | mTLS or shared secret rotation |
Rate limiting a webhook too aggressively without backoff cooperation can break billing; signature failure logging is the better signal for forgery.
Distributed rate limiting edge cases
- NAT / corporate egress — many users one IP → prefer user key after auth; generous IP soft limits.
- IPv6 — attackers may have vast address space; /64 bucketing is a research tradeoff.
- Clock skew on sliding windows — use Redis server time.
- Multi-region — one Redis region vs global; eventual consistency can allow double burst; document the risk.
- ** pen tests** — allowlist scanner IPs carefully; do not leave allowlists forever.
Abuse playbooks (on-call)
Credential stuffing spike
- Confirm 401 rate and top IPs/users.
- Tighten login limits temporarily; force Turnstile globally if needed.
- Notify users with unusual success-after-failures if takeover suspected.
- Keep signup open if possible — locking everyone out is a secondary outage.
AI bill runaway
- Kill switch feature flag on generation routes.
- Lower caps; enable provider hard limit.
- Rotate provider keys if leak suspected.
- Audit which users/tokens burned quota.
Scrape of public catalog
- Add auth or signed URLs if data was not meant to be open.
- Cache public GETs at CDN with bot management.
- Legal/ToS path for commercial scrapers when relevant.
Testing your limits
- Unit: mock Redis; assert 429 after N.
- Integration: parallel curl flood on staging login.
- Chaos: block Redis; assert fail-open/closed policy matches doc.
- Product: ensure legitimate bursty UX (dashboard load) still works.
Document limits for API customers in public docs — surprises create support load and accidental self-DoS by partners.
Related Resources
- API Security Guide — Comprehensive API security best practices for SaaS
- Authentication Implementation — Secure auth patterns for AI-generated apps
- API Security Testing — Test your API endpoints for security vulnerabilities
- Token Leak Checker — Find the API keys that already shipped in your frontend bundle
- Runtime protection for SaaS — Broader runtime controls
Distributed rate limiting gotchas
Serverless instances do not share memory. Use Redis/Upstash keys that include user id + route class. Avoid only IP limits behind corporate NAT (false positives) and only user limits before login (stuffing).
Graceful 429s
Return Retry-After and stable JSON errors so legitimate mobile clients back off. Log 429 volume — sudden spikes mean you are under attack or your limits are too tight for a launch.
Bot management beyond CAPTCHA
Device attestation, impossible travel on sessions, and per-account anomaly scores complement Turnstile. Start simple; add signal as abuse sophistication grows.
Putting numbers on “good enough”
For a seed-stage SaaS with a few hundred DAU:
- Login limits as in the table above are enough to force stuffing onto many IPs (still combine with breach-password checks).
- AI routes at 10/min and a hard daily token cap prevent overnight bankruptcies.
- 429 rates under 1% for legitimate users mean limits are not too tight; investigate if higher.
Revisit when you hit enterprise multi-tenant scale — NAT-heavy customers need user-keyed limits more than IP keys.
Detect API Abuse Before It Costs You
VibeEval scans your SaaS app for exposed API keys, missing auth, and open write paths that make abuse trivial. Rate limits and Turnstile matter — but only after the API stops handing out other users’ data for free. Run the vibe code scanner to find issues before attackers exploit them.
COMMON QUESTIONS
FIND THE ABUSE PATHS FIRST
We hit your live API the way an attacker would: unauthenticated writes, cross-user IDs, and open admin routes. Fix what we prove, not what you assume.
14-day free trial · No credit card · Cancel anytime