A free tier is the cheapest door into your system, and it stays that way whether the person walking through it is a real prospect or a script testing stolen credit cards. If you don't put a cost gate at that door, someone else will find it before your first paying customer does.
The abuse shows up as a bill, not a warning. LLM token spend climbs on a Tuesday for no product reason. A shared connection pool that served two hundred trial signups fine for months suddenly can't keep up because forty of those signups are the same bot rotating email addresses. Support tickets from real customers start mentioning slow responses, and the root cause turns out to be someone else's scraper running against your product catalog.
The fix isn't 'add rate limiting' as a single checkbox. It's deciding which layer enforces the limit, what unit the limit is measured in, and how you tell a bot from a customer who just started a big job. Get that wrong and you either bleed money on your free tier or throttle the exact users you're trying to convert.
Why free tiers take the abuse first
Every unauthenticated or low-friction endpoint in your product is a candidate for abuse, because it's the one an attacker can hit without first proving they're a real customer. Signup forms get hit by bots farming referral credits or trial API keys. Checkout and payment-method endpoints get hit with small authorization attempts as someone tests a batch of stolen card numbers before using the working ones elsewhere. Public search or catalog endpoints get scraped because they're the only pages that don't require a login.
None of this is a hypothetical risk you can wave off until it happens. Running Node microservices on Kubernetes against fleets of per-tenant Postgres databases across dozens of platforms taught us that the noisy-neighbor problem isn't only about tenants sharing infrastructure fairly. It's about one anonymous IP consuming resources a hundred real trial users were supposed to share.
Rate limit at the edge, not only deep in your stack
If a request never should have reached your application, don't let it. Enforcing a first-pass limit at the edge, before the request hits your origin, your database, or a paid API call, is the cheapest place to reject abuse, because you're not paying compute or third-party API cost for a request you're about to throw away. We've built this pattern running on Cloudflare Workers, D1, and Pages, where the edge layer holds a rough per-identity counter and rejects obvious over-limit traffic before it ever calls the origin.
The tradeoff is consistency. Edge counters distributed across points of presence are eventually consistent, not exact. A determined abuser spread across regions can squeeze a few extra requests past a distributed bucket before it catches up. That's fine for repelling bulk scraping and credential stuffing. It is not fine for anything you bill against or use as a hard security boundary; that needs a single source of truth in a central store, checked at the point where the cost actually gets incurred.
Price the limit in the unit you actually pay for
Requests per minute is the wrong unit if your cost driver isn't requests. A search call against a cached index and a vision-model call against a construction drawing don't cost the same, and a limiter that treats them the same either starves the cheap endpoint or lets the expensive one bleed you dry. Weight every endpoint by its real cost and give each identity a budget in that unit, not a flat request count.
A free-tier account might get 50,000 cost units a day. A cached search call costs 1 unit. An AI-assisted takeoff call, the kind we run against raw construction plans for BuildAEC, costs 400 units. The same daily budget then behaves correctly for both endpoints without a separate hand-tuned rule for each one.
This is the same discipline behind entitlement gating and AI spend wallets: the limit should track the line item on your cloud bill, not a round number that felt safe in a planning meeting. It's the kind of plumbing we build as part of SaaS platform engineering work, because it touches billing, auth, and infrastructure at once.
A limiter you can implement this week
The mechanism is a token bucket per identity per endpoint class, checked before the request runs and updated after. Each bucket has a capacity, the burst allowance, and a refill rate, the sustained allowance. Store it wherever your edge layer already has fast access, whether KV, D1, or Redis, keyed by a tuple, not a single field, so an anonymous IP and an authenticated free account get separately tracked budgets even if they end up being the same person.
type BucketKey = { identity: string; endpointClass: 'cheap' | 'ai' | 'auth' };
function checkAndConsume(key: BucketKey, cost: number, now: number) {
const bucket = store.get(key) ?? { tokens: capacityFor(key), last: now };
const elapsed = now - bucket.last;
bucket.tokens = Math.min(capacityFor(key), bucket.tokens + elapsed * refillRateFor(key));
if (bucket.tokens < cost) {
return { allowed: false, retryAfter: (cost - bucket.tokens) / refillRateFor(key) };
}
bucket.tokens -= cost;
bucket.last = now;
store.set(key, bucket);
return { allowed: true };
}
Three separate buckets per identity, one for cheap endpoints, one for AI or otherwise expensive calls, one for auth-adjacent actions like password reset and login, stops the common failure mode where a generous limit on search accidentally becomes a generous limit on your most expensive AI endpoint too.
Telling abuse from a legitimate spike
A blanket per-minute cap punishes exactly the customer you want to keep: the one running a real batch job. A retailer pushing a full catalog sync to Amazon, eBay, and Google Shopping overnight generates a burst of API calls that looks identical to a scraper on a request-count graph. We've operated that exact traffic shape in Retailer OS, and the signal that separates the two isn't volume, it's shape and provenance: a real sync job is tied to a known job ID, started from an authenticated session, moves through a predictable sequence of endpoints, and tapers off when the job finishes. A scraper or credential-stuffing run doesn't taper. It just keeps going at the same rate until it's blocked.
- Many new accounts created from the same IP or device fingerprint within minutes of each other
- Traffic that only ever hits the cheapest, least-instrumented endpoints in a repeating pattern
- Request volume with no corresponding session activity elsewhere in the product, no page views, no support activity, nothing but API calls
- A burst that doesn't decay: legitimate batch jobs finish, abuse doesn't stop on its own
The honest tradeoff: any automated signal you build to catch this will have false positives. A real customer running their first big import will occasionally look exactly like the thing you built the rule to stop. Decide in advance whether a false positive should hard-block or soft-throttle with a visible message and a path to a human. For a free tier, soft-throttle with a clear explanation is almost always the safer default, because the cost of losing a real signup is higher than the cost of a few extra scraped pages.
What to keep so the decision holds up later
Every throttling or block decision should leave a record that answers why you stopped this particular request months later, not just a spike on a dashboard. Log the identity, the endpoint class, the bucket state at the moment of the decision, and whether the call was allowed or rejected. The same discipline we apply to usage metering that has to survive a customer dispute applies here, because a blocked request and a billed request are the same kind of claim: something the customer will eventually ask you to justify with more than a graph.
That log also becomes the source of truth if a free-tier user upgrades and asks why their trial felt slow, or if a security review, internal or from a prospective enterprise buyer, asks how you detect and log automated abuse. Capture the decision and its inputs at the time it's made; reconstructing it after the fact from raw request logs alone rarely works.
The limiter that matters most is the one you can explain in one sentence to the customer it just blocked. If your rule can't be stated that simply, it's probably encoding an assumption about traffic patterns that will be wrong the first week a real customer does something unusual with your product.
Frequently asked
What's a reasonable rate limit for a free-tier API?
There's no universal number. Size the limit around your actual cost per call, not a round request count. Give each identity a budget in cost units, where a search call costs 1 unit and an AI call costs hundreds, sized so a determined free-tier abuser costs you less than one converted customer is worth. Revisit it as your unit costs change.
Should I rate limit by IP address or by account?
Both, as separate buckets. IP-based limits catch pre-signup abuse: scraping, credential stuffing, mass account creation. Account-based limits catch abuse from an authenticated identity, including one IP legitimately running many accounts behind NAT or a corporate network. Relying on only one lets the other slip through entirely.
How do I stop credential stuffing without adding a CAPTCHA to every login?
Rate limit login and password-reset endpoints separately from the rest of your API, add progressive delay after repeated failures from the same identity, and only escalate to a CAPTCHA or manual review past a threshold. Most real users never see it, and bots hitting your login endpoint at volume get slowed until the attack isn't economical.
Does rate limiting have to run at the edge, or is application-level enough?
Application-level is enough if your traffic volume and cost per request are both low. Once a bad request costs you real money, an AI API call, a database write, a third-party lookup, reject it before it reaches your application. Edge enforcement is the cheapest point to do that, though it trades exact consistency for speed.
What should happen when a real customer trips the limit during a legitimate spike?
Soft-throttle rather than hard-block: slow the response, return a clear message explaining the limit and how to request a higher one, and log the event so support can see it without the customer having to explain it twice. Reserve hard blocks for identities that show no other signs of being a real account.