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

PlanQuotaBurstCapacité totale
Free / Pro / Enterprise120 req/min+60180 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 :

HeaderTypeDescription
X-RateLimit-LimitintCapacité maximale par fenêtre (par défaut 180).
X-RateLimit-RemainingintTokens restants dans la fenêtre courante.
X-RateLimit-Resetint (epoch s UTC)Instant exact où la fenêtre se réinitialise.

En cas de 429, deux headers supplémentaires :

HeaderTypeDescription
Retry-Afterint (secondes)Délai avant de pouvoir réessayer (= X-RateLimit-Reset − maintenant).
Content-Typeapplication/jsonBody : {"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=N plutô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.updated etc., 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

CodeQuandAction
429Quota dépasséAttendre Retry-After secondes, puis retry.
503Backend saturé (rare)Retry avec backoff exponentiel.
401Token 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.

On this page