Skip to content
← Back to the blog
securityapps

Rate limiting AI features so one user can't run up your bill

How to put per-user limits, token caps and a global budget on the AI features in your app, where to enforce them, and how to tell users when they hit one.

By Devonauts Team6 min read
Leer en español →
A brass-framed hourglass with sand running through it on a wooden table

To stop one user from running up your AI bill, enforce limits on your server, per signed-in user: a cap on requests per minute, a daily or monthly quota of requests or tokens, a maximum size for each prompt and each answer, and a global budget that switches the feature off before your provider limit is reached. Limits set only in the browser, or only in the provider's dashboard, don't protect you, because the first can be skipped and the second stops everyone at once.

This guide explains which limits matter, where each one belongs, a simple way to count usage in a Postgres database such as Supabase, and how to show users a limit without making the feature feel broken.

Why do AI features need their own limits?

Ordinary endpoints cost you almost nothing per request. An AI call can cost real money every time, and the price grows with the length of the input and the output. That changes the risk. A script that calls your "summarise" button in a loop, a user who pastes a whole book into the box, or a bug in your own frontend that retries forever can each produce a bill in hours.

OWASP lists this as "Unbounded Consumption" in its Top 10 for large language model applications: letting users trigger uncontrolled, expensive model use. It is also a security issue, because it is the cheapest way for someone to hurt a small business: make it pay.

Which limits should I set?

Think in layers. Each one catches something the others miss.

LimitWhat it stopsTypical shape
Must be signed inAnonymous scripts using your featureReject requests without a valid session
Requests per minute per userLoops, double clicks, retry stormsA small number per minute
Quota per day or month per userOne account using far more than its planRequests or tokens per period, by plan
Input size capHuge pastes that cost a lot per callMaximum characters or tokens accepted
Output capVery long answersThe model's maximum output tokens setting
Concurrency per userMany parallel calls at onceOne or two in flight per user
Global budgetEverything at once, including bugsSwitch off the feature near a daily spend
Provider hard limitThe last line if all else failsMonthly cap in the provider dashboard

The numbers depend on your margins. Start from what a paying user on each plan is worth to you per month and work back to how many calls that pays for. If you charge for usage, our guide to usage-based billing with Stripe covers metering on the billing side.

Where should the limits be enforced?

On the server, in the same function that holds the AI key and makes the call. The browser can show a counter, but it can't be trusted to respect it: anyone can call your endpoint directly.

The order inside that function matters:

  1. Check the session and get the user ID from it, never from the request body.
  2. Check the per-minute limit and the quota. If either is exceeded, stop here.
  3. Check the input size. Reject or trim before you pay for it.
  4. Call the provider with an explicit maximum output.
  5. Record what was used (requests, and tokens if the provider returns them).

If the AI key is still in your frontend, fix that first: a limit in your server function means nothing if the key can be used without it. Our guide on an API key exposed in your frontend shows how to move it.

How do I count usage per user?

You need a counter that every call updates safely, even when two requests arrive at the same moment. Two common options:

  • A Redis-style store such as Upstash, which offers ready-made rate-limit helpers and is fast for per-minute windows. Supabase's documentation has an example of rate limiting Edge Functions with Upstash Redis.
  • A table in your own database. Slower than Redis but simple, and fine for daily or monthly quotas.

A minimal daily quota in Postgres looks like this:

create table public.ai_usage (
  user_id uuid not null references auth.users (id) on delete cascade,
  day date not null default current_date,
  requests int not null default 0,
  primary key (user_id, day)
);

alter table public.ai_usage enable row level security;
-- No policies: only server code using the secret key touches this table.

Your server function then increments and reads the counter in one statement, so two simultaneous calls can't both slip under the limit:

insert into public.ai_usage (user_id, day, requests)
values ($1, current_date, 1)
on conflict (user_id, day)
do update set requests = public.ai_usage.requests + 1
returning requests;

If the returned number is above the user's daily allowance, return an error instead of calling the model. Add a tokens column if you want to cap by cost rather than by count.

What should users see when they hit a limit?

A clear message, not a generic error. Return HTTP status 429 Too Many Requests from your function, with a Retry-After header for short-term limits, and show the user something specific:

  • For the per-minute limit: "You're going a bit fast. Try again in a few seconds."
  • For the daily or monthly quota: how much they have used, when it resets, and, if you sell plans, what the next plan includes.

Show remaining usage before people run out. A small "12 of 50 left today" line prevents most support emails. Plan-based quotas pair well with checking the plan on the server, which we cover in limiting features by plan in a SaaS.

How do I set a global safety switch?

Per-user limits don't help if you have a bug, or if many new accounts are created to farm free quotas. Add one more check before every AI call: today's total spend (or total tokens) across all users. When it crosses a threshold you choose, turn the feature off for free users, or for everyone, and alert yourself.

Behind that, set a hard monthly limit in the provider's own dashboard. It is a blunt tool, since it stops all AI traffic at once, so treat it as the last line, not the plan. Our guide to controlling AI API costs covers the cost side: caching, cheaper models and shorter prompts.

How do I test that the limits work?

  • Call the function signed out: it should refuse.
  • Call it quickly many times from one account: the per-minute limit should kick in.
  • Set your own quota to a low number in a test environment and use it up.
  • Send an input larger than the cap and confirm it is rejected before any provider call is made.
  • Check your provider's usage page after the tests to confirm the rejected calls cost nothing.

Quick answers

Is the provider's spending limit enough? No. It protects your wallet but takes the feature down for everyone at once, including paying customers.

Should anonymous visitors get AI features? Only with a very small, per-IP limit and a CAPTCHA, or not at all. Accounts make limits enforceable.

Requests or tokens: which should I limit? Requests are simpler to explain to users. Tokens match your costs more closely. Many apps limit requests per minute and tokens per month.

Can my AI tool add this for me? It can write the code, but check that the limit runs on the server, before the AI call, and uses the user ID from the session.

Want a second pair of eyes?

The free health check shows what your live app exposes from outside. If you'd like a person to review how your AI endpoints are protected and add the limits, that fits our maintenance and support plans or a Supabase security review; you can also ask us directly.

Still stuck with your app?

Run the free health check on your app's URL, or get the $299 App Rescue Audit: a senior engineer reviews the code and sends you a written report in 2 business days.