Every public API needs rate limiting, or one bad actor, or one buggy script, can hammer it into the ground. But clumsy rate limiting punishes your best customers along with the abusers. The goal of API rate limiting is to stop abuse while normal use never notices the limit exists.
Abuse isn’t always malicious. A customer’s own script stuck in a retry loop, or a sync job someone set to run every few seconds, can do as much damage as an attacker. Rate limiting is what stops one mistake from becoming everyone’s outage. Unrestricted resource consumption is common enough that it appears in the OWASP API Security Top 10.

Choose the right key
Limiting by IP alone is blunt: an entire office behind one IP shares the limit, and one heavy user there blocks everyone. For an authenticated API, limit per API key or per account so each customer gets their own budget and one customer can’t affect another.
Unauthenticated endpoints are different. A login, a password reset or a signup has no account to key on yet, so limit by IP there and also by the target, such as the email address being tried. That second key is what slows down a distributed attempt to guess one user’s password. I cover the wider picture in authentication mistakes found in small SaaS apps.
Set limits to real usage, and be forgiving
Look at how your real users behave and set the ceiling well above normal peaks. A limit that a legitimate integration hits during normal work is too low. Different endpoints can have different limits, a cheap read can allow far more than an expensive export. Use a token bucket so short bursts (which real apps do) are allowed while sustained abuse is still capped.
Which API rate limiting algorithm to use
There are three common ways to count, and the choice decides how the limit feels to a real client:
| Algorithm | How it works | The catch |
|---|---|---|
| Fixed window | Counts requests per clock minute (or hour) and resets at the boundary | Simple, but a client can send a full budget at the end of one window and another at the start of the next |
| Sliding window | Counts requests over the last N seconds, whenever you ask | Smoother, but a little more work to track |
| Token bucket | A bucket refills at a steady rate and each request takes a token | Allows bursts up to the bucket size and caps the sustained rate; you tune two numbers instead of one |
For most SaaS APIs I reach for a token bucket per API key, stored somewhere fast and shared, such as Redis, so every app server sees the same count. Counting in each server’s memory breaks as soon as you run more than one instance, because each server hands the customer a full budget of its own.
Tell clients where they stand
Then communicate. Return the limit, how many requests remain, and when it resets in response headers, and use the standard 429 status with Retry-After when someone is throttled. A developer who can see the limits will design around them; one who gets a silent failure will just be angry.
In practice that means three things:
- Headers on every response. Many APIs use
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset. Pick a format, document it, and keep it consistent across endpoints. - A proper 429. The 429 Too Many Requests status tells any HTTP client exactly what happened, and
Retry-Aftertells it how long to wait. - A readable error body. Say which limit was hit and link to the page in your docs that explains the limits.

Common mistakes
- Returning 500 or 403 when throttling. The client can’t tell "slow down" from "broken" or "forbidden", so it either gives up or retries harder.
- One global limit for every endpoint. A limit tight enough to protect an expensive report is far too tight for a cheap status check.
- Throttling your own front end. A dashboard that fires many requests on load can trip limits designed for third-party scripts. Give first-party traffic its own budget, or make the page ask for less.
- Throttling search engines on your public site. Google treats a 429 as a sign the server is overloaded and slows its crawling. If your marketing pages sit behind the same limiter and aren’t showing up, rule this out along with the usual causes in why a website is not ranking on Google.
- No way to raise the limit. Your biggest customers will eventually need more. Limits per plan, or a simple way to ask for a higher one, turn throttling into an upgrade conversation instead of a support ticket.
Limits beyond the API
The same idea protects every public form you run. A contact form on a SaaS marketing page, or a quote request form on a trade site, gets hit by bots too. A per-IP limit on submissions, plus a hidden honeypot field, keeps most spam out without putting a puzzle in front of real people. The form still has to be quick for humans, which is a big part of why contractors’ websites don’t get the phone ringing.
How to roll it out safely
- Log first. Run the limiter in a log-only mode for a while, recording who would have been throttled.
- Review the log. If a legitimate customer would have been blocked, raise the ceiling or talk to them about their integration.
- Enforce with headers already in place, so clients can see their budget from the first request.
- Alert on 429s per customer. A sudden spike usually means a broken integration, and you want to know before they email you.
Done well, rate limiting is invisible to everyone except the traffic you actually wanted to stop. If you are building an API and want limits like this designed in from the start, it is part of how I approach SaaS development.




