Rate limits
Per-principal budgets by endpoint group, and how 429 asks you to slow down.
Limits are enforced after authentication, keyed by the verified identity — an attacker with a bad token consumes nothing, and principals never compete with each other. Exceeding a budget answers 429 RATE_LIMITED with a Retry-After header holding the seconds until the next slot.
The budgets
| Group | Applies to | Default | Key |
|---|---|---|---|
| resolve | POST /internal/v1/credentials/resolve |
100 r/s, burst 200 | service client_id |
| user read | all user-plane GETs (catalog, credentials, status) | 120/min | token sub |
| user mutations | user-plane POST/PATCH/DELETE on credentials | 30/min | token sub |
| admin | /v1/admin/* and /internal/v1/admin/* |
60/min | sub, plus the X-Admin-Actor label on the service plane |
| reveal | POST .../credentials/{id}/reveal |
10/min | sub + operator label |
| health | /health/* |
unlimited | — |
Numbers are deployment configuration (a limit of 0 disables its group); the values above are the defaults and are quoted per endpoint in the ReDoc reference.
Three details worth knowing:
- Personal and organization credentials share one budget. The key is your
sub, so mutations on/v1/credentialsand/v1/org-credentialsdraw from the same 30/min. - Admin-panel operators do not throttle each other. On the internal admin plane the key includes the
X-Admin-Actorlabel, so each named operator gets their own 60/min. - Reveal is deliberately scarce. Ten reveals a minute per operator is a human inspecting keys, not a script walking the store — that is the point (Reveal).
Handling 429
import time
import httpx
def call_with_backoff(request):
while True:
response = request()
if response.status_code != 429:
return response
time.sleep(int(response.headers.get("Retry-After", "1")))
Budgets are per replica and refill continuously; a single respected Retry-After is normally enough.