Rate limiting exists to protect a service from being overwhelmed — by a runaway client, a bug in someone's retry loop, or genuine abuse — and to keep one noisy consumer from degrading the API for everyone else. The algorithm matters less than most discussions make it sound; what matters more in practice is communicating limits clearly enough that well-behaved clients can back off before they get throttled, instead of learning about the limit from a wall of 429s.
Token bucket: the common default
A token bucket holds a maximum number of tokens, refills at a fixed rate, and each request consumes one token; when the bucket is empty, requests are rejected until it refills. This allows short bursts up to the bucket's capacity while still enforcing a steady average rate over time — a client that's been idle can burst 100 requests instantly if the bucket is full, then has to wait for tokens to trickle back in. That burst tolerance is usually what you want: real client traffic is bursty, not a perfectly even trickle, and a limiter that punishes normal bursts frustrates legitimate use without stopping actual abuse.
HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 23
RateLimit-Reset: 41
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 12
Retry-After: 12
Sliding window: smoother enforcement, more state to track
A fixed window (100 requests per minute, clock-aligned) has a boundary problem: a client can send 100 requests in the last second of one window and another 100 in the first second of the next, bursting 200 requests in two seconds while technically staying within the stated per-minute limit. A sliding window — counting requests in a continuously moving window rather than a clock-aligned bucket — closes that gap, at the cost of more state to track (a log of recent request timestamps, or a weighted combination of the current and previous fixed windows as a cheaper approximation). Whether the gap is worth closing depends on how much a two-window burst actually threatens your backend; for many APIs it doesn't, and the simpler fixed-window or token-bucket approach is enough.
Standard rate-limit headers are worth adopting as-is
The IETF draft standard (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) plus the long-standing Retry-After header give clients everything they need to self-throttle: how many requests they have left, when the window resets, and exactly how long to wait after being throttled. Emitting these on every response, not just on 429s, lets a well-implemented client proactively slow down as RateLimit-Remaining approaches zero, rather than discovering the limit only after getting rejected.
IP-based limiting punishes every user behind a shared NAT or corporate proxy equally, and does nothing to stop a single authenticated user hitting your API from many IPs. Limiting per API key or per authenticated user identity, with IP-based limiting as a secondary, coarser layer against unauthenticated abuse, is usually the more accurate model.
Per-endpoint limits for disproportionately expensive operations
A single global rate limit treats a cheap cached GET the same as an expensive search or export endpoint that hits several downstream services per call — which means the limit has to be set low enough to protect the expensive endpoint, throttling the cheap ones far more than necessary, or set high enough for the cheap endpoints that the expensive ones can still cause damage. Per-endpoint or per-operation-cost limits (weighting expensive calls as multiple "requests" against the budget) solve this more precisely, at the cost of a slightly more complex limiter configuration.
A login endpoint needs a much stricter, IP-and-account-aware limit specifically to slow down credential-stuffing and brute-force attempts — a generic per-API-key limit doesn't help here because the attacker doesn't have a valid key yet. Treat auth endpoints as a distinct rate-limiting policy, not a special case of the general one.
Wrapping up
Token bucket is the right default for most APIs — simple, burst-tolerant, cheap to implement; sliding window closes a real but often minor boundary gap at higher implementation cost. Whichever algorithm you pick, emit the standard rate-limit headers on every response so well-behaved clients can self-throttle, and rate-limit by identity rather than IP wherever an authenticated identity is available.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.