# Rate limit errors explain when to retry

- **Pattern:** `ab-002300` (`saas-error-handling.user-errors.rate-limit-retry-explain`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002300
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002300)

## 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:

```ts
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 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:

  ```ts
  return NextResponse.json(
    { error: 'Too many requests', retryAfter: 60 },
    { status: 429, headers: { 'Retry-After': '60' } }
  )
  ```

  On the client, detect 429s and show the timing:

  ```ts
  if (response.status === 429) {
    const { retryAfter } = await response.json()
    setError(`You've hit the rate limit. Please wait ${retryAfter} seconds before trying again.`)
  }
  ```

---

Taxons: user-experience

HTML version: https://auditbuffet.com/patterns/ab-002300
