# Retries and Backoff

> Retry only 429 and 5xx responses with exponential backoff and jitter, honor Retry-After, and leave validation errors alone.

Canonical: https://currency-core.com/docs/guides/retries/
Updated: 2026-11-11
Published: 2026-11-11

---
A retry loop that treats every failure the same will burn your per-minute budget and delay the real fix. Retrying a malformed request returns the same `400`, and retrying a billing `402` never clears the payment state. The recommendation is narrow: retry `429` and `5xx` with capped exponential backoff and jitter, and fail fast on everything else.

| Response | Retry? | Action |
|---|---|---|
| `429 rate_limited` with `Retry-After` | Yes | Wait the stated seconds, then retry with jitter |
| `429` without `Retry-After` | Bounded | Retry at most a few times, then inspect quota or credits |
| `500`, `502`, `503` | Yes | Back off exponentially and cap attempts |
| `400`, `401`, `402`, `403`, `404`, `413`, `422` | No | Fix the request, key, plan, or billing state |

A time-based `429` is a pause, not a permanent failure, so `Retry-After` is the strongest signal available. The API does not publish quota headers beyond that value, so do not infer a remaining budget from response headers. A `429` that persists after the stated delay usually means the monthly quota, hard cap, or AI credit balance is exhausted. Escalate instead of extending the loop.

The per-minute limit counts every request, including retries, while the monthly quota counts successful billable requests. That makes a tight retry loop expensive even when the calls keep failing.

```ts
type ApiFailure = Error & { status?: number; retryAfter?: number };

async function getJson<T>(url: URL, key: string): Promise<T> {
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${key}` },
    signal: AbortSignal.timeout(20_000),
  });
  if (res.ok) return (await res.json()) as T;

  const seconds = Number(res.headers.get("Retry-After"));
  const error = new Error(`HTTP ${res.status}`) as ApiFailure;
  error.status = res.status;
  error.retryAfter = Number.isFinite(seconds) ? seconds : undefined;
  throw error;
}

async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      const e = err as ApiFailure;
      const retryable = e.status === undefined || e.status === 429 || e.status >= 500;
      if (!retryable || i === attempts - 1) throw err;
      const waitMs = e.retryAfter ? e.retryAfter * 1000 : 500 * 2 ** i;
      await new Promise((resolve) => setTimeout(resolve, waitMs + Math.random() * 250));
    }
  }
  throw new Error("unreachable");
}
```

Every data endpoint is a `GET`, so repeating a request is naturally idempotent. If a retry can trigger a side effect in your own system, use an idempotency key that your application owns; the API has no idempotency-key endpoint.

## Related

- [OpenAPI specification](/docs/openapi/)
- [Authentication](/docs/authentication/)
- [Errors](/docs/errors/)
- [Rate limits](/docs/rate-limits/)
- [Guides](/docs/guides/)
