> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.morphiaone.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.morphiaone.ai/_mcp/server.

# Retries and backoff

## Retry these

```text
a transport failure or timeout with no response at all
408, 429
500, 502, 503, 504
409 IDEMPOTENCY_IN_PROGRESS
```

## Do not retry these unchanged

```text
400  the request is malformed
401  the key is missing, wrong or no longer usable
402  there are no credits
403  the key lacks this operation's scope
404  it is not there
409  IDEMPOTENCY_KEY_REUSED  -- you changed the request under an existing key
429  QUOTA_EXCEEDED  -- the daily quota resets on its own clock, not after Retry-After
410  the input asset expired  -- upload again, then submit under a new key
413  the upload is too large
415  the upload is not a supported image type
422  the image cannot be used
```

Retrying any of those repeats a request that is wrong, at cost, until a quota stops
you. `IDEMPOTENCY_KEY_REUSED` is the one most likely to be retried by accident: it
looks like a conflict that might clear, and it never will, because the conflict is
between two things you sent.

Every error response also says so itself: `error.retryable` is `true` exactly for
the codes above that may be retried, and `error.doc_url` links the code's row in the
[error reference](/api-reference/errors). Both are fields of the JSON error envelope,
so they work over plain HTTP today.

The SDKs expose them on `ApiError` as `retryable` and `doc_url` (Python) or `docUrl`
(TypeScript).

## Keep the idempotency key across retries

The retried call must carry the **same** `Idempotency-Key` as the attempt that
failed. A retry with a fresh key is a second submission, and it will be analysed and
billed. See [Idempotency](/concepts/idempotency).

## Honour Retry-After, then back off

When `Retry-After` is present, use it. It is not a suggestion — it is the server
telling you when it will be able to answer, and ignoring it converts a brief
saturation into a sustained one.

A `429` can also carry `RateLimit-*` headers naming each limit and when it resets; see
[Rate limits](/concepts/rate-limits).

When it is absent, use capped exponential backoff with jitter:

```python
import random, time

def backoff(attempt, cap=30.0, base=0.5):
    window = min(cap, base * 2 ** attempt)
    return random.uniform(0, window)          # full jitter
```

The jitter matters more than the exponent. Without it, every client that failed at
the same moment retries at the same moment, and the retry storm is the outage.

## Two deadlines, not one

```python
PER_REQUEST_TIMEOUT = 10      # seconds on a single HTTP call
OPERATION_DEADLINE  = 300     # seconds for the whole submit-poll-fetch sequence
```

A per-request timeout alone gives you an unbounded loop of individually quick calls.
An overall deadline alone lets one hung socket consume the whole budget. You want
both, and you want the overall deadline to be shorter than whatever is waiting on
you upstream.

When the overall deadline expires, stop waiting — but remember that
**stopping does not cancel the job**. Persist the job id, return control, and
pick the result up later. The analysis will finish.

## Polling retries keep the ETag

A retried poll should still send `If-None-Match` with the last `ETag` you held. A
`304` is cheaper for both sides, and the retry does not invalidate what you cached.

## What a correct loop looks like

```text
submit with a stable Idempotency-Key
  |-- 2xx  -> persist the job id
  |-- 429/503/5xx/transport -> wait (Retry-After or backoff), retry with the same key
  \-- 4xx (other) -> stop, report error.code

poll, waiting Retry-After between attempts, sending If-None-Match
  |-- 304 -> keep waiting
  |-- 200 queued|running -> keep waiting
  |-- 200 succeeded -> fetch the result assets before expires_at
  |-- 200 failed -> handle job.error.code, do not resubmit blindly
  \-- 503 -> keep waiting; this is not expiry

until the operation deadline, then persist and resume later
```