Rate Limiting: Protecting Your API Without Punishing Good Users
A practical guide to API rate limiting for small teams: algorithms, headers, key choices, and how to fail politely instead of breaking clients.
Every API eventually meets a client that misbehaves. Sometimes it's an attacker probing your login endpoint. More often it's your own mobile app stuck in a retry loop, or a well-meaning integration partner who wrote a sync job with no delay between requests. Without rate limiting, one noisy client can degrade the service for everyone — and your database pays the bill. The good news: a useful rate limiter is a small amount of code, and the hard part is mostly deciding on policy.
Pick the right key, not just the right number
Before choosing an algorithm, decide what you're counting per. This matters more than the limit itself:
- Per API key or user ID — the right default for authenticated APIs. Fair, predictable, hard to evade.
- Per IP address — necessary for unauthenticated endpoints like login and signup, but be careful: corporate NATs and mobile carriers put thousands of legitimate users behind one IP.
- Per endpoint class — expensive operations (report generation, bulk export) deserve much tighter limits than cheap reads.
A common mistake is one global limit for everything. A client that can make 1,000 reads per minute should probably not be allowed 1,000 password reset emails per minute. Group endpoints by cost and sensitivity, and give each group its own budget.
Choose a boring algorithm
You don't need anything exotic. Two approaches cover almost every real case:
- Fixed window is trivial: count requests per key per minute in Redis with
INCRand an expiry. Its flaw is the boundary burst — a client can send a full quota at 11:59:59 and another at 12:00:00. - Token bucket (or sliding window) smooths that out: each key accrues tokens at a steady rate up to a cap, and each request spends one. Clients get sustained fairness plus room for short, legitimate bursts.
For most small teams, a token bucket in Redis — or simply the rate limiting built into your gateway or nginx — is more than enough. Resist building a distributed, perfectly consistent limiter. A limiter that's occasionally off by a few requests is fine; it's a safety valve, not a billing system.
Fail politely
How you reject a request matters as much as when. A good rate limiter:
- Returns 429 Too Many Requests, never a generic 500.
- Sends a Retry-After header so well-behaved clients know exactly how long to wait.
- Exposes remaining quota via headers like
X-RateLimit-Remaining, so integrators can throttle themselves before hitting the wall. - Uses a clear, machine-readable error body — clients should be able to distinguish "slow down" from "your request is broken."
A 429 with Retry-After turns a potential outage into a brief, self-correcting slowdown. A bare 500 turns it into a support ticket and an angry retry storm.
Watch it before you tighten it
Deploy your limiter in log-only mode first. Record who would have been throttled for a week, and you'll usually find surprises: an internal cron job, a dashboard polling too aggressively, a partner you forgot about. Fix or allowlist those, then enforce. Tightening limits on real traffic you've never measured is how you break paying customers on a Friday afternoon.
The takeaway
Rate limiting isn't about being hostile to users — it's about making sure one client's bad day doesn't become everyone's. Count per user, budget by endpoint cost, use a simple token bucket, return honest 429s with Retry-After, and observe before you enforce. It's an afternoon of work that buys you years of quieter incidents.
Build with Abati Technology
We build software that ships — WhatsApp API, developer tools, POS, and mobile apps. Let's talk about your project.
Get in Touch →