Rate limits
Comprendre les limites de débit de l'API publique, lire les headers de réponse et implémenter une logique de retry robuste.
L'API publique applique un rate limiting Bucket4j fixed-window par tenant (ou par token PAT pour l'API publique), pour protéger la plateforme contre les pics de trafic, abus et boucles de retry mal câblées.
Limite par défaut
| Plan | Quota | Burst | Capacité totale |
|---|---|---|---|
| Free / Pro / Enterprise | 120 req/min | +60 | 180 req / fenêtre 60 s |
La capacité totale (180 = 120 sustained + 60 burst) absorbe les pics courts. Au-delà, l'API retourne 429 Too Many Requests avec les headers de retry.
Les overrides par endpoint (ex. : @RateLimited(perMinute=10) pour un endpoint coûteux) sont sur la roadmap. À ce jour, la limite globale s'applique uniformément à tous les endpoints /api/**.
Headers de réponse
Chaque réponse — succès ou 429 — porte trois headers :
| Header | Type | Description |
|---|---|---|
X-RateLimit-Limit | int | Capacité maximale par fenêtre (par défaut 180). |
X-RateLimit-Remaining | int | Tokens restants dans la fenêtre courante. |
X-RateLimit-Reset | int (epoch s UTC) | Instant exact où la fenêtre se réinitialise. |
En cas de 429, deux headers supplémentaires :
| Header | Type | Description |
|---|---|---|
Retry-After | int (secondes) | Délai avant de pouvoir réessayer (= X-RateLimit-Reset − maintenant). |
Content-Type | application/json | Body : {"error":"rate_limit_exceeded"} |
Exemple : réponse en cours de fenêtre
HTTP/1.1 200 OK
X-RateLimit-Limit: 180
X-RateLimit-Remaining: 142
X-RateLimit-Reset: 1750801260
Content-Type: application/json
...Exemple : réponse 429
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 180
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1750801283
Content-Type: application/json
{"error":"rate_limit_exceeded"}Implémenter un retry robuste
Pattern recommandé : exponential backoff + respect du Retry-After
async function callWithRetry<T>(
fn: () => Promise<Response>,
maxAttempts = 5,
): Promise<T> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const res = await fn();
if (res.status !== 429) {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// Respecter Retry-After si présent, sinon backoff exponentiel.
const retryAfter = Number(res.headers.get("Retry-After")) || 2 ** attempt;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
}
throw new Error("Rate limit retries exhausted");
}Pattern recommandé : pacing proactif via X-RateLimit-Reset
Pour éviter complètement les 429 sur un job batch, lisez X-RateLimit-Remaining à chaque réponse. Quand il tombe sous un seuil (ex. 10 %), suspendez les appels jusqu'à X-RateLimit-Reset :
import time, requests
response = requests.get(url, headers=auth_headers)
remaining = int(response.headers.get("X-RateLimit-Remaining", "180"))
reset_epoch = int(response.headers.get("X-RateLimit-Reset", "0"))
if remaining < 20:
sleep_for = max(0, reset_epoch - time.time())
time.sleep(sleep_for)Bonnes pratiques
- Cache local agressif sur les endpoints lecture (
/applications,/functions) — leur contenu change peu. ETags HTTP supportés sur tous les GET du référentiel. - Pagination, pas tout-en-un :
?size=100&page=Nplutôt que tirer 5 000 éléments d'un coup. - Webhooks plutôt que polling (v0.1.77.1) — la majorité des cas d'usage qui polling sur le référentiel devraient consommer des webhooks
application.updatedetc., bien plus efficient. - Allowlist IPs pour les load tests : pour des bench prod sur IP fixe, demandez-nous d'ajouter votre IP source à
app.ratelimit.allowed-ips(bypass complet du limiter).
Compteur partagé multi-pod
En production, le compteur est stocké dans Redis (cluster-wide via INCR + PEXPIRE atomiques). Vos requêtes peuvent atterrir sur n'importe quel pod arch-core ; la limite reste celle de votre tenant globalement, pas par pod.
En dev / single-node, un store in-memory (ConcurrentHashMap) sert de fallback. Identique en API, comportement légèrement différent à la nanoseconde près sur le X-RateLimit-Reset.
Codes d'erreur liés
| Code | Quand | Action |
|---|---|---|
429 | Quota dépassé | Attendre Retry-After secondes, puis retry. |
503 | Backend saturé (rare) | Retry avec backoff exponentiel. |
401 | Token invalide / révoqué | Rotater le PAT, ne PAS retry. |
Roadmap
- v0.1.77.1 — overrides par endpoint (
@RateLimited(perMinute=10)sur les endpoints coûteux comme les exports). - v0.1.78 — quotas par plan (Free = 60/min, Pro = 180/min, Enterprise = 600/min).
- v0.2 — webhooks outbound qui réduisent drastiquement le besoin de polling.