# Rate limits, quotas & error handling

> Every API key is throttled by a three-tier rate limiter and a monthly quota. When you push past either, the API answers with a structured error and headers that tell you exactly how long to wait. This page covers the tiers, the headers to read, the error taxonomy, and how to back off.

HTML version: https://api.nyne.ai/documentation/rate-limits

## The three-tier rate limiter

Requests are counted **per API key** across three windows at once. All three are enforced together, and the **first tier you exceed wins** - a fast burst can trip the burst tier even while you are well under the hourly cap. The caps scale from the key's `rate_limit_per_minute` and `rate_limit_per_hour` (defaults 100/min and 1,000/hour).

| Tier | Window | Default cap | Derived from |
| --- | --- | --- | --- |
| Burst | 10-second window | 49 requests / 10s | int(perMinute x 10/60 x 3) |
| Sustained | 5-minute rolling window | 500 requests / 5min | perMinute x 5 |
| Hourly | 1-hour window | 1,000 requests / hour | perHour |

The burst cap is about 3x your per-minute rate spread across a 10-second slice, then floored: at the default 100/min the float math (100 x 10/60 x 3 = 49.9...) truncates down to `49`, not 50. If you fan out requests in parallel, pace them: hitting the burst tier returns `429` even when your minute and hour budgets are untouched.

## The monthly quota

On top of the per-second and per-hour rate limits, each key has a **monthly request quota** - a ceiling on how many requests it can make in a billing month. The rate limiter smooths traffic over short windows; the quota caps total volume over the month. You can exhaust the quota without ever tripping a rate tier, and when you do, requests fail with `monthly_limit_exceeded` until the quota resets or you raise it.

The quota counts *requests*; credits meter *results*. A key can have quota left but be out of credits, or the reverse. See the [Credits guide](https://api.nyne.ai/documentation/credits.md).

## Response headers to read

Every response carries the live rate-limit and quota counters. Read them proactively - warn or slow down as `Remaining` approaches zero, rather than waiting for the first `429`. Note the two `Reset` conventions: `X-RateLimit-Reset` is an **absolute Unix timestamp**, while `RateLimit-Reset` is **relative seconds from now**.

| Header | Meaning |
| --- | --- |
| X-RateLimit-Limit | Requests allowed in the current window. |
| X-RateLimit-Remaining | Requests left in the current window. |
| X-RateLimit-Reset | When the window resets, as an absolute Unix timestamp. |
| RateLimit-Limit | Same limit, standard-header spelling. |
| RateLimit-Remaining | Same remaining count, standard-header spelling. |
| RateLimit-Reset | Seconds from now until the window resets (relative). |
| Retry-After | Seconds to wait before retrying. Present only after you have been throttled. |
| X-Quota-Limit | Total requests allowed this month. |
| X-Quota-Used | Requests already spent this month. |
| X-Quota-Remaining | Requests left before the monthly quota is exhausted. |
| X-Quota-Reset | When the monthly quota rolls over. |

`Retry-After` appears only when you have been throttled, and it is the server telling you exactly how many seconds to wait. When it is present, use it verbatim - it beats any backoff you compute yourself.

## Error taxonomy

Throttling and access failures use the same uniform envelope as every other reply: `success` is `false`, the detail moves under `error`, and a `timestamp` is always present. Branch on the stable `error.code`, never the human-readable `message`:

```json
{
  "success": false,
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Retry after the period in the Retry-After header."
  },
  "timestamp": "2026-05-28T18:04:45"
}
```

Each `error.code` maps to an HTTP status, and tells you whether to retry or to fix something first:

| error.code | HTTP | Handling | What to do |
| --- | --- | --- | --- |
| rate_limit_exceeded | 429 | Retryable | Back off and retry; honor Retry-After when present. |
| monthly_limit_exceeded | 429 / 403 | Needs action | Monthly quota exhausted. Wait for the reset or raise the quota. |
| insufficient_credits | 402 | Needs action | Top up credits before retrying. |
| missing_credentials | 401 | Needs action | Send the X-API-Key and X-API-Secret headers. |
| invalid_credentials | 401 | Needs action | Fix or reissue the key and secret. |
| subscription_inactive | 403 | Needs action | Reactivate the subscription behind the key. |
| ip_not_allowed | 403 | Needs action | Allow-list the IP your backend calls from. |
| access_denied | 403 | Needs action | Grant the key access to this product. |
| service_unavailable | 503 | Retryable | Transient outage. Back off and retry the same request. |

Only `429` and `503` are safe to retry unchanged. `monthly_limit_exceeded` can surface as `429` or, on some paths, `403` - either way it means the quota is spent, not that a short wait will help. For the credentials and access codes (`401`/`403`) see the [Authentication guide](https://api.nyne.ai/documentation/authentication.md); for `insufficient_credits` see [Credits](https://api.nyne.ai/documentation/credits.md).

## Recommended client behavior

Build one retry policy and apply it everywhere:

- Retry **only** `429` and `503`, with **exponential backoff** (1s, 2s, 4s, ...) and a capped number of attempts.
- **Always honor `Retry-After`** when it is present - use its value instead of your computed delay.
- **Surface the quota headers**: warn before you hit the wall using `X-RateLimit-Remaining` and `X-Quota-Remaining`.
- Do **not** retry `402`, `401`, or `403` - they need an action (credits, credentials, or access), not another call.

```bash
# Retry 429/503 with backoff; honor Retry-After when the server sends it.
attempt=0
while :; do
  code=$(curl -sS -D headers.txt -o body.json -w '%{http_code}' \
    "https://api.nyne.ai/person/enrichment?request_id=$RID" \
    -H "X-API-Key: $KEY" -H "X-API-Secret: $SECRET")

  case "$code" in
    2*) break ;;                      # done
    429|503)
      retry=$(grep -i '^Retry-After:' headers.txt | tr -d '\r' | awk '{print $2}')
      # Prefer Retry-After; otherwise exponential backoff (1s, 2s, 4s, ...).
      sleep "${retry:-$(( 2 ** attempt ))}"
      attempt=$(( attempt + 1 )) ;;
    401|402|403) echo "not retryable - fix creds/credits/access"; break ;;
    *) echo "unexpected $code"; break ;;
  esac
done
```

Every poll for an async result is a request, so the same tiers and quota apply while you wait. Space your polls (roughly once a second) and honor `Retry-After` there too. See [Async requests](https://api.nyne.ai/documentation/async.md).

## What's next

- [Credits](https://api.nyne.ai/documentation/credits.md) - how usage is metered and when a request consumes a credit.
- [Authentication](https://api.nyne.ai/documentation/authentication.md) - keys, secrets, IP-locking, and the auth error codes.
- [Async requests](https://api.nyne.ai/documentation/async.md) - the submit-and-poll lifecycle that must respect these limits.
- [Responses](https://api.nyne.ai/documentation/responses.md) - the uniform envelope every reply shares.
