Limites de taxa
Como funcionam os limites de requisições da API da Axuz, por token pessoal axp_, por mutação em equipe e por IP, quais headers acompanham as respostas e como reagir com backoff exponencial e jitter.
A API limita requisições para proteger o serviço de abuso e de laços descontrolados. Há mais de um limite, em camadas diferentes, e cada um responde 429 Too Many Requests com um cabeçalho Retry-After. Esta página descreve os que o código implementa hoje.
Limite por token pessoal (axp_)
Cada token pessoal tem o seu próprio teto por minuto, definido na criação (rate_limit_per_min, de 1 a 1000, padrão 60). Ele não pode ser alterado depois: para mudar, revogue e crie outro token (veja o guia inicial). O teto vale para o token, não para a conta: dois tokens do mesmo usuário têm baldes separados.
O contador é uma janela fixa por minuto de relógio: o balde da requisição é o minuto UTC corrente (chave token + minuto), e zera na virada do minuto. Por isso você pode usar o teto inteiro nos últimos segundos de um minuto e de novo logo depois da virada.
O limite se aplica apenas a quem chega por token. Requisições do navegador com sessão (cookie) não passam por esse balde.
Headers
Quando o limitador está ativo, toda resposta de uma chamada com token, permitida ou negada, traz:
| Header | Significado |
|---|---|
X-RateLimit-Limit | O teto por minuto do token (por exemplo 60). |
X-RateLimit-Remaining | Quantas chamadas ainda cabem neste minuto (nunca negativo). |
X-RateLimit-Reset | Segundo Unix (época, em segundos) em que o próximo minuto começa e o balde zera. |
Retry-After | Só no 429: segundos até a próxima janela, entre 1 e 60. |
O 429 traz também um corpo JSON:
{ "error": "rate_limited", "limit": 60, "count": 61, "retry_after": 12 }
count é o valor do contador depois desta requisição; pode passar de limit, pois requisições negadas também contam no minuto.
Fail-open ou fail-closed
O que acontece se o Redis estiver fora do ar na hora da requisição depende de uma configuração do servidor (RATE_LIMIT_FAIL_CLOSED):
- fail-open (padrão do código): a requisição passa e o servidor registra um aviso. Você perde o teto por um instante, nunca a disponibilidade;
- fail-closed: a requisição é recusada com
503e{"error": "rate_limiter_unavailable"}, comRetry-After: 60.
O descritor de implantação da frota liga o fail-closed por padrão, então em ambientes hospedados trate o 503 com Retry-After como “tente de novo daqui a pouco”, exatamente como um 429.
Limite de mutações por equipe
As rotas de equipe do serviço de workspace (/teams/{teamId}/...) limitam escritas: POST, PATCH, PUT e DELETE. GET não conta. O balde é por par (usuário, equipe), com padrão de 60 mutações por minuto, e é um token bucket que reabastece continuamente (60 por minuto = 1 por segundo), em vez de uma janela fixa. A resposta 429 é:
{ "error": "rate_limited", "retry_after": 3 }
com Retry-After (segundos, arredondado para cima, mínimo 1) e X-RateLimit-Limit com a capacidade do balde. Não há X-RateLimit-Remaining nem Reset nesse limite. O mesmo ajuste fail-open/fail-closed existe (MUTATION_RATE_LIMIT_FAIL_CLOSED), com 503 rate_limiter_unavailable no segundo caso.
Na prática: importações em massa, scripts que criam centenas de tarefas e testes de carga esbarram nesse limite antes de qualquer outro. Espaçe as escritas (cerca de uma por segundo por equipe) em vez de contar com rajadas.
Outros limites que você pode encontrar
- Login e tentativas de autenticação. O serviço de autenticação limita tentativas de login, verificação de e-mail e emissão de códigos, e bloqueia IPs abusivos. A resposta é
429comRetry-After; no bloqueio por IP o corpo é{"error": "ip_blocked", "retry_after_seconds": N}. Os números são ajustáveis e podem mudar; respeite sempre oRetry-After. - Endpoints OAuth (
/oauth/token,/oauth/revoke,/oauth/introspect):429comRetry-Aftereerror: "rate_limited"no formato de erro do OAuth. - Assistente de IA (
POST /me/ai/chat): limite por usuário;429comRetry-After. - Envio de arquivos: há teto por identidade e um teto folgado por IP, contra inundação, no serviço de arquivos.
- Convites: limite por minuto (padrão 20) no envio de convites de equipe.
- Chaves de equipe (
axt_..., parte das integrações, em andamento): têm o mesmo desenho do token pessoal, comX-RateLimit-*eRetry-After. O contrato público dessa superfície ainda está sendo concluído.
Como reagir
- Respeite
Retry-After. É o sinal mais confiável. Espere esse tempo (mais um pequeno acréscimo aleatório) antes de repetir. - Sem
Retry-After, use backoff exponencial com jitter. Base de 0,5 s, dobrando até um teto (por exemplo 30 s), e sorteie o tempo real entre 0 e o valor calculado (full jitter). O jitter evita que vários clientes acordem juntos e batam de novo no limite. - Desacelere antes de bater. Se
X-RateLimit-Remainingchegar a0, durma atéX-RateLimit-Resetem vez de provocar o429. - Limite as tentativas. Pare depois de algumas repetições (por exemplo 5) e devolva o erro ao chamador.
- Só repita o que é seguro.
GET,PUTeDELETEpodem repetir; umPOSTque cria algo só comIdempotency-Keyou depois de reconciliar o estado. Veja Erros. - Não paralelize para “compensar”. Mais conexões consomem o mesmo balde mais rápido.
Node
// node 20+, ESM. fetch com repetição segura para 429/503.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function axuzFetch(url, init = {}, { retries = 5 } = {}) {
const headers = { Authorization: `Bearer ${process.env.AXUZ_TOKEN}`, ...init.headers };
const method = (init.method ?? 'GET').toUpperCase();
const safe = ['GET', 'HEAD', 'PUT', 'DELETE'].includes(method) || headers['Idempotency-Key'];
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { ...init, headers });
// Desacelera antes de bater no teto.
const remaining = Number(res.headers.get('x-ratelimit-remaining'));
const reset = Number(res.headers.get('x-ratelimit-reset'));
if (res.ok && res.headers.has('x-ratelimit-remaining') && remaining === 0 && reset) {
await sleep(Math.max(0, reset * 1000 - Date.now()) + 50);
}
const retryable = res.status === 429 || res.status === 503;
if (!retryable || !safe || attempt >= retries) return res;
const retryAfter = Number(res.headers.get('retry-after'));
const waitMs = retryAfter > 0
? retryAfter * 1000 + Math.random() * 500 // respeita o servidor
: Math.random() * Math.min(30_000, 500 * 2 ** attempt); // full jitter
await sleep(waitMs);
}
}
const res = await axuzFetch(`${process.env.AXUZ_API}/me/summary`);
console.log(res.status);
Python
import os
import random
import time
import urllib.error
import urllib.request
def axuz_get(url, retries=5):
"""GET com repetição para 429/503: respeita Retry-After, senão full jitter."""
headers = {"Authorization": "Bearer " + os.environ["AXUZ_TOKEN"]}
for attempt in range(retries + 1):
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req, timeout=30) as res:
remaining = res.headers.get("X-RateLimit-Remaining")
reset = res.headers.get("X-RateLimit-Reset")
if remaining == "0" and reset:
time.sleep(max(0.0, int(reset) - time.time()) + 0.05)
return res.read()
except urllib.error.HTTPError as e:
if e.code not in (429, 503) or attempt == retries:
raise
retry_after = e.headers.get("Retry-After")
if retry_after and retry_after.isdigit() and int(retry_after) > 0:
wait = int(retry_after) + random.uniform(0, 0.5)
else:
wait = random.uniform(0, min(30.0, 0.5 * 2**attempt))
time.sleep(wait)
print(axuz_get(os.environ["AXUZ_API"] + "/me/summary")[:200])
O que não fazer
- Não rode vários processos com o mesmo token esperando multiplicar a vazão: o teto é do token.
- Não ignore
Retry-Afterpara repetir em laço apertado: cada tentativa negada ainda conta no balde. - Não trate
429como erro fatal em jobs longos: é um sinal normal de controle de fluxo. - Não crie tokens extras só para fugir do teto. Se o uso legítimo precisa de mais de 1000 requisições por minuto, o desenho (lote e agregação, como
GET /me/summary, em vez de uma chamada por item) é o caminho.