Rate limit errors (HTTP 429) handled gracefully
Why it matters
HTTP 429 (Too Many Requests) responses from third-party APIs — OpenAI, Stripe, SendGrid, Twilio — are not errors; they are instructions. An application that treats them as generic failures and retries immediately makes the rate-limit situation worse and may get its API key suspended. CWE-703 (improper check for exceptional conditions) applies. Users who trigger rate-limited operations see cryptic failures when they should see a clear message like 'You've sent too many requests. Please wait 60 seconds.' The Retry-After header provides the exact wait time — ignoring it is wasteful and user-hostile.
Severity rationale
Low severity for individual users, but repeated unhandled 429s can trigger API key suspension, causing a service-wide outage for all users simultaneously.
Remediation
Detect 429 responses explicitly, extract the Retry-After header, and surface a user-readable wait message. Do not auto-retry 429s without respecting the header delay.
// lib/api-client.ts
export async function apiFetch(url: string, options?: RequestInit) {
const res = await fetch(url, options)
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '60', 10)
throw new RateLimitError(
`Too many requests. Please wait ${retryAfter} seconds before trying again.`,
retryAfter
)
}
return res
}
In the UI, catch RateLimitError and show a countdown or disabled button with the remaining wait time rather than a generic error toast.
Detection
-
ID:
rate-limit-handling -
Severity:
low -
What to look for: Count all external API integrations. Enumerate which handle 429 (rate limit) responses with backoff vs. which crash or retry immediately. Check whether the application handles HTTP 429 (Too Many Requests) responses. Look for Retry-After header extraction and display of user-friendly messages.
-
Pass criteria: Rate limit errors (429) are caught and displayed to users with a friendly message; Retry-After header is extracted and used to show when they can retry. At least 80% of external API integrations must handle 429 responses with appropriate backoff.
-
Fail criteria: 429 responses are not handled; no Retry-After header extraction; user sees raw error.
-
Skip (N/A) when: The application makes no external API requests or is not rate-limited.
-
Cross-reference: For retry logic, see
retry-logic-backoff. For circuit breaker, seecircuit-breaker-pattern. -
Detail on fail:
"Rate limit responses not handled. User sees 'Too Many Requests' error"or"429 responses not distinguished from other errors" -
Remediation: Handle rate limits:
// lib/api-client.ts — rate limit handling if (res.status === 429) { const retryAfter = parseInt(res.headers.get('Retry-After') || '60'); await sleep(retryAfter * 1000); return retry() }async function fetchWithRateLimitHandling(url: string) { try { const response = await fetch(url) if (response.status === 429) { const retryAfter = response.headers.get('Retry-After') const seconds = retryAfter ? parseInt(retryAfter) : 60 throw new RateLimitError( `Too many requests. Please wait ${seconds} seconds before trying again.`, seconds ) } return response } catch (error) { if (error instanceof RateLimitError) { // Display to user with countdown toast.warning(error.message) } throw error } }
External references
- cwe · CWE-703 — Improper Check or Handling of Exceptional Conditions
- iso-25010:2011 · reliability.fault-tolerance
Taxons
History
- 2026-04-18·v1.0.0·Initial import from error-resilience·automated