Rate limits help a small web service stay available when a client sends too many requests, whether by mistake, through a retry loop, or as part of abuse. A useful limit must do more than count requests: it must define who is limited, how usage is measured, where the decision is enforced, and what clients should do when they reach the limit.
This guide explains common rate-limiting algorithms, identity choices, proxy considerations, HTTP 429 responses, and the trade-offs involved in deploying limits across multiple service instances.
Key takeaways
- Enforce limits at a trusted server-side boundary; a client-side counter is not protection.
- Choose an algorithm based on the traffic pattern you need to control: fixed windows are simple, sliding windows smooth boundaries, and token buckets allow bounded bursts.
- Combine identity signals thoughtfully. An authenticated account is often more useful than an IP address alone, but no single identifier fits every endpoint.
- Trust forwarded client-IP headers only when they are set by proxies you control.
- Return HTTP 429 with useful retry guidance, and make retry behavior safe for your service.
- In a multi-instance deployment, coordinate counters or deliberately accept the limits of local counters.
Decide what the limit protects
Start by defining the resource or operation you want to protect. A general request-per-minute cap may be useful for an inexpensive read endpoint, but it may not protect a costly report-generation route, a password-reset flow, or an endpoint that triggers third-party work.
For each endpoint group, consider:
- Cost: Does a request consume substantial CPU, database capacity, storage, or paid third-party usage?
- Abuse potential: Could repeated requests enable credential guessing, resource exhaustion, or unwanted messages?
- Legitimate usage: Do clients make short bursts, run batch jobs, or retry after failures?
- Impact of rejection: Is it acceptable to reject excess traffic, or should the service queue work instead?
Use different policies for operations with different risk and cost. For example, a low-cost catalog lookup may allow a higher rate than an endpoint that starts an expensive export. Rate limiting is one control, not a substitute for authentication, input validation, quotas, or capacity planning.
Choose a rate-limiting algorithm
A rate limiter tracks usage over time and decides whether to admit a request. The algorithm determines how it treats bursts and how much state it needs.
Fixed window
A fixed-window limiter counts requests in a fixed interval, such as 60 seconds. It allows requests until the counter reaches the configured limit, then blocks further requests until the next window.
Advantages: It is easy to understand, inexpensive to implement, and uses little state per key.
Trade-off: A client can send requests near the end of one window and again near the beginning of the next. A limit of 60 per minute can therefore admit close to 120 requests over a short period straddling a boundary. Whether that is acceptable depends on the resource being protected.
Sliding window
A sliding-window approach measures usage over a moving interval rather than resetting every client at one shared boundary. A sliding log stores request timestamps and removes entries that have aged out. This can be precise, but storing and checking every timestamp costs more memory and processing.
A sliding-window counter approximates the moving total using counts from the current and previous fixed windows. It requires less state than a full log and reduces boundary spikes, at the cost of approximation. The chosen method should match the precision and operational cost you can support.
Token bucket
A token bucket holds a limited number of tokens. Each request consumes a token, and tokens replenish at a configured rate up to a maximum capacity. The refill rate controls sustained traffic; the capacity controls the size of a burst the service will tolerate.
For example, a policy might replenish 2 tokens per second and hold up to 20. It permits a burst of up to 20 requests when the bucket is full, while limiting sustained use to the refill rate over time. This is often a practical fit for APIs whose clients occasionally send small bursts but should not maintain a high request rate indefinitely.
A token bucket still needs a clear policy for requests that arrive when no tokens remain: reject them, delay them, or enqueue work. Delaying or queuing can add complexity and consume resources, so do not assume it is safer than returning a rejection.
Pick for behavior, not fashion
| Need | Possible starting point | Main trade-off |
|---|---|---|
| Simple cap per time interval | Fixed window | Can allow boundary bursts |
| Smoother rolling limit | Sliding-window counter | Approximate usage; more complex than a fixed counter |
| Controlled bursts with a sustained rate | Token bucket | Requires tracking refill state accurately |
| Exact recent-request history | Sliding log | More state and work per request |
Measure the traffic and failure modes that matter. A mathematically sophisticated algorithm is not automatically a better operational choice if its state store is unreliable or its policy is difficult to explain.
Choose the identity that the policy limits
A rate limit is applied to some key: the identity whose usage is counted. Choose that key according to the endpoint and the behavior you want to control.
- Authenticated account or API key: Useful for per-customer quotas and protecting account-specific operations. Ensure the identity is authenticated before relying on it, and define how multiple keys belonging to one account are treated.
- IP address: Useful as one signal for unauthenticated traffic. It is imperfect: many legitimate users may share an address, while a single client may appear under different addresses. Mobile networks, corporate gateways, and carrier-grade NAT can make address-based limits especially blunt.
- User or session: Can help limit user-specific actions, but sessions may be recreated, shared, or unavailable before login.
- Endpoint or operation: Keep separate counters for materially different costs. A single global counter can let cheap traffic consume capacity intended for critical operations, or allow an expensive route too much traffic.
- Combination of signals: A service may apply both a per-account limit and an IP-based limit, with separate thresholds and purposes. Avoid creating a huge number of keys from arbitrary client input, which can increase storage use.
For login and account-recovery routes, an IP-only limit may be unfair to shared networks, while an account-only limit can be abused to deny service to a targeted account. Consider layered controls, careful monitoring, and a response policy that does not reveal whether an account exists.
Put enforcement on the server side
A limit enforced only in browser code or a mobile app is not an effective server-side protection. A client can change or skip its own checks, and other clients may call the API directly. Enforce the admission decision in infrastructure you control: for example, in an API gateway, reverse proxy, or application middleware.
Choose a layer that has the identity and context needed for the policy. An edge proxy may cheaply reject obvious excess traffic before it reaches the application. Application-level enforcement may be necessary when limits depend on authenticated accounts, endpoint-specific costs, or business rules. Some services use both, with clearly separated responsibilities.
Configure proxy identity carefully
When a service sits behind a proxy, the connection's immediate peer address is usually the proxy, not the original client. Proxies may pass an address in headers such as Forwarded or X-Forwarded-For, but those values are only trustworthy when they come through a known, correctly configured proxy chain.
A safe approach is to:
- Identify which proxy addresses or network ranges are trusted to connect to the application.
- Configure the framework or gateway to accept forwarded client information only from those trusted proxies.
- Ensure the edge proxy overwrites or sanitizes incoming forwarding headers rather than blindly passing client-supplied values as authoritative.
- Test direct access paths, proxy hops, and deployments where the proxy configuration changes.
If an application accepts a client-supplied forwarding header as the identity key, a caller may be able to change that value from request to request and evade an IP-based limit. Conversely, treating every request as the proxy's address may group unrelated clients into one counter. Proxy trust is a deployment boundary, not just a header-parsing detail.
Return a useful 429 response
When a request exceeds a limit, HTTP 429 Too Many Requests is the usual response. Keep the response understandable and avoid exposing internal counters or implementation details that clients do not need.
For example:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 10
{"error":"rate_limit_exceeded","message":"Try again later."}
Retry-After can communicate when a client should retry, either as a delay in seconds or as an HTTP date. Choose a value consistent with the policy and avoid telling the client to retry before the request is likely to be admitted. Standardized rate-limit fields may also communicate a limit, remaining capacity, or reset information where your API supports them; document the exact behavior you expose.
Clients should not immediately retry every 429. That can create a retry storm and worsen the overload. They should respect Retry-After when present, use bounded exponential backoff when appropriate, and add jitter so many clients do not retry simultaneously. For operations that are not safe to repeat, use an idempotency strategy or make the operation's retry behavior explicit.
A 429 response means the request exceeded a policy for the client or identity. If the service is generally overloaded and cannot accept work regardless of the caller's usage, a service-unavailable response may better describe the condition. Do not use a rate-limit response to conceal unrelated server failures.
Plan for multiple service instances
A counter stored only in application memory is local to one process. If a load balancer distributes requests among several instances, each process can independently admit traffic, so the effective service-wide limit may be higher than the configured per-process limit. Restarts also discard local state.
Common deployment choices include:
- Local counters: Simple and fast, but limits apply per instance and state is not shared. Appropriate only when that behavior is intentional or when a separate layer provides the effective shared limit.
- Shared store: A cache or datastore can coordinate counters across instances. This provides a more consistent shared view, but adds network latency, operational dependency, and the need for atomic updates and expiry behavior.
- Gateway or edge enforcement: A gateway can apply shared policies before requests reach application instances. Confirm that its identity, keying, and policy capabilities match your requirements.
- Partitioned or approximate enforcement: Some high-scale designs distribute counters or tolerate small discrepancies to improve throughput. This trades exactness for scalability and should be an explicit decision.
When using a shared store, make the read-update decision atomic. Separate operations such as reading a count, checking it, and incrementing it can race under concurrent requests. Also consider what happens if the store is unavailable: failing open preserves availability but weakens protection; failing closed preserves the limit but can block legitimate traffic. Choose per endpoint, and monitor the chosen behavior.
Use expiry times so old counters do not accumulate indefinitely. Be deliberate about time sources, key cardinality, and store capacity. A hot key can itself become a bottleneck when many requests contend on one identity. Distributed enforcement is not automatically exact: network partitions, replication behavior, and independent regions can produce temporary differences depending on the datastore and design.
A practical rollout workflow
- Inventory endpoints. Group operations by cost, abuse risk, and expected legitimate usage.
- Select identities. Decide whether each policy is keyed by account, API key, IP, or a combination, and document the rationale.
- Set initial thresholds. Use existing traffic observations and product requirements where available. If usage is unknown, begin with a cautious, observable policy rather than an unexplained hard cap.
- Choose the algorithm. Define both the sustained rate and any permitted burst. Decide whether a boundary spike is acceptable.
- Enforce at a trusted server boundary. Verify that authentication and proxy-derived identity are handled before the policy uses them.
- Define rejection behavior. Return 429 for excess requests, include useful retry information when possible, and document how clients should behave.
- Test concurrency and deployment behavior. Test simultaneous requests, multiple application instances, restarts, and shared-store failures. Verify that the configured policy matches actual behavior.
- Monitor and adjust. Track allowed and rejected requests by endpoint and policy, along with store errors and latency. Avoid logging secrets or unnecessary personal data in limiter keys or diagnostic events.
Common mistakes and troubleshooting
Applying one limit to every endpoint
A uniform cap can be too restrictive for routine reads and too permissive for expensive actions. Separate policies by operation and cost.
Treating an IP address as a person
An IP-based policy can group unrelated users or miss a client that changes networks. Use it as one signal, and apply account-aware policies when a trustworthy authenticated identity exists.
Trusting arbitrary forwarded headers
If limits seem easy to evade by changing an address header, review trusted-proxy configuration and ensure untrusted clients cannot supply the effective identity. If every user appears to share one address, verify that the application extracts the client address through the trusted proxy chain correctly.
Counting non-atomically
Concurrent requests can pass a check before any of them increments the counter. Use an atomic operation or a limiter designed for concurrent access, and test it under parallel requests.
Retrying too aggressively
Clients that retry immediately after a 429 can keep themselves limited and increase load. Return actionable retry guidance and document backoff behavior.
Assuming local limits are global
If the observed allowance grows with the number of instances, counters may be isolated per process. Decide whether to use a shared limiter, gateway enforcement, or an explicitly per-instance policy.
Conclusion
Good API rate limiting starts with a clear protection goal, a suitable identity, and a policy matched to the endpoint's cost and traffic shape. Fixed windows, sliding windows, and token buckets make different trade-offs; none removes the need for trusted server-side enforcement, careful proxy configuration, and sound behavior across instances. Return clear 429 responses, test failure cases, and refine thresholds using operational evidence so the limits protect the service without needlessly blocking legitimate clients.
