Rate limiting in one sentence
A rate limit is a ceiling on how many requests one client may make in a given period. Under the ceiling nothing happens. Over it, requests are refused — normally with an HTTP 429 Too Many Requests — until the window resets.
That is the whole idea, which is why the same control turns up in API design, abuse prevention and DDoS mitigation at once. It is also the control that gets misconfigured most often, because the interesting decisions are not how many requests. They are what counts as one client, where the counting happens, and what you do to the requests that go over.
Get those three right and a rate limit is the highest-value fifty lines of configuration on your site. Get them wrong and you have either a limit that never fires or an outage you caused yourself, aimed at the users least able to retry.
What rate limiting is actually good at
Rate limits earn their keep against abuse that depends on repetition:
- Credential stuffing and brute force. A login endpoint that answers a thousand password guesses a minute is a password database with a slow API in front of it.
- Scraping. Price lists, listings, catalogues and search results get harvested by the page, in order, quickly. That pattern is trivially expensive for you and trivially cheap for the scraper.
- Expensive endpoints. Search, report generation, PDF export, anything that touches the database hard. A handful of requests per second against the wrong endpoint costs more than tens of thousands against a cached page.
- Card testing and signup abuse. Checkout, coupon validation and registration endpoints get walked through automatically, which is covered in more detail in protecting an online store.
- A runaway integration. Not every flood is hostile. A partner's retry loop with no backoff will take you down exactly as effectively as an attacker, and a limit turns the incident into a graph instead of a phone call.
What all of these share is that a single client is doing something abnormal. The moment that stops being true, so does the usefulness of a limit — a point we come back to at the end, because it is the one that decides whether rate limiting is your defense or just part of it.
The algorithms, and where the difference shows up
Every rate limiter answers the same question — has this client used its allowance? — and the way it counts changes what gets through.
| Algorithm | How it counts | Burst behavior | Cost | Best for |
|---|---|---|---|---|
| Fixed window | One counter per client per clock window | Allows up to 2× the limit across a boundary | Tiny | Crude internal limits |
| Sliding window log | Timestamp of every request, old ones expired | Exact, no boundary effect | High memory per client | Low-volume, high-value endpoints |
| Sliding window counter | Current window plus a weighted share of the previous one | Near-exact, no boundary spike | Tiny | The sane general default |
| Token bucket | Tokens refill at a steady rate into a bucket of fixed size | Deliberately allows a burst up to the bucket size | Tiny | Human traffic and public APIs |
| Leaky bucket | Requests queue and drain at a constant rate | Smooths everything, no burst at all | Small | Shielding a fragile backend |
The fixed window deserves its bad reputation. With a limit of 100 per minute, a client that sends 100 requests at 11:59:59 and another 100 at 12:00:00 has sent 200 requests in one second and broken nothing, because each landed in a different counter. Attackers find that boundary immediately; so do retry storms, which tend to align on the minute.
Token bucket is usually the right shape for anything a human touches. Real browsing is bursty — one page load is thirty-odd requests in two seconds, then twenty seconds of nothing — and a limiter that refuses bursts on principle will break normal use long before it inconveniences a script. A bucket that holds sixty tokens and refills at one per second permits the burst and still caps the sustained rate.
Leaky bucket is the opposite trade: it delays instead of refusing, which protects a fragile origin beautifully and ruins latency for anyone in the queue. Use it in front of a backend that must not be overwhelmed, not in front of your website.
What you key the limit on decides everything
This is where most rate limiting quietly fails. The counter has to be attached to something, and every choice is a compromise.
| Key | Works well for | Fails when |
|---|---|---|
| IP address | Anonymous traffic, obvious single-source abuse | Carrier-grade NAT, offices, schools and VPNs put thousands of people behind one address; a botnet has more addresses than you have limits |
| IPv6 prefix (/64) | IPv6 clients | Only if you key on the prefix — a single host is routinely handed trillions of individual addresses |
| Session or cookie | Logged-in flows, checkout, account actions | An attacker simply discards the cookie and arrives as a new visitor |
| API key or token | Machine clients, partner integrations | Leaked or shared keys; unauthenticated endpoints have no key to count |
| Account identity (username, email) | Login, password reset, verification | Only usable on endpoints where identity is submitted — but essential there |
| Client fingerprint (TLS + HTTP signature) | Anonymous traffic that must not be blocked wholesale | Requires an edge that terminates TLS and sees the whole population |
Two practical rules come out of that table.
Never key an authentication limit on IP alone. Credential stuffing spreads guesses across a botnet precisely to stay under per-IP limits, and a per-IP limit tight enough to catch it will lock out an entire office sharing one address. Limit per account and per source, with different numbers: a handful of attempts per account per quarter-hour, a looser ceiling per address.
Assume IP addresses are shared. Mobile carriers routinely put six figures of subscribers behind a single NAT address. If your limit is per-IP and your traffic is mobile, you are rate limiting a city.
Where you enforce it
The same limit does very different work depending on how far the request has already travelled before it is counted.
| Enforced at | Stops the request from | Still costs you |
|---|---|---|
| Application code | Touching the database, sending mail, doing real work | Bandwidth, TLS handshake, a worker, a framework boot |
Origin reverse proxy (nginx limit_req, Apache) | Reaching the application at all | Bandwidth, connection state, and your origin's own capacity |
| Edge / reverse-proxy network | Reaching your infrastructure at all | Nothing on your side |
Application-level limits are useful — they are the only place that knows what an account is — but they are also the last line, and by the time the check runs you have paid for almost everything except the query. Origin-level limits with nginx's limit_req are cheap, effective and the right thing to configure regardless. They still cannot help with a flood large enough to fill the pipe or the connection table in front of them, which is the same argument as in low and slow DDoS attacks: a limit only helps once the request is already yours to refuse.
Enforcement at the edge is the version that scales, because the request is refused on a network built to absorb it, in a location near the client, before it consumes anything of yours. The other advantage is visibility: an edge that sees every request across every customer can tell an unusual client from an unusual population, and that distinction is invisible to a limiter that only ever sees one server's traffic.
Belt and braces is the correct answer. Edge limits for volume and abuse, origin limits as a backstop, and application limits for anything that needs to know who the user is.
Choosing numbers that do not hurt real users
Pulling a limit out of the air is how you find out which of your customers had the slowest connection. Measure instead.
- Start from your own data. For each endpoint class, look at requests per client per minute across a normal week and find the 99th percentile. Your first limit should sit comfortably above it — two to five times — not on top of it.
- Limit per endpoint class, not per site. One global number cannot be right for both a login form and an image. A single page view might legitimately be forty requests.
- Run in log-only mode first. Watch what the rule would have blocked on live traffic for a week before it blocks anything. Nearly every unpleasant surprise — a monitoring probe, a partner's sync job, your own mobile app's startup burst — shows up here rather than in support tickets.
- Exempt what should be cached instead. Static assets do not need a limit; they need a CDN. Counting them into the same budget as write requests is how limits end up too loose to matter.
- Allowlist verified bots. Googlebot and Bingbot crawl in bursts by design, and rate limiting your way out of the index is an expensive mistake. Verify by reverse DNS rather than trusting the user-agent string.
As a starting shape — adjust to your own percentiles, do not adopt as gospel:
| Endpoint class | Typical honest pattern | Reasonable first limit |
|---|---|---|
| Login, password reset, 2FA | A few attempts, then a pause | 5–10 per account per 15 min, plus a looser per-source cap |
| Signup, coupon, checkout submit | One or two per session | 3–10 per hour per client |
| Search and other expensive queries | Bursty, then idle | 10–30 per minute, token bucket |
| Read API | Steady, machine-paced | 60–600 per minute per key |
| Write API | Much lower than reads | 10–60 per minute per key |
| Static assets and cached pages | Very bursty | No limit — cache at the edge |
What to return when a request goes over
The response is part of the design, not an afterthought. A limiter that refuses correctly keeps well-behaved clients working; one that refuses badly turns a throttle into an outage.
- Use
429 Too Many Requests(RFC 6585). Not403, which says never, and never200with an error page in the body — that teaches every client, cache and crawler that the failure was a success. - Always send
Retry-After. It is the difference between a client that backs off and a client that hammers you harder at exactly the moment you asked it to stop. - Expose the budget with
RateLimit-Limit,RateLimit-RemainingandRateLimit-Resetheaders so integrators can pace themselves instead of discovering the limit by hitting it. - Never answer a machine client with an HTML challenge page. An API caller cannot solve it, cannot parse it, and will retry forever — the full reasoning is in protecting an API from DDoS attacks.
- Log every rejection with the rule that fired and the key it counted. A limit you cannot audit is a limit you will be afraid to tighten.
- Fail open on the limiter itself. If the counter store is unreachable, serving traffic unlimited is almost always better than refusing all of it.
Where rate limiting stops working
Here is the arithmetic that decides how much of your DDoS strategy a rate limit can be.
Say you set a generous 60 requests per minute per IP. An attacker with 5,000 hosts — a small botnet, rentable by the hour — instructs each one to send 59. Nothing trips a limit anywhere, and your origin receives 295,000 requests a minute. Every individual client was well behaved. The population was not.
That is the defining property of a Layer 7 DDoS attack, and it is why a limit alone cannot be the answer. To catch that traffic with a static threshold you would have to lower the limit to a handful of requests per minute per address, at which point you have blocked the office, the mobile carrier and the university before you have inconvenienced the botnet. Low and slow attacks evade limits from the other direction: they send almost no requests at all, and simply refuse to finish the ones they send.
So the honest framing is that rate limiting is a floor, not a ceiling. It removes the cheap, noisy, single-source abuse that would otherwise consume your attention, and it makes everything above it easier to see. What sits above it is behavioral analysis across the whole population, client fingerprinting that survives an IP change, challenges that separate browsers from scripts, a WAF for the request content a limiter never inspects, and caching so most requests never reach an origin at all.
How rate limiting works at Itnetic
Itnetic is a reverse-proxy edge, so limits are enforced at the point of presence nearest the client — the refused request never reaches your server, never consumes your bandwidth and never occupies a worker.
- Rate limiting is on every plan, including the free Starter plan. It is not a tier upgrade, because a control this basic should not be one — see pricing.
- Limits are keyed on more than an address. Client fingerprinting from TLS and HTTP-level signals identifies the tooling behind a request regardless of the user agent it claims, so a rotating botnet does not get a fresh budget with every new IP, and a shared office address does not get treated as one abusive client.
- API zones return
429withRetry-After, never an HTML interstitial, so machine clients degrade gracefully instead of breaking. - Custom WAF rules run in log-only mode first, which is the safe way to discover the real shape of your traffic before a rule starts refusing anything.
- Adaptive Layer 7 detection builds a behavioral baseline per host and endpoint, which is what catches the distributed, politely-under-the-limit traffic that no static threshold can price.
- Per-request logs record status, latency, TLS details, client fingerprint and the rule and verdict behind every decision, so you can see exactly who was limited and why — details in logs and analytics.
- Verified good bots stay allowlisted so search crawlers keep working while everything else is measured.
- Attack traffic is never metered against your bandwidth quota, and going live is two DNS records and about five minutes.
A short checklist
- Rate limit authentication endpoints per account and per source, with different numbers.
- Pick a sliding window or token bucket; retire any fixed-window limit that guards something that matters.
- Measure your 99th percentile per endpoint class before choosing a number.
- Run every new limit in log-only mode for a week.
- Return
429withRetry-After, and never an HTML challenge to an API client. - Cache static assets instead of counting them.
- Allowlist verified crawlers by reverse DNS, not by user agent.
- Enforce at the edge, keep origin limits as a backstop, and assume a distributed attack will walk under all of it — which is what always-on mitigation is for.