Rate limits
Requests are metered with distributed token buckets. Several apply at once, and the strictest one wins.
| Bucket | Applies to | Quota |
|---|---|---|
client | Every data-plane request, per credential | 120/min, adjustable per credential |
company | Every data-plane request, across all credentials of the company | 600/min |
transcript | Transcript reads only, on top of the two above | 60/min |
oauth-client | POST /oauth/token, per client id | 10/min, burst 5 |
oauth-ip | POST /oauth/token, per source IP, and GET /oauth/authorize | 30/min, burst 10 |
oauth-grant | POST /oauth/token with grant_type=authorization_code or refresh_token, per connection | 10/min, burst 5 |
oauth-revoke | POST /oauth/revoke, per connection | 10/min, burst 5 |
A company may hold up to 20 active company credentials; user credentials do not count toward that
limit, and every credential of the company draws from the same company quota. Call GET /v1/context to read the credential quota actually
in effect for yours.
The oauth-grant bucket belongs to one connection made by signing in with OAuth, so people behind the same
address never share it, and such a grant is not charged to oauth-ip. A grant that does not match any
connection, for example an unknown code, is charged to oauth-ip instead. The metadata documents
under /.well-known/ have no rate limit.
POST /oauth/revoke is charged the same way. A token that is still valid for a connection, whether its
refresh token or one of its access tokens, is charged to that connection’s oauth-revoke bucket; any
other token is charged to oauth-ip.
Headers
Section titled “Headers”Every response carries the current state. RateLimit-Policy lists every bucket
the request consumed; RateLimit reports the single one closest to exhaustion:
RateLimit-Policy: "client";q=120;w=60, "company";q=600;w=60RateLimit: "client";r=118;t=1q is the quota, w the window in seconds, r what is left and t the seconds
until another token. RateLimit names whichever bucket is holding the fewest
tokens: normally your credential’s, the transcript one on transcript reads, and
the company one once sibling credentials are spending it. Pace against the
ceilings in RateLimit-Policy — the bucket named in one reading is not the only
one binding you.
r counts tokens in the bucket, whose capacity is the burst rather than the
quota, so it does not start at q and count down. Treat it as room available
right now.
A rejected request returns 429 RATE_LIMIT_EXCEEDED with Retry-After in
seconds. Wait that long — do not retry immediately, and add jitter so a fleet of
workers does not resynchronise into the next window.
Behaviour when the limiter is degraded
Section titled “Behaviour when the limiter is degraded”Read paths fail open: if the limiter is briefly unreachable, business reads keep serving rather than going dark.
Token issuance, transcript reads and internal control-plane routes fail
closed with 503 SERVICE_UNAVAILABLE. These are the paths where serving
unmetered traffic is worse than serving none.
The WAF limit is separate
Section titled “The WAF limit is separate”A coarse per-IP limit runs at the WAF, before traffic reaches the service, and it
is not part of the quotas above: roughly 2000 requests per five minutes per
source IP, and 100 per five minutes on /oauth/token. Grants authorization_code and
refresh_token do not count in the WAF per-IP limit; the service limits them with the oauth-grant and oauth-ip buckets described above. GET /oauth/authorize
and POST /oauth/revoke stay subject to the general per-IP limit.
It fails differently, which matters when you meet it:
- it blocks with
403, not429, so it reads as an authorization failure; - there is no error code, no
Retry-Afterand no rate limit headers, because the request never reaches the service; - it counts by source IP, so every integration behind the same egress address shares it, whatever credentials they use.
A 403 with none of our error body, especially on /oauth/token, is this limit
rather than a missing scope.
Staying under the limit
Section titled “Staying under the limit”- Page with
limit=100instead of many small pages — one request returns up to 100 records at the same cost as one returning 10. - Filter by period rather than walking the whole history repeatedly.
- Cache the access token; every
/oauth/tokencall consumes its own bucket. - Fetch transcripts only for records you will actually process.
has_transcriptlets you filter the list first.