← All postsEngineering

Rate limiting: protecting an API without punishing good clients

A rate limit exists to keep one caller from degrading the service for everyone. The design question is not the number — it is what happens at the boundary, and whether a client can tell.

EngineeringR

Rate limiting caps how many requests a client may make in a period. Its purpose is protective rather than commercial: one badly written integration in a retry loop can consume the capacity of a shared service, and the limit is what stops that becoming everybody's outage.

The interesting design decisions are not about the number. They are about what the client is told, how the boundary behaves, and whether a well-behaved integration can stay under the limit without guessing.

The algorithms, briefly

  • Fixed window: count requests per clock minute. Simple, and it allows a double burst across the boundary — a client can spend its whole allowance at 10:59:59 and again at 11:00:00.
  • Sliding window: count over the trailing period. Smoother, slightly more expensive to track, and the usual right answer.
  • Token bucket: tokens refill at a steady rate and requests consume them. Allows a controlled burst, which suits real traffic patterns better than a flat cap.
  • Leaky bucket: requests drain at a fixed rate and queue or spill. Good where downstream capacity is genuinely constant.
  • For most APIs, token bucket or sliding window. Fixed window is chosen for its simplicity and then produces the boundary bursts it was meant to prevent.

What the client must be told

  1. Return 429 for a rate-limited request, not 403 and certainly not 200 with an error inside.
  2. Include a Retry-After header saying how long to wait. Without it, every client invents its own guess and they are all wrong.
  3. Publish remaining quota and reset time in response headers, so a well-behaved client can slow down before hitting the wall rather than after.
  4. Document the limits in the reference, next to the endpoints they apply to, as covered in api documentation.
  5. Make the limit per credential rather than per IP where you can. Shared IPs punish innocent clients, and IP-based limits are trivially worked around by the callers you actually wanted to stop.

The most common client-side failure is retrying immediately after a 429, which turns a temporary limit into a self-inflicted outage. Exponential backoff with jitter is the correct response, and jitter matters more than people expect — without it, every client that was throttled at the same moment retries at the same moment, and the recovery attempt becomes the next spike.

Choosing the numbers

  • Start from capacity, not from a round figure: what can the service sustain, and what share should a single client be able to consume?
  • Set different limits for different operations. A cheap read and an expensive report should not share a budget.
  • Allow a burst above the sustained rate. Real integrations are bursty, and a flat cap fails legitimate usage that a bucket would absorb.
  • Watch who is being limited before tightening anything. Usually it is one integration with a bug, and a conversation fixes it better than a lower limit.
  • Log limit events with the credential, so that both you and the customer can see what happened.

Where it fits

Ettex API applies limits per credential and returns the standard signals described above; the retry behaviour expected of a client is covered in api integration, and the same backoff discipline applies to webhook delivery.

The honest boundary: current limits belong in the reference, not in an article, because they change with capacity. Nothing here is a commitment about specific numbers.

Frequently asked

What status code should a rate-limited request return?

429, with a Retry-After header. Using 403 conflates authorisation with throttling, and returning 200 with an error inside breaks generic clients.

Which rate limiting algorithm should you use?

Token bucket or sliding window for most APIs. Fixed window is simplest and allows a double burst across the window boundary, which is the problem it was meant to solve.

Should limits be per IP or per API key?

Per credential wherever possible. Shared IP addresses punish innocent clients, and IP limits are easy to evade for anyone deliberately abusing the service.

How should a client handle a 429?

Respect Retry-After, then exponential backoff with jitter. Immediate retries turn a temporary limit into an outage, and unjittered retries synchronise every throttled client into the next spike.

AS
Written by Alex S.

Part of the Ettex team — writing about product, engineering and the future of work.

More posts
Get the best of the Ettex blogProduct news, guides and tips — straight to your inbox, no spam.