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:

  1. Create a Turnstile site key + secret in Cloudflare.
  2. Render the widget on the form (official React component or script).
  3. Send cf-turnstile-response token to your server.
  4. 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/chat with 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

  1. Soft limit — 5 failures / account / 15 minutes → Turnstile required.
  2. Hard limit — continued failures → temporary lock or exponential backoff.
  3. IP fan-out — one IP failing on many accounts → block/challenge that IP.
  4. MFA — for admin and high-value accounts.
  5. 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/403 on 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; reject limit=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

  1. Confirm 401 rate and top IPs/users.
  2. Tighten login limits temporarily; force Turnstile globally if needed.
  3. Notify users with unusual success-after-failures if takeover suspected.
  4. Keep signup open if possible — locking everyone out is a secondary outage.

AI bill runaway

  1. Kill switch feature flag on generation routes.
  2. Lower caps; enable provider hard limit.
  3. Rotate provider keys if leak suspected.
  4. Audit which users/tokens burned quota.

Scrape of public catalog

  1. Add auth or signed URLs if data was not meant to be open.
  2. Cache public GETs at CDN with bot management.
  3. 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.

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

01
What rate limit should a SaaS login endpoint use?
Start with something like 5 failed attempts per account per 15 minutes, plus a per-IP cap across accounts (e.g. 20 failures/hour) to slow password spray. Add CAPTCHA/Turnstile after a few failures and temporary lockout only with a clear unlock path.
Q&A
02
Is express-rate-limit enough for production?
In-memory express-rate-limit is fine for single-node demos. Multi-instance or serverless apps need a shared store (Redis/Upstash) so attackers cannot bypass limits by hitting different instances. Use sliding window or token bucket in Redis for production.
Q&A
03
How do I stop AI endpoint cost abuse?
Per-user token budgets, per-day spend caps at the provider, aggressive rate limits on generation routes, auth required for all paid-model proxies, and alerts at 50/80/95% of budget. Never expose provider API keys in the browser.
Q&A
04
Does Cloudflare Turnstile replace rate limiting?
No. Turnstile reduces automated browser abuse on forms. API clients, stolen tokens, and scripted calls with valid sessions still need server-side rate limits and authz. Use both.
Q&A
05
What should I log for API abuse detection?
Timestamp, route, method, user id (if any), IP, user-agent, status code, latency, and rate-limit decisions. Alert on spikes in 401/403/429, fan-out across user ids from one IP, and unusual token consumption on AI routes.
Q&A

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

RUN API SCAN