Handbooks / REST API Design / Chapter 5

Rate Limiting and Idempotency

7 pages · ~5 min✓ Reviewed

Builds on Versioning and Pagination.

REST API DesignPage 1

Rate Limiting Mental Model

1. What It Does

  • Rate limiting caps requests per client per time window
  • Excess requests are rejected or delayed
  • Protects the server and enforces fairness

2. Key and Knobs

Limit keyShares one budget across
API keyone client app
User IDone account
IP addresseveryone behind one NAT
Routeone endpoint
  • Sustained rate: requests per second, long-run
  • Burst size: how many can arrive at once

3. Where It Runs

Server-sideClient-side
Protectsthe serviceyour own quota
Goalreject excessstay under provider limit
bash
INCR rl:user42:minute_28
EXPIRE rl:user42:minute_28 60

shared atomic counter in Redis (or one Lua script)

Per-node counters

3 nodes each allowing 100/min really allow 300/min. Share one atomic counter.

REST API DesignPage 2

Token Bucket, Fixed and Sliding Window

1. Token Bucket

  • Holds up to capacity tokens, refills at rate r per second
  • Each request spends 1 token
  • Empty bucket → reject
  • Bursts up to capacity are allowed
  • Refill is lazy: no timer thread
  • State per key: tokens, last_refill_time
python
tokens = min(capacity, tokens + elapsed * rate)
last_refill_time = now

run on each request

2. Fixed Window

  • Count per calendar window (e.g. per minute)
  • Reset counter at the boundary
  • Cheap: one counter per key
Boundary flaw

Full limit at the end of one window plus full limit at the start of the next gives up to 2x the limit in a short span.

3. Sliding Window

VariantStoresAccuracy
Logevery request timestampexact, memory-heavy
Countertwo window countersapproximate, cheap
  • Counter weights the previous window: current + previous * (1 - elapsed fraction)
REST API DesignPage 3

429 Too Many Requests and Retry-After

1. Status and Headers

  • 429 = standard rate-limit rejection; 503 = general overload
  • Neither is a payload bug: don't edit the request
  • RateLimit-* / X-RateLimit-*: informational, not universal; read docs
Retry-AfterForm
30delay-seconds
Wed, 21 Oct 2026 07:28:00 GMTHTTP-date

2. Client Rule

  1. 429
  2. Retry-After?
  3. Wait at least that
  4. Retry
  • Parse both forms; prefer it over your own backoff
python
from email.utils import parsedate_to_datetime as p
from datetime import datetime, timezone
def wait_s(v):
    if v.isdigit(): return int(v)
    d = p(v) - datetime.now(timezone.utc)
    return max(0, d.total_seconds())

3. Mistake and Interview Point

Tight retry loop

Retrying a 429 immediately worsens overload and can extend the ban.

  • Retry-After vs backoff? Retry-After wins; backoff is the fallback
  • 429 vs 503? Rate limit vs general overload
REST API DesignPage 4

Retries and Idempotency Keys

1. What to Retry

  • Retry transient failures: timeout, 429, 502, 503, 504
  • Never retry 4xx like 400, 401, 404: they fail again

2. Backoff and Jitter

  • Doubling: wait 1s, 2s, 4s, ...
  • Cap: a maximum wait per attempt
  • Max attempts: stop after N tries
  • Jitter: randomise the delay so clients do not retry in lockstep (retry storm)
python
import random, uuid
key = str(uuid.uuid4())  # once per operation
headers = {"Idempotency-Key": key}
wait = random.uniform(0, min(30, 2 ** n))

fragment: n = attempt number, cap 30s

3. Idempotency Keys

MethodEffect of a retry
GET, PUT, DELETEsafe: same state
POSTunsafe: duplicate, e.g. double charge
  1. ClientPOST + key
  2. Serverstores key + first result
  3. Repeatsame stored result
Key rule

Reuse the same key on every retry. A new key only for a new operation.

REST API DesignPage 5

Webhooks Basics

1. What a Webhook Is

  • An HTTP POST the provider sends to your registered URL
  • Event happens → provider pushes; you stop polling
  • Delivery is at-least-once and unordered

2. Handling an Event

  1. Verify HMACraw body
  2. Return 2xxfast
  3. Queue jobasync work
  4. Dedupeevent ID
  • Provider signs the raw bytes with a shared secret (HMAC-SHA256)
  • Never verify re-serialised JSON
  • Slow or failed response → provider retries
  • Store the event ID; skip seen ones to stay idempotent
python
import hmac, hashlib
mac = hmac.new(secret, raw_body,
               hashlib.sha256).hexdigest()
ok = hmac.compare_digest(mac, sig_header)

recompute, then compare in constant time

3. Common Mistake

Trusting the payload

Skipping the signature or timestamp check allows spoofed and replayed events.

REST API DesignPage 6

Rate Limiting and Retries Cheat Sheet

1. Algorithms

AlgorithmBehaviourCost
Token bucketbursts, steady average2 values
Fixed window2x boundary burst1 counter
Sliding countersmooth, approximate2 counters
Sliding logexactmemory per request

2. Reject and retry

  • Server: 429 + Retry-After (seconds or HTTP-date) + RateLimit-* headers
  • Retry timeouts, 429, 502, 503, 504; never other 4xx, 500 or 501
  • Honour Retry-After, else exponential backoff with cap and jitter
  1. Send
  2. Retryable?429, 502-504
  3. WaitRetry-After or backoff
  4. Attempts left?else give up

3. Idempotency and webhooks

  • One Idempotency-Key per logical POST, reused on every retry
  • Server returns the stored result for repeats
  • Webhook: verify HMAC on the raw body, return 2xx fast, queue work
  • Dedupe by event ID
Mistake

Instant 429 retries worsen overload; a new key per retry duplicates the POST.

  • Why jitter? Clients stop retrying in lockstep.
  • Why the raw body? Re-serialised JSON changes the signed bytes.
REST API DesignPage 7

Check yourself

Fixed window, limit 100/min. A client sends 100 requests at 12:00:59 and 100 more at 12:01:00. How many get through?
  • All 200: they land in two different windows.
  • That is 2x the limit in about a second, the boundary flaw.
A POST /charge times out. The client retries with a freshly generated Idempotency-Key each time. What goes wrong?
  • The server sees new keys, so it may charge twice.
  • Create the key once per operation and reuse it on every retry.
A webhook handler credits an account on every delivery. Event evt_7 arrives twice. What is the bug?
  • Delivery is at-least-once, so duplicates happen.
  • Store the event ID and skip ones already processed.