---
description: The error hierarchy and retries.
---

# Errors and retries

## Errors

Every exception the SDK raises descends from `belfort_ml.BelfortMLError`.

```mermaid
flowchart LR
    B[BelfortMLError] --> Cfg["ConfigurationError<br/>no API key, or a base URL that is not http(s)"]
    B --> Cmp["CompileError<br/>local or Belfort-side compile failure"]
    B --> Up["UploadError<br/>an upload part failed after retries"]
    B --> Dn["DownloadError<br/>reading compiled artifacts failed"]
    B --> Km["KeyMaterialError<br/>a key directory holds a file that is not an evaluation key"]
    B --> To["TimeoutError<br/>a wait ran past its timeout"]
    B --> Cn["ConnectionError<br/>the gateway was unreachable after retries"]
    B --> Api["APIError<br/>the gateway returned an error"]
    Api --> V["ValidationError<br/>400, 413"]
    Api --> Au["AuthenticationError<br/>401"]
    Api --> Ic["InsufficientCredits<br/>402, with .required and .available"]
    Api --> Pe["PermissionError<br/>403"]
    Api --> Nf["NotFoundError<br/>404"]
    Api --> Co["ConflictError<br/>409"]
    Co --> Dl["DeployLimitReached<br/>409 deploy_limit_reached, with .limit and .running"]
    Api --> Rl["RateLimitError<br/>429, with .retry_after"]
    Api --> Su["ServiceUnavailable<br/>503"]
```

`APIError` carries `message`, `code`, `detail`, `status_code` and `request_id`.
Include the `request_id` when you report a problem to Belfort.

```python
try:
    deploy = model.deploy(key_set=minted, max_lifetime=timedelta(hours=2))
except belfort_ml.InsufficientCredits as e:
    print(f"need {e.required} credits, have {e.available}")
except belfort_ml.APIError as e:
    print(e.code, e.message, e.request_id)
```

Common `ConflictError` codes:

| Code | Cause |
|---|---|
| `name_taken` | Creating a model with a name your account already uses, or `models.get(name=, create_from=)` with a different MLIR than that model's |
| `deploy_running` | Deleting a model while one of its deploys is starting or ready |
| `key_set_not_ready` | Loading a key set that has not been verified |
| `key_set_unusable` | Starting a deploy for a key set whose keys failed verification or expired |
| `key_loaded` | Deleting a key set that a running deploy serves |
| `key_set_mismatch` | Loading another key set than the one the deploy serves |
| `keys_already_loaded` | Loading the deploy's key set while it holds or loads it |
| `deploy_not_ready` | Using a deploy that is still starting or already stopping |
| `deploy_limit_reached` | Starting a deploy while the account's one deploy is starting or ready; raised as `DeployLimitReached` |

## Retries

The SDK retries connection errors, timeouts, and 429, 500, 502 and 504
responses up to `max_retries` times (3 by default), with exponential backoff and
jitter capped at 30 seconds. A 429's `Retry-After` takes precedence over the
computed backoff. Other 4xx responses are not retried. A read or write timeout
can come after the gateway acted on the request, so after one only GET
requests are sent again; a `POST /deploys` that timed out raises
`ConnectionError` instead of starting a second deploy.

Inference is never retried automatically, since a retry would run on the GPU
again. To retry one yourself, pass the same `request_id` to `deploy.infer`
within five minutes: the gateway returns the stored result instead of running
it again. Choose the `request_id` yourself on the first call, since a call that
fails does not return the one `deploy.infer` generated.

`model.create_client()` is not retried either: each call opens a key set, so a
retry after a lost answer would leave a key set whose token nobody holds. If
it fails, call it again; an unused key set expires with its token.

## Capacity

A deploy's runtime handles one inference at a time; concurrent requests wait
for it. A request waits up to 30 seconds for the runtime, then fails with 429
and `retry_after: 30`. Each inference gets 300 seconds of GPU time.
