## Published limits

Each of the three `/v1` endpoints is metered against one of two buckets:

| Endpoint | Limit |
|---|---|
| [`GET /v1/catalog`](/reference), [`GET /v1/coverage`](/reference) | 120 requests/min per team |
| [`GET /v1/sample`](/reference) | 30 requests/min per team |

`/v1/sample` carries its own, lower ceiling — it's for judging shape, not
for bulk reads, and the bulk reads belong on `/v1/catalog`. These are per
key, not per team — two keys on the same team each get their own window.
Limits are published, not discovered: you should never need to hit a 429 to
find out what your ceiling is.

## The headers

Every `/v1` response carries three headers, whether it succeeds or fails on
rate limits:

```http
RateLimit-Limit: 120
RateLimit-Remaining: 87
RateLimit-Reset: 42
```

| Header | Meaning |
|---|---|
| `RateLimit-Limit` | The ceiling for this endpoint group, this minute |
| `RateLimit-Remaining` | Requests left in the current window |
| `RateLimit-Reset` | Seconds until the window resets |

## Self-throttle before you're throttled

Watch `RateLimit-Remaining` on every response. When it gets low, sleep for
`RateLimit-Reset` seconds rather than waiting for a 429:

```python
if int(resp.headers["RateLimit-Remaining"]) < 5:
    time.sleep(int(resp.headers["RateLimit-Reset"]))
```

That keeps a busy integration under the ceiling without ever seeing a failed
request.

## When you do hit 429

[`rate_limited`](/errors#rate_limited) is [`retryable: true`](/docs/error-handling). The response carries `Retry-After` —
honor it. It reflects what the server actually needs, which will always beat
a backoff schedule you invent yourself:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 42
```

## The pre-auth floor

Failed [authentication](/docs/authentication) attempts are also
rate-limited, per source address, independent of any key. This isn't a
catalog limit — it exists so that a flood of made-up keys can't buy unlimited
lookups against the auth store. If you're scripting retries against
[`invalid_credentials`](/errors#invalid_credentials), fix the credential;
don't loop on it.

Limits are published, not discovered.
