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.
Low severity for individual users, but repeated unhandled 429s can trigger API key suspension, causing a service-wide outage for all users simultaneously.
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.
ID: error-resilience.graceful-degradation-shutdown.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, see circuit-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
}
}