Rate limits
Stay inside the per-token request limits and back off correctly when you reach one.
The API limits how many requests a credential can make per minute. Limits are counted per deploy token, so two tokens on the same project each have their own budget.
The limits
| Requests | Limit | Counted per |
|---|---|---|
Every /v1 endpoint not listed below | 60 per minute | Deploy token |
POST /v1/push | 300 per minute | Deploy token, in its own budget |
POST /v1/batches/{batch}/cancel | 60 per minute | Deploy token, in its own budget |
GET /v1/cdn/{project}/{locale} and .../v{version} | 300 per minute | CDN key and client IP address |
Push and cancel draw on budgets of their own, so they never compete with the general one:
- A repository upload is one push per file, so a project with many locale files can upload them all without touching the budget your status checks use.
- Polling a batch never uses up the requests you need to cancel it.
Headers
Responses from a limited endpoint carry the current state of the limit:
| Header | On | Meaning |
|---|---|---|
X-RateLimit-Limit | Every response to an authenticated request | Requests allowed per minute |
X-RateLimit-Remaining | Every response to an authenticated request | Requests left in the current minute |
Retry-After | 429 only | Seconds to wait before the next request will be accepted |
X-RateLimit-Reset | 429 only | Unix time, in seconds, when the limit resets |
When you exceed a limit, the answer is 429 with the problem type /problems/rate-limited:
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1791820920
{"type":"/problems/rate-limited","title":"Rate limited","status":429,"detail":"Too many requests."}Not every 429 is a rate limit
/problems/quota-exceeded and /problems/extract-cap-reached are also 429, but they mean the account has used its AI allowance for the cycle. They carry no Retry-After and do not clear in seconds; resets_at in the body says when they lift. Branch on type before you retry. See Errors.
Backing off
Wait for Retry-After when it is present. When it is not (a 5xx, or a dropped connection), back off exponentially with jitter, and give up after a few attempts.
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export async function requestWithBackoff(url: string, init: RequestInit, attempts = 5): Promise<Response> {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, init);
const retryable =
res.status >= 500 ||
(res.status === 429 && (await res.clone().json()).type === '/problems/rate-limited');
if (!retryable || attempt === attempts - 1) return res;
const retryAfter = Number(res.headers.get('Retry-After'));
const delay = retryAfter > 0
? retryAfter * 1000
: Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 250;
await sleep(delay);
}
}Recent versions of curl honour Retry-After on a 429 when you ask them to retry:
curl --retry 4 --retry-max-time 120 \
https://api.transglot.ai/v1/batches/4822 \
-H "Authorization: Bearer $TRANSGLOT_TOKEN"Staying under the limits
- Poll with backoff. When you watch a batch, start at one second and double the interval up to about fifteen. A run takes minutes, not seconds.
- Use webhooks instead of polling. Subscribe an endpoint to
batch.completedandbatch.failedand you need no status requests at all. See Webhooks. - Read many values at once.
POST /v1/translations/batchreads up to 1,000 keys in one request whereGET /v1/translationsreads one. - Revalidate instead of downloading. Send the
ETagyou have asIf-None-MatchonGET /v1/pulland the CDN reads. A304is still a request, but it carries no body. - One token per pipeline. Each token has its own budget, so a busy CI pipeline does not slow down another job that uses a different token.
The command line tool already waits for Retry-After and backs off while it polls.