Why “100 requests per minute” is not a rate-limiting strategy


When one route starts eating more than its share of capacity, such as an export endpoint that an integration is polling all day, the quickest protection is a number: limit requests to 100 per minute.
It’s easy to add to middleware, and it gives clients a clear 429 Too Many Requests response. But it immediately raises harder questions. Is the limit per IP address, per user or per organisation? Should short bursts be allowed? Should an export cost the same as a cached read? And should a payment webhook be rejected because unrelated traffic used up the budget first?
Until you know which resource needs protecting and whose requests should share it, 100 requests per minute is just a counter with a threshold.
In this article, I’ll turn that number into an actual strategy: what each limit protects, who it applies to, how bursts and refusals should work and what happens when the limiter itself fails.
One count for very different work
A single global cap still has value: it can reduce obvious accidental overload, protect a small service from one poorly behaved client and set a basic boundary before more detailed policies exist. Its weakness is that it counts every request as the same amount of work.
A single global count treats all of these operations as equivalent. The cheap route can consume the same budget as the expensive one, a user may be blocked from a core action while the endpoint causing the pressure remains reachable, and a legitimate integration may be throttled because unrelated traffic shares its identity.
What each route is protecting
A rate limit should begin with the resource or failure mode being protected. For example:
The policy follows from that answer. An expensive report route may need a small per-user or per-organisation limit; a read-heavy route may need caching and a higher allowance; a login route may use an IP-level policy before authentication and a user-level policy after it. The point is not to create a unique algorithm for every endpoint, only to stop treating every request as the same kind of work.
Identity determines whether the policy is fair
Every rate limiter needs a key: the identity used to group requests. Common options include:
Each has limitations. IP address can be useful for anonymous abuse, but it can unfairly group users on shared Wi-Fi, mobile networks, offices, universities or schools. User ID is more meaningful after authentication, but it does not protect a public login endpoint. API keys work well for integrations when they are scoped and revocable, but they cannot protect a browser route where no key exists. A more realistic policy is layered:
There is rarely a perfect key. Fairness here has little to do with mathematical evenness: a policy is fair when it stops one actor from consuming a shared resource in a way that harms others, without punishing unrelated users who happen to share an identity.
Fixed windows look correct until the boundary
A fixed-window counter is straightforward:
The boundary creates a familiar issue: a client can send 100 requests at the end of one minute and another 100 immediately at the beginning of the next. The policy technically allows both, but the protected system experiences a concentrated burst. That may be fine for a low-risk endpoint, but not for a route that triggers expensive work. Different approaches create different behaviour:
The choice depends less on which algorithm is most advanced than on two policy questions: should this client be allowed to burst, and how quickly must the system recover afterwards? If a dependency can only process work steadily, a queue or leaky-bucket-like policy may fit. If a client should be able to make a small burst of valid requests, a token bucket states that policy directly.
Token buckets make the burst policy explicit
A token bucket starts with a defined capacity, and tokens are added back at a steady rate. Each request costs one or more tokens; if enough tokens are available, the request proceeds, and if not, it is rejected or delayed.
A client can make a short burst of up to 20 requests. Over time, the average is constrained to five requests per second. That burst allowance is useful for traffic that is legitimate but uneven:
Unlike a fixed window, the allowance does not reset in full when the minute ends: it permits a limited burst while keeping sustained demand within a defined rate.
Not every request should cost one token
Because a request can cost more than one token, the bucket can also reflect how expensive the work is:
This does not mean every route needs a complicated scoring system. It means a cheap health check and an expensive export should not have equal claims on capacity. The same idea applies to whole classes of work, which can draw from separate budgets:
This is where rate limiting connects with product priorities: when the system is busy, which user actions must remain possible?
A local counter is not a distributed policy
An in-memory counter can work in a single development process, but it becomes unreliable when traffic is distributed:
A client may receive 300 requests of effective capacity while the intended limit was 100. That does not mean every project needs Redis immediately. A shared store, API gateway, CDN policy, managed platform feature or dedicated rate-limiting service earns its place when distributed enforcement is a real requirement. Redis is a common option because atomic counters with expiry are useful, but adding it solely to satisfy a system-design checklist is not evidence of good architecture. The system should use the simplest enforcement point that reliably protects the resource.
A shared store also puts a new dependency in front of every request it guards, which raises a question the local counter never had to answer: what happens when the limiter itself is slow or unavailable? Failing closed refuses traffic because enforcement cannot be trusted. Failing open admits it unchecked. Neither is right everywhere, and the choice follows from what the route is protecting. A login endpoint that exists to slow credential stuffing may be safer refusing requests than admitting them without a check. A cached read route limited mainly for fairness may be better off staying available through a short period without enforcement.
Either way, the limiter's own latency and errors need to be visible. Otherwise a limiter that adds delay to every request, or fails open unnoticed, has changed the policy without anyone deciding to.
Reject, queue or delay
A rate limit is a refusal, and a `429` should not feel like a broken product. A client needs enough information to recover. `Retry-After` can be expressed as a delay in seconds, so a client that must wait 42 minutes can receive:
HTTP/1.1 429 Too Many RequestsRetry-After: 2520A user-facing interface should explain that the action did not complete and when it can be retried, without exposing unnecessary internal detail:
Rejection is not the only option. For work that can finish later, a queue may be more appropriate:
That only works when delay preserves the meaning of the workflow. A long report can reasonably become an asynchronous job; a booking confirmation cannot silently enter an unbounded queue.
Rate limits and retries can fight each other
A badly designed client can turn a rate limit into additional pressure:
The client should respect `Retry-After`, apply backoff and jitter, and have a bounded retry budget. Mutations need extra care: if a client times out around the point a request is processed, it may not know whether the operation happened, and retrying blindly can create a duplicate effect.
They solve different problems and need to work together.
Observe who is being limited
A rate limiter that only emits a `429` is difficult to improve. I want to know:
- Which route is rejecting requests?
- Which identity class is affected?
- Are requests legitimate, abusive, accidental, or caused by a broken client?
- Which limits are protecting a real resource?
- Are clients respecting the recovery instruction?
- Are retries creating more rejected traffic?
- Is one customer consuming disproportionate expensive work?
Avoid logging raw personal identifiers unnecessarily; aggregated metrics and safe identifiers are often enough. It also helps to keep rejection reasons distinct:
These may look similar to a user, but they need different operational responses.
Before choosing the number
“100 requests per minute” is not a strategy until it answers:
A rate limiter is an admission-control policy. It does not create downstream capacity. It decides how the capacity that already exists is shared, so that legitimate users keep the service when demand, bugs or abuse would otherwise consume it. The best limit is rarely the strictest one. It protects the requests that matter without letting one source of traffic consume the capacity everyone else needs.