Rate limit errors explain when to retry
Why it matters
A 429 response with no Retry-After header and no client-side timing message forces users to guess when to try again, which produces two bad outcomes: they either give up, or they hammer retry and trigger a retry storm that extends the rate-limit window. RFC 6585 defines Retry-After precisely so clients and humans can coordinate backoff. Without it, the rate limiter is invisible to the user and indistinguishable from a 500, erasing the distinction between throttling and an outage.
Severity rationale
Low because rate limiting still protects the backend even without timing information; only the user experience degrades.
Remediation
Return Retry-After on every 429 response and surface the value on the client so users see a concrete wait time instead of a generic error. Match the header value to your limiter's window. Edit the rate-limit handler in middleware.ts or your API route:
return NextResponse.json(
{ error: 'Too many requests', retryAfter: 60 },
{ status: 429, headers: { 'Retry-After': '60' } }
)
On the client, branch on response.status === 429 and display Please wait ${retryAfter} seconds rather than the generic error component.
Detection
-
ID:
rate-limit-retry-explain -
Severity:
low -
What to look for: Check if the project implements rate limiting (look for rate limiting middleware, libraries like
@upstash/ratelimit,express-rate-limit, or custom Redis-based rate limiting). If rate limiting is implemented, examine the 429 response and the client-side handling: (1) Does the 429 response include aRetry-Afterheader or aretryAfterfield in the response body? (2) Does the client display a message telling the user when they can try again? A message like "You've made too many requests. Please wait 30 seconds." is ideal. -
Pass criteria: List all rate-limited endpoints and examine each for Retry-After headers. Pass if rate limiting is implemented AND either: the 429 response includes a
Retry-Afterheader or body field, OR the client displays a message explaining the rate limit and when to retry. Report even on pass: "X rate-limited endpoints found, Y include Retry-After information." -
Fail criteria: Fail if rate limiting returns 429 with a generic "Too many requests" message with no timing information. Fail if the client shows the same error UI for 429s as for 500s (indistinguishable to the user). At least 1 rate-limited endpoint must include Retry-After information.
-
Skip (N/A) when: The project does not implement rate limiting. Signal: no rate limiting middleware or library in
package.json, no rate limit logic in API routes. Note: the absence of rate limiting itself is a concern addressed in the SaaS API Design Audit. -
Detail on fail:
"Rate limiting returns 429 but response body is {error: 'Too many requests'} with no Retry-After header; client shows generic error message". Max 500 chars. -
Remediation: Rate limit errors without timing information force users to guess how long to wait. A specific wait time reduces frustration and prevents retry storms.
In your rate limit response:
return NextResponse.json( { error: 'Too many requests', retryAfter: 60 }, { status: 429, headers: { 'Retry-After': '60' } } )On the client, detect 429s and show the timing:
if (response.status === 429) { const { retryAfter } = await response.json() setError(`You've hit the rate limit. Please wait ${retryAfter} seconds before trying again.`) }
Taxons
History
- 2026-04-18·v1.0.0·Initial import from saas-error-handling·automated