Abuse, misuse and rate limiting
Protect AI features from abuse with rate limits, quotas, misuse detection and cost-attack defenses, and handle users who push a feature beyond its purpose.
On this page
Why this matters
Every request to an AI feature costs you money. That changes the economics of abuse: a scraper hitting a normal page costs you a little bandwidth; a script hammering your AI endpoint costs you real model spend, possibly a lot of it, possibly overnight. And a free, capable AI feature is an attractive target for people who want free compute for purposes that have nothing to do with your product.
Abuse also has a safety side: people trying to make your feature produce harmful content, or use it to target others. This article covers the kinds of abuse to plan for, how to design rate limits and quotas, a working limiter, and how to detect and respond to misuse without punishing legitimate users.
Kinds of abuse
| Kind | What it looks like | Main risk |
|---|---|---|
| Free compute | Using your feature as a general-purpose AI for unrelated tasks | Cost, and your terms being broken |
| Cost attacks | Scripts sending large, expensive requests as fast as possible | A large bill in a short time |
| Scraping and extraction | Systematically pulling outputs or trying to extract your prompts | Lost work, competitive exposure |
| Harmful content | Trying to get the feature to produce content that harms others | Safety, legal and reputational harm |
| Account farming | Many free accounts created to multiply free quotas | Cost, distorted metrics |
Model providers apply their own safety measures, and models decline many harmful requests. That’s a layer, not a replacement for your own limits: your provider can’t know your feature’s purpose, your users or your budget.
Rate limits and quotas
Limits work best in layers, each catching something the others miss:
Show text descriptionHide text description
A stack of 5 layers, listed from top to bottom.
- Per request : Maximum input size and max_tokens, so no single request can be enormous
- Per user, per minute : A burst limit that stops scripts without bothering people
- Per user, per day : A cost budget by plan: free users get less than paying ones
- Per plan, per month : Usage included in the price; beyond it, upgrade or pay per use
- Global : A spending limit on the provider account and alerts on total spend
Design guidance:
- Budget in money, not just requests. A request with a 100-page document costs far more than a one-line question. Track estimated and actual cost per user.
- Size limits to real usage. Look at how much your heaviest legitimate users use, and set daily budgets comfortably above that for their plan. Limits that hit real users are bugs.
- Make limits visible. Show remaining usage where it matters, and when a limit is hit, say when it resets or how to get more. “You’ve used today’s drafting allowance. It resets at midnight UTC, or upgrade for more” is a fine message; a generic error isn’t.
- Enforce on the server. Client-side limits are suggestions.
A rate limiter in TypeScript
Here’s a combined limiter: a token bucket per user that allows short bursts but caps the sustained rate, and a daily cost budget per user based on their plan. Before a request, it checks both using the estimated cost; after the request, it records the actual cost from the response’s usage.
type Plan = 'free' | 'pro';
const limits: Record<Plan, { burst: number; refillPerSecond: number; dailyBudgetUsd: number }> = {
free: { burst: 5, refillPerSecond: 0.2, dailyBudgetUsd: 0.25 }, // 5 quick requests, then one every 5 s
pro: { burst: 20, refillPerSecond: 1, dailyBudgetUsd: 5 },
};
interface UserState {
tokens: number;
lastRefill: number;
day: string;
spentUsd: number;
}
// In production, keep this in a shared store such as Redis, so every server instance sees the same counts.
const state = new Map<string, UserState>();
export type LimitDecision = { allowed: true } | { allowed: false; reason: 'rate' | 'budget'; retryAfterMs: number };
export function checkLimit(userId: string, plan: Plan, estimatedCostUsd: number, now = Date.now()): LimitDecision {
const { burst, refillPerSecond, dailyBudgetUsd } = limits[plan];
const day = new Date(now).toISOString().slice(0, 10);
const s = state.get(userId) ?? { tokens: burst, lastRefill: now, day, spentUsd: 0 };
if (s.day !== day) Object.assign(s, { day, spentUsd: 0 }); // New day, new budget.
// Refill the bucket for the time that has passed, up to its capacity.
s.tokens = Math.min(burst, s.tokens + ((now - s.lastRefill) / 1000) * refillPerSecond);
s.lastRefill = now;
state.set(userId, s);
if (s.spentUsd + estimatedCostUsd > dailyBudgetUsd) {
const midnight = Date.parse(`${day}T00:00:00Z`) + 86_400_000;
return { allowed: false, reason: 'budget', retryAfterMs: midnight - now };
}
if (s.tokens < 1) {
return { allowed: false, reason: 'rate', retryAfterMs: Math.ceil(((1 - s.tokens) / refillPerSecond) * 1000) };
}
s.tokens -= 1;
return { allowed: true };
}
/** Call after each request with the real cost from response.usage, including requests that failed or were cut off. */
export function recordCost(userId: string, actualCostUsd: number): void {
const s = state.get(userId);
if (s) s.spentUsd += actualCostUsd;
}
// A script sends 8 requests in one second, then the user keeps going slowly.
let t = Date.parse('2026-10-17T12:00:00Z');
const results: string[] = [];
for (let i = 0; i < 8; i++) {
const d = checkLimit('u1', 'free', 0.01, t + i * 100);
results.push(d.allowed ? 'ok' : `${d.reason} (retry in ${(d.retryAfterMs / 1000).toFixed(1)} s)`);
if (d.allowed) recordCost('u1', 0.012);
}
console.log(results.join(' | '));
// Later the same day: enough spend to reach the free daily budget.
t += 3_600_000;
for (let i = 0; i < 30; i++) {
const d = checkLimit('u1', 'free', 0.01, t + i * 10_000);
if (!d.allowed) {
console.log(`Request ${i + 1} an hour later: ${d.reason}, resets in ${(d.retryAfterMs / 3_600_000).toFixed(1)} h`);
break;
}
recordCost('u1', 0.012);
} Things to adapt for production:
- Use a shared store such as Redis, so limits hold across server instances and restarts.
- Estimate before, record after. The estimate uses input size and
max_tokens; the recorded cost uses the actualusagefrom the response, including failed and cut-off requests, because they cost money too. - Return the reason and the retry time to the client, so the interface can explain the limit.
- Key limits by account and by IP or device for free plans, to slow down account farming.
Detecting and responding to misuse
Limits stop volume. Misuse also includes requests that are within limits but outside the feature’s purpose, or harmful. Signals worth watching:
- Off-purpose usage. A support-reply feature receiving requests to write essays or code. A small classifier, or a check in the prompt that flags off-topic requests, can route these to a polite refusal.
- Prompt extraction attempts. Repeated requests asking for your instructions. Assume your prompts can be extracted eventually, and keep secrets out of them.
- Refusal patterns. A user generating many refusals in a short time is worth a look.
- Unusual cost profiles. Consistently maximum-size inputs, or usage that stops matching how real users behave.
Respond in proportion: a friendly message about what the feature is for, then tighter limits for the account, then review and suspension for clear abuse, with a way to appeal. Most “abusers” are curious users testing boundaries; reserve heavy responses for clear patterns. Log decisions so you can review them and handle appeals fairly.
Common mistakes
- Limiting requests but not cost. One large request can cost as much as hundreds of small ones. Budget in money.
- Client-side or per-instance limits. Enforce on the server, in a shared store.
- Limits that hit real users. Base them on your heaviest legitimate usage, by plan, and show remaining allowance.
- Not counting failed requests. Cut-off and failed requests still cost money; record actual usage for every call.
- Secrets in prompts. Assume prompts can be extracted; keep keys, internal URLs and sensitive rules out of them.
Try it
Exercise
About 40 min
Set quotas for an AI feature
- Look at usage for your feature (or estimate it): requests per user per day and cost per request, for typical and heavy users.
- Set burst, daily budget and monthly allowance per plan, comfortably above heavy legitimate use.
- Calculate the maximum a single free account could cost per day, and per month.
- Write the messages users see when they hit each limit.
Show a hint
If you have no data yet, use your cost estimate from estimating cost per task and assume your heaviest users will use the feature ten times more than average.
Show a worked solution
Example for drafted support replies (illustrative):
- Typical agent: 40 drafts a day at about $0.02 each, so $0.80. Heavy agent: 150 drafts, $3.
- Limits: free trial, 5-request burst and $0.50 a day; paid seat, 20-request burst and $8 a day; organization-wide monthly budget with an alert at 80%.
- Worst case per free account: $0.50 a day, $15 a month. With sign-up checks limiting accounts per company domain, acceptable.
- Message: “You’ve reached today’s draft limit for the trial. It resets at midnight UTC. Upgrade for unlimited drafting within fair use.”
Check your understanding
Key takeaways
- AI abuse is a cost risk as well as a safety risk: plan for free compute, cost attacks, scraping, harmful content and account farming.
- Layer limits: request size, burst rate, daily cost budget, monthly allowance and a global spend limit.
- Enforce on the server, in a shared store, and record the actual cost of every request.
- Detect misuse with purpose, refusal and cost signals, and respond in proportion.
Progress is saved in this browser and lights up the roadmap.
Related articles
- Cost dashboards and budgets Track and control AI spend with cost dashboards by feature, model and customer, budgets and alerts, and spend reviewed as a product metric. Level: Expert
- Pricing AI features Price AI features with bundling, usage-based pricing, credits and tiers, protecting margins from heavy users and communicating limits fairly. Level: Expert
- Prompt injection and tool-use risks Understand prompt injection and the risks of giving AI features tools, including attack paths, realistic threat models and defenses that limit the damage. Level: Expert