# Rate limits

# Rate limits

| Endpoint | Limit | Partitioned by |
|---|---|---|
| `POST /api/v1/oauth/token` | 20 requests / minute | per `client_id` **and** per caller IP (whichever is hit first) |
| every other `/api/v1/*` call | 60 requests / minute by default (may be raised per club) | per client only — never per IP |

Exceeding either is `429 RATE_LIMIT_EXCEEDED`, with `Retry-After: 60` on both. `Retry-After` is always an
integer number of seconds (never an HTTP date). There is no burst allowance beyond the stated rate.

```json
{ "type": "https://sailracing.club/docs/errors#RATE_LIMIT_EXCEEDED", "status": 429,
  "code": "RATE_LIMIT_EXCEEDED", "traceId": "…" }
```

A separate case, [`503 API_UNAVAILABLE`](/docs/errors#API_UNAVAILABLE) (the read model didn't answer — nothing
to do with your call rate), carries a shorter `Retry-After: 30` — same integer-seconds format, different
number because it's a different, usually brief problem. Both `429` and `503` are handled the same way: wait
exactly `Retry-After` seconds, then retry.

## Staying under the limit

You almost never need 60 calls/min for one club's data — results are cached for up to an hour at our end
([Caching & freshness](/docs/caching)), so polling faster than your `Cache-Control: max-age` just spends
your rate-limit budget on 304s (which still count against the limit). A sensible integration:

- Poll each endpoint you use **once**, on your own schedule (e.g. every 5–15 minutes), not per page view.
- Serve your own visitors from your own cache/database, not from a live call per request.
- If you operate many clients (an agency serving several clubs), give each club its own credential rather
  than fanning one client's limit out across all of them.

## What backs off automatically

The token you hold does not need refreshing more than once an hour, so a correct
[refresh-before-expiry](/docs/authentication#refresh-before-expiry-pattern) implementation naturally stays
far under the token endpoint's 20/min limit even under load.
