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.
Low because rate limiting still protects the backend even without timing information; only the user experience degrades.
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.
ID: saas-error-handling.user-errors.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 a Retry-After header or a retryAfter field 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-After header 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.`)
}