> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alterscope.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Retries and Backoff

> Client-side reliability patterns for the Alterscope API: exponential backoff with jitter, capped attempts, timeouts, and a per-status retry-safety matrix.

Networks fail, limiters trip, and upstreams hiccup. A resilient client retries the failures that are worth retrying — and only those. This page is standard, vendor-neutral guidance for building that client against the Alterscope API. It does not replace the per-code reference in [Errors](/develop/get-started/errors); it tells you what to *do* with each code.

## Retry only what's safe to retry

Branch on the HTTP status and the [error envelope](/develop/get-started/errors)'s `code`, never on the human-readable `message`.

| Status                      | Retry?         | What to do                                                                                                                   |
| --------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `429` Too Many Requests     | Yes            | Honor `Retry-After` (seconds), then back off. See [Rate limits](/develop/get-started/rate-limits).                           |
| `409` idempotency in-flight | Yes            | A request with the same `X-Idempotency-Key` is still running. Honor `Retry-After`, then retry the **same** key.              |
| `503` / `504` / `5xx`       | Yes            | Transient server or gateway condition. Back off with jitter and a cap.                                                       |
| Network error / timeout     | Yes            | Connection reset, DNS failure, read timeout. Treat as transient and back off.                                                |
| `400`, `401`, `403`, `404`  | No — fix first | Malformed request, bad/expired/revoked key, missing scope, wrong path. Retrying the identical call won't change the outcome. |
| Other `4xx` validation      | No — fix first | Correct the request body or parameters, then send once.                                                                      |

<Note>
  None of these cases call for a **new** idempotency key. A retry should reuse the same key so the server can deduplicate it — see [Idempotency keys are safe to reuse](#idempotency-keys-are-safe-to-reuse) below.
</Note>

For `401`/`403`, the fix is in your key or scopes — see [Authentication](/develop/get-started/authentication) and [Scopes](/develop/get-started/scopes). For `429`, see [Rate limits](/develop/get-started/rate-limits).

## Honor `Retry-After` when present

Two responses carry a `Retry-After` header (in seconds), and you should wait at least that long before retrying:

* **`429 Too Many Requests`** — the per-minute limiter. The response also carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`.
* **`409` idempotency in-flight** — a concurrent request with the same `X-Idempotency-Key` is still executing. Wait the suggested interval, then retry the same key.

When `Retry-After` is present, treat it as a floor: wait at least that long, then apply your normal backoff for any subsequent attempts.

## Exponential backoff with jitter

For transient failures without a `Retry-After`, back off exponentially and add **jitter** so that many clients retrying after the same outage don't synchronize into a thundering herd. Cap both the per-attempt delay and the total number of attempts.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const RETRYABLE = new Set([429, 503, 504]);

  async function withRetry<T>(
    fn: () => Promise<Response>,
    maxAttempts = 5,
    baseMs = 200,
    capMs = 20_000,
  ): Promise<Response> {
    let attempt = 0;
    for (;;) {
      let res: Response;
      try {
        res = await fn();
      } catch (err) {
        // Network error / timeout — retryable.
        if (++attempt >= maxAttempts) throw err;
        await sleep(backoff(attempt, baseMs, capMs));
        continue;
      }

      if (res.ok || (res.status >= 400 && res.status < 500 && !RETRYABLE.has(res.status))) {
        // Success, or a client error you must fix — don't retry.
        return res;
      }

      if (++attempt >= maxAttempts) return res;

      // Honor Retry-After (seconds) when the server sends it.
      const retryAfter = res.headers.get("Retry-After");
      const waitMs = retryAfter
        ? Math.max(Number(retryAfter) * 1000, backoff(attempt, baseMs, capMs))
        : backoff(attempt, baseMs, capMs);
      await sleep(waitMs);
    }
  }

  function backoff(attempt: number, baseMs: number, capMs: number): number {
    const exp = Math.min(capMs, baseMs * 2 ** (attempt - 1));
    return Math.random() * exp; // full jitter
  }

  const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
  ```

  ```python Python theme={null}
  import random
  import time

  RETRYABLE = {429, 503, 504}

  def with_retry(do_request, max_attempts=5, base_s=0.2, cap_s=20.0):
      attempt = 0
      while True:
          try:
              res = do_request()  # returns an object with .status_code and .headers
          except (ConnectionError, TimeoutError):
              attempt += 1
              if attempt >= max_attempts:
                  raise
              time.sleep(_backoff(attempt, base_s, cap_s))
              continue

          if res.status_code < 500 and res.status_code not in RETRYABLE:
              # Success, or a client error you must fix — don't retry.
              return res

          attempt += 1
          if attempt >= max_attempts:
              return res

          retry_after = res.headers.get("Retry-After")
          wait_s = _backoff(attempt, base_s, cap_s)
          if retry_after:
              wait_s = max(float(retry_after), wait_s)
          time.sleep(wait_s)

  def _backoff(attempt, base_s, cap_s):
      exp = min(cap_s, base_s * (2 ** (attempt - 1)))
      return random.random() * exp  # full jitter
  ```
</CodeGroup>

The official SDKs ([TypeScript `@alterscope/sdk`, Python `alterscope`](/develop/sdks/overview)) apply backoff for you; reach for the patterns above when you're calling the API directly or generating a client from the OpenAPI spec.

## Set sensible timeouts

A retry loop is only as good as the timeouts under it. Without them, a single stalled connection blocks the whole loop.

* **Per-attempt timeout** — bound each request so a hung connection fails fast and frees the slot for a retry rather than hanging indefinitely.
* **Total deadline** — cap the wall-clock time across all attempts. Once the deadline passes, stop retrying and surface the error.
* **Connect vs. read** — a short connect timeout catches unreachable hosts quickly; a longer read timeout accommodates heavier analytical endpoints.

Tune the read timeout to the endpoint: a webhook subscription returns in milliseconds, while a portfolio analysis or Monte-Carlo run is heavier. Keep a generous total deadline so a brief outage doesn't fail a request you'd otherwise recover.

## Idempotency keys are safe to reuse

Mutation endpoints accept an `X-Idempotency-Key` header. When you retry, **send the same key** — that's what makes the retry safe:

* A replay of a completed request returns the original cached response (within a 24-hour window) with an `X-Idempotency-Replayed: true` header, instead of executing the side effect again.
* If the original request is still in flight, the retry receives `409` with `Retry-After`; wait and retry the same key.

A retry never needs a **new** key. Mint a new `X-Idempotency-Key` only for a genuinely new operation, never to "force through" a failed one — a new key bypasses deduplication and can double-execute the side effect. See [Idempotency](/develop/get-started/idempotency) for the full contract.

<Warning>
  Many mutation endpoints touch live, money-moving operations. When in doubt, retry with the **same** idempotency key and let the server deduplicate. Never generate a fresh key to retry a request that may have already taken effect.
</Warning>

## Checklist

* Retry `429`, `409` in-flight, `503`/`504`/`5xx`, and network/timeout errors. Don't retry `400`/`401`/`403`/`404` or other validation failures — fix the request first.
* Honor `Retry-After` (seconds) on `429` and `409` before backing off.
* Use exponential backoff with full jitter, a per-attempt delay cap, and a capped attempt count.
* Set per-attempt timeouts and a total deadline.
* Retry with the **same** `X-Idempotency-Key`; never mint a new one to force a retry.

## Related

<CardGroup cols={2}>
  <Card title="Idempotency" icon="key" href="/develop/get-started/idempotency">
    Make mutations safe to retry with `X-Idempotency-Key`.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/develop/get-started/errors">
    The error envelope, status codes, and per-code retry guidance.
  </Card>

  <Card title="Rate limits" icon="gauge-high" href="/develop/get-started/rate-limits">
    Per-tier limits and `429` retry behavior.
  </Card>

  <Card title="Authentication" icon="lock" href="/develop/get-started/authentication">
    Fix `401`/`403` failures at the key and scope level.
  </Card>
</CardGroup>
