jsonapirestrate-limiting

Rate Limiting APIs: 429, Retry-After & JSON Errors

·9 min read·APIs

Why rate limit

Without limits, one buggy client or one bad actor can overwhelm your API, degrading it for everyone. Rate limiting caps how many requests a client can make in a window, protecting your infrastructure and ensuring fair use. Done well, it is also a good developer experience — clients know exactly when they are throttled and when they can retry.

The 429 status code

When a client exceeds its limit, return HTTP 429 Too Many Requests with a clear JSON body:

json
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Retry after 30 seconds.",
    "retry_after": 30
  }
}

A machine-readable code lets clients branch on the error type, and including retry_after in the body (not just the header) helps clients that do not read headers.

Retry-After and rate-limit headers

The standard way to tell clients when to retry is the `Retry-After` header (seconds, or an HTTP date):

text
HTTP/1.1 429 Too Many Requests
Retry-After: 30

Even better, expose the client's budget on every response so they can self-pace before hitting the wall:

text
RateLimit-Limit: 1000
RateLimit-Remaining: 4
RateLimit-Reset: 1781100600

A well-behaved client watches RateLimit-Remaining and slows down before it ever sees a 429.

Common algorithms

AlgorithmIdeaTrade-off
Fixed windowN requests per clock windowSimple; bursts at window edges
Sliding windowN requests over a rolling periodSmoother; a bit more state
Token bucketTokens refill over time; each request spends oneAllows controlled bursts

Token bucket is the most popular for public APIs because it permits short bursts while enforcing a steady average rate.

What to rate limit on

  • API key / user — the usual unit for authenticated APIs.
  • IP address — for anonymous/public endpoints (with care behind proxies).
  • Endpoint cost — charge expensive operations more tokens than cheap ones.

Document your limits clearly so clients can design around them rather than discover them by getting blocked.

Client-side: respect the limit

A good client does not hammer through a 429:

javascript
async function callWithBackoff(fn, attempt = 0) {
  const res = await fn();
  if (res.status === 429 && attempt < 5) {
    const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
    await new Promise(r => setTimeout(r, wait * 1000));
    return callWithBackoff(fn, attempt + 1);
  }
  return res;
}

Honor Retry-After, add exponential backoff with jitter, and cap retries — that turns throttling from an error into a brief pause.

Frequently asked questions

HTTP 429 Too Many Requests, ideally with a Retry-After header and a JSON body containing a machine-readable error code and retry time.

Retry-After tells a throttled client when to retry. RateLimit-* headers expose the remaining budget on normal responses so clients can slow down before being throttled.

Token bucket is a strong default for public APIs — it enforces an average rate while allowing short bursts. Fixed window is simplest; sliding window is smoother but holds more state.

Try JSON Formatter

Format and inspect your 429 error response bodies.