HTTP APIs · beginner

API 429 burst limit: map caller rate to the provider's response window

When an HTTP API returns 429, the failure is almost never "the API is down" — it is a burst-window rejection. This guide shows how to map the caller's actual request emission rate onto the provider's short-term rate policy so that the 429s stop being mysterious. The argument: a 429 only makes sense once you can read the provider's window, the requester's rhythm, and the gap between them.

The symptoms

  • Clients receive HTTP 429 Too Many Requests responses clustered around predictable wall-clock intervals rather than spread evenly across time.
  • Workloads that look modest on a per-second average still trip the limit, because emissions are bursty (cron ticks, retry storms, parallel fan-out, queue flushes).
  • Endpoints that are nominally "read" or "metadata" return 429 under load, even though the provider documents the path as cheap or uncounted.
  • Retry-after or rate-limit hints are ignored or treated as advisory, so the same caller keeps producing new 429s each window.
  • 429s appear only on a subset of endpoints, regions, or API keys, indicating the cap is per-resource, not global.

Likely causes

  • The caller's peak burst rate exceeds the provider's short-window cap, even though the long-window average stays under any documented quota.
  • Multiple workers, replicas, or browser tabs share one API key or token and collectively burst past a per-credential ceiling.
  • A retry loop re-emits the failed request faster than the rate-limit window resets, compounding the burst instead of relieving it.
  • The provider counts a "request" differently from the caller: sub-requests, prefetches, secondary API calls, or websocket frames may all be billed toward the same window.
  • Clock drift between the caller and the provider's window edge causes synchronized emissions to land on the worst possible 100 ms or 1 s slice.
  • A caching layer or client SDK transparently retries on idempotent reads, silently inflating the real emission rate above what the application code emits.

First ten minutes

  1. 01Confirm the status code is exactly 429 and capture the response headers verbatim, including any RateLimit-* / Retry-After / X-RateLimit-* headers the provider sends.
  2. 02Record the timestamp of each 429 with sub-second precision so you can see whether rejections cluster at window edges or spread evenly.
  3. 03Identify the provider's documented rate-limit window (per-second, per-minute, per-IP, per-token) from its API reference, not from memory.
  4. 04Count the caller's emissions in the same window the provider documents, separating reads from writes and per-endpoint paths if the cap is scoped that way.
  5. 05Decide whether the 429s come from a single caller, a fleet sharing one credential, or a fan-out trigger such as a cron, retry, or queue drain.
  6. 06Check whether the client is honoring the Retry-After hint, ignoring it, or retrying faster than the window resets.

Evidence to collect

  • Full 429 response including all RateLimit, X-RateLimit, Retry-After, and any provider-specific quota headers, with timestamps.
  • Server-side access logs filtered to the affected credential, region, or endpoint, showing request arrival times relative to the window.
  • Application-side emit timestamps for the same window, ideally from a metrics counter that distinguishes 2xx, 429, and 5xx outcomes.
  • Provider documentation excerpts naming the exact window length, the resource the cap is keyed on, and how partial windows are counted.
  • Inventory of all callers, SDKs, sidecar proxies, and retry layers that can emit requests under the same identity.
  • Clock-skew evidence between caller and provider, since aligned ticks make bursts look larger than they are.

Where to look

  • At the provider boundary: response headers on the 429 itself, plus the API reference page that defines the rate-limit policy and its window.
  • At the client SDK boundary: default retry policy, idempotency handling, and any silent request fan-out (prefetch, list pagination, conditional GETs).
  • At the application boundary: emit-side counters per endpoint and per credential, not aggregate request volume, since the cap is rarely global.
  • At the orchestration boundary: cron schedules, queue consumer concurrency, worker replica counts, and any "drain the backlog" code path that fans out under pressure.
  • At the network boundary: connection reuse vs. new connections, HTTP/2 stream multiplexing, and any proxy or API gateway that buffers or replays requests.

Diagnostic steps

  1. 01Plot the 429 timestamps on a timeline and mark the provider's documented window boundaries; if 429s cluster at the start of each window, the caller is bursting past the cap each cycle.
  2. 02Compute peak emissions in the smallest window the provider documents (e.g., requests per 1 s, 10 s, or 60 s) and compare against the stated ceiling, not the long-term average.
  3. 03Bucket the 429s by credential, region, and endpoint to determine whether the cap is per-key, per-route, or per-IP, since the fix differs for each scope.
  4. 04Diff the response headers of the 429 against a successful 200 response from the same endpoint to see which counters the provider exposes (remaining, reset, policy).
  5. 05Replay a synthetic, paced request stream from a controlled caller and observe whether 429s appear at the same wall-clock points, which isolates provider behavior from application behavior.
  6. 06Inspect the client retry path: confirm whether retries are exponential, jittered, and gated on the Retry-After value, or whether they re-fire inside the same window.
  7. 07Check for hidden emitters: health checks, readiness probes, metrics scrapes, and SDK prefetches often count toward the same window as user-driven calls.

Common mistakes

  • Trusting a per-minute average and ignoring per-second peaks, which is exactly what a burst-limit policy is designed to catch.
  • Treating Retry-After as a suggestion and retrying immediately, which lands the retry inside the same window the provider just closed.
  • Sharing one API key across many replicas or workers and reasoning about per-instance rate, while the provider caps the credential.
  • Assuming all endpoints share one budget when the provider scopes the cap per-endpoint or per-method, so "cheap" calls still get rejected.
  • Reading 429 as a server health problem and paging on-call, when the fix is pacing the client, not restarting the service.
  • Silently downgrading to a slower path on 429 without recording the trigger, which masks the real burst and prevents a proper pacing fix.

Safe fixes

  • If the provider exposes RateLimit-* headers, have the client honor them as the source of truth for remaining budget and reset time before emitting the next request.
  • Introduce a token-bucket or leaky-bucket scheduler on the emit side, sized to the provider's documented per-window cap, and cap concurrency so the bucket cannot be overdrawn in parallel.
  • Spread scheduled work (cron, queue drain, backfill) with jitter so emissions do not align on the provider's window boundary; aim for sub-window granularity, not just sub-minute.
  • Shard the workload across additional credentials, regions, or tenants when the provider caps per-identity, rather than retrying harder on the existing one.
  • Replace immediate retries with exponential backoff plus jitter, gated on the Retry-After value, and bound total retries so a partial outage does not turn into a sustained burst.
  • Disable transparent SDK prefetch, speculative retries, or silent list-paging fan-out when those paths are not strictly necessary for the user-visible behavior.

Prove the fix

  1. 01429 rate on the affected endpoint and credential drops to zero across at least one full provider window, with emit-side pacing metrics showing peak rate stays under the documented cap.
  2. 02RateLimit-* headers from the provider indicate non-zero remaining budget during the same window the workload is running, confirming the client is reading the window correctly.
  3. 03Retry-After is honored: a synthetic retry storm respects the header and the second attempt does not produce another 429 inside the same window.
  4. 04Burst test: a controlled fan-out at the previous peak rate no longer produces 429s, while a deliberately higher rate still does, proving the cap is the boundary and the fix is the limiter.
  5. 05Jitter check: scheduled jobs no longer align their emits on the provider's window edge, visible as a flatter emit histogram rather than synchronized spikes.

Prevention and next steps

  • Encode the provider's documented rate-limit window as a first-class configuration value, not as a comment, and validate emit code against it in CI where possible.
  • Export per-credential and per-endpoint emit counters so a burst shows up before users see 429s, and alert on a budget burn rate rather than only on raw 429 counts.
  • Default client retries to jittered exponential backoff bounded by the window length, and require an explicit override for tighter loops.
  • Review any new fan-out path (cron, queue, backfill, prefetch) for its worst-case peak rate before deploy, and cap concurrency at the call site.
  • Periodically re-read the provider's rate-limit documentation, since window length, scope, and counter semantics change without a major version bump.

Safe commands and checks

awk '$9==429{print $4}' /var/log/app/access.log | head -n 50 # show timestamps of recent 429 responses from a caller-side access log
grep -E 'RateLimit-|X-RateLimit-|Retry-After' /var/log/app/access.log | head -n 20 # extract rate-limit response headers from the access log to compare against provider docs
awk '$9==429{print $7}' /var/log/app/access.log | sort | uniq -c | sort -rn | head -n 20 # count 429s per endpoint path to identify which routes the cap actually applies to
awk '$9==429{print $11}' /var/log/app/access.log | sort | uniq -c | sort -rn | head -n 20 # count 429s per credential or API key id, placeholder <credential-field> is the logged token alias