← Stackzilla Blog

Rate Limiting That Protects Your App Without Punishing Normal Users

Published October 27, 2026 · 3 min read · API design, security, infrastructure, backend

Choose sensible rate-limit keys, handle bursts, and give clients useful responses when they reach a limit.

A public endpoint can be overwhelmed by malicious traffic, an enthusiastic integration, or a perfectly ordinary client with a retry bug. Rate limiting sets a boundary on how much work one caller can request, but a poorly chosen boundary can block legitimate people. Start by naming the resource you are protecting. Login attempts, expensive reports, file uploads, and simple cached reads should not necessarily share the same policy. Their costs and abuse risks differ. ## Choose identity carefully For authenticated APIs, an account or API key is often more useful than an IP address. A single office or mobile network can put many legitimate users behind one address. Conversely, an attacker can distribute requests across many addresses. Layer controls where appropriate: a broad network-level limit, a per-account allowance, and stricter rules for sensitive operations. Trust forwarded IP headers only from known proxies. Otherwise a caller may supply a fake address and bypass your policy. Multi-tenant systems should consider fairness between accounts. One large customer should not accidentally consume all shared capacity before smaller customers can make ordinary requests. ## Allow bursts without allowing unlimited work A fixed window is easy to understand but can allow a large burst across the boundary between windows. Sliding windows or token buckets offer different tradeoffs in precision, state, and burst tolerance. A token bucket can allow short bursts while enforcing a sustained rate. That can fit browser behavior, where loading one screen triggers several requests together. Expensive jobs may also require concurrency limits: a caller can stay under a request-rate allowance while keeping many long-running jobs active. Shared enforcement needs atomic updates when several application instances handle requests. A separate in-memory counter on every server does not provide a global allowance. Decide explicitly what happens if the shared limiter becomes unavailable. ## Explain rejection and observe its effects Use an appropriate `429 Too Many Requests` response and communicate when a client may try again. Return a stable error shape that application developers can handle. Do not expose internal implementation details or security thresholds unnecessarily. Test with realistic usage patterns before tightening limits. Look at rejected requests by endpoint and account type, and inspect whether normal workflows are affected. Password recovery and accessibility-related repeated actions deserve particular care. Rate limiting is one layer, not complete denial-of-service protection. Request-size caps, query bounds, connection limits, and upstream protection may still be necessary. The useful question is not how many requests sound reasonable. It is how much work the system can safely admit while preserving a predictable experience for other users. ## Further reading - [MDN: HTTP 429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) - [Redis: Rate limiting](https://redis.io/glossary/rate-limiting/)

Read the full article on Stackzilla →