Source: https://aiprimetech.io/fr/blog/rate-limits-and-retries/ Juin 23, 2026 · 7 min · API # Gérer les limites de débit et les nouvelles tentatives sur les API de LLM A Par [AI Prime Tech](https://aiprimetech.io/fr/) · Avec l’aide de l’IA À 09:12, un mardi, notre pipeline de résumé semblait fonctionner normalement : 96 workers actifs, une latence médiane du modèle d’environ 2.8 secondes, aucun taux élevé d’erreurs 5xx. Puis un client a importé un arriéré de 38,000 tickets d’assistance. En quatre minutes, le pipeline était « en panne » sans qu’aucun serveur ne soit hors service. Nous atteignions deux limites différentes à la fois : - requests_per_minute: trop d’appels API - tokens_per_minute: un volume trop important de prompts et de réponses La logique naïve de nouvelle tentative a aggravé la situation. Chaque worker recevait 429, attendait une seconde, puis tous réessayaient ensemble. Cela créait une vague de nouvelles tentatives, qui produisait davantage de 429, ce qui créait une autre vague. Le système avait accidentellement mis en place une attaque par déni de service synchronisée contre son propre quota. Gérer les limites de débit des LLM ne se résume pas à « ajouter une temporisation exponentielle ». En production, vous devez envisager les tokens, les files d’attente, la concurrence, les nouvelles tentatives, l’idempotence, les fournisseurs de secours et le comportement de facturation comme un seul système. ## Les deux limites essentielles : RPM et TPM La plupart des API de LLM imposent une combinaison des limites suivantes : - RPM: requêtes par minute - TPM: tokens par minute - RPD / TPD: requêtes ou tokens par jour - Limites de requêtes simultanées: nombre maximal d’appels en cours - Limites propres à chaque modèle: limites plus élevées pour les petits modèles, plus basses pour les modèles de pointe Le point essentiel : Les dépassements des limites RPM et TPM se manifestent différemment. Si vous envoyez de nombreux petits prompts, vous atteignez d’abord la limite RPM. Si vous envoyez moins de prompts, mais plus volumineux, vous atteignez d’abord la limite TPM. Exemple : ``` Limit: - 600 requests/minute - 300,000 tokens/minute Workload A: - 1,000 short classification calls/minute - 200 tokens each - 200,000 tokens/minute total Result: - TPM is fine - RPM fails Workload B: - 100 long document analysis calls/minute - 5,000 tokens each - 500,000 tokens/minute total Result: - RPM is fine - TPM fails ``` Un piège fréquent : les développeurs limitent le débit des requêtes, mais pas celui des tokens. Cela fonctionne en préproduction, où les prompts sont courts. Puis les utilisateurs en production collent des PDF, des historiques de conversation, des journaux ou des contrats juridiques, et le budget de tokens s’évapore. En pratique, je considère que chaque appel à un LLM réserve deux ressources : ``` cost = { requests: 1, tokens: estimated_prompt_tokens + max_output_tokens } ``` Même si le modèle renvoie moins de tokens de sortie que max_output_tokens, réserver la borne supérieure évite de dépasser la capacité disponible. ## Ce qui se passe réellement lorsque vous ignorez la limite TPM Supposons que vous ayez : ``` TPM limit: 120,000 Average request: 4,000 input tokens + 1,000 max output tokens Reserved cost: 5,000 tokens ``` Votre débit maximal réel ne correspond pas à « autant de workers que possible ». Il est de : ``` 120,000 / 5,000 = 24 requests per minute ``` Cela représente une requête toutes les 2.5 secondes en moyenne. Si vous exécutez 100 workers, tous les appels du premier lot peuvent démarrer sans problème si le fournisseur autorise les pics de trafic. Puis le compte se heurte à une limite stricte. Chaque requête suivante reçoit 429, et les nouvelles tentatives s’accumulent. La solution ne consiste pas seulement à réduire le nombre de workers. Il vous faut un ordonnanceur qui tient compte des réservations de tokens. ## Un limiteur de débit minimal tenant compte des tokens Pour un seul processus, un seau à jetons suffit généralement. Vous reconstituez la capacité au fil du temps et exigez que chaque requête obtienne à la fois une capacité de requête et une capacité de tokens. Voici un exemple simplifié en Python : ``` import asyncio import time from dataclasses import dataclass @dataclass class Bucket: capacity: float refill_per_second: float tokens: float updated_at: float def refill(self): now = time.monotonic() elapsed = now - self.updated_at self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_per_second) self.updated_at = now async def acquire(self, amount: float): while True: self.refill() if self.tokens >= amount: self.tokens -= amount return missing = amount - self.tokens sleep_for = missing / self.refill_per_second await asyncio.sleep(min(sleep_for, 1.0)) class LLMRateLimiter: def __init__(self, rpm: int, tpm: int): now = time.monotonic() self.request_bucket = Bucket( capacity=rpm, refill_per_second=rpm / 60, tokens=rpm, updated_at=now, ) self.token_bucket = Bucket( capacity=tpm, refill_per_second=tpm / 60, tokens=tpm, updated_at=now, ) self.lock = asyncio.Lock() async def acquire(self, estimated_tokens: int): async with self.lock: await self.request_bucket.acquire(1) await self.token_bucket.acquire(estimated_tokens) ``` Utilisation : ``` estimated_tokens = prompt_tokens + max_output_tokens await limiter.acquire(estimated_tokens) response = await client.chat.completions.create( model="gpt-5.5", messages=messages, max_tokens=max_output_tokens, ) ``` Cet exemple est volontairement simple. Dans un système distribué, utilisez Redis, des verrous consultatifs Postgres ou une file d’attente avec distribution centralisée. Le principe essentiel reste le même : limitez à la fois le nombre de requêtes et le budget de tokens avant d’appeler l’API. ## Nouvelles tentatives : la temporisation est nécessaire, mais ne suffit pas Les nouvelles tentatives sont utiles dans les cas suivants : - Transitoires 429 réponses - Dépassements de délai réseau - Temporaires 500, 502, 503, ou 504 erreurs - Surcharge du côté du fournisseur - Réinitialisations de connexion Les nouvelles tentatives sont dangereuses dans les cas suivants : - Erreurs de validation - Erreurs de longueur du contexte - Échecs d’authentification - Rejets liés à la politique de sécurité - Prompts incorrects provoquant systématiquement un échec - Appels d’outils non idempotents La politique de nouvelle tentative doit d’abord classer les erreurs. | Type d’erreur | Réessayer ? | Action habituelle | 429 rate_limit_exceeded | Oui | Temporiser, respecter les en-têtes de nouvelle tentative, réduire la concurrence | 500/502/503/504 | Oui | Temporiser avec une variation aléatoire et un nombre limité de tentatives | Dépassement du délai avant le corps de la réponse | Peut-être | Réessayer avec une clé d’idempotence si cette fonction est prise en charge | Longueur du contexte dépassée | Non | Compresser, découper ou acheminer vers un modèle à contexte plus long | Clé API non valide | Non | Désactiver la clé, alerter l’opérateur | Rejet lié au contenu ou à la sécurité | Non | Renvoyer un échec contrôlé ou reformuler la requête | Effet de bord de l’outil déjà exécuté | Non/Peut-être | Nécessite une conception prenant en compte l’idempotence La forme de base de la temporisation : ``` import random def retry_delay(attempt: int, base=0.5, cap=30.0): exponential = min(cap, base * (2 ** attempt)) return random.uniform(0, exponential) ``` Ce random.uniform(0, exponential) correspond à la variation aléatoire. Ne l’omettez pas. Sans variation aléatoire : ``` 100 workers fail 100 workers sleep 1s 100 workers retry 100 workers fail 100 workers sleep 2s 100 workers retry ``` Avec variation aléatoire : ``` 100 workers fail workers retry across a spread of 0-1s then 0-2s then 0-4s ``` Le second schéma laisse au fournisseur et à votre propre file d’attente le temps de se rétablir. ### Respecter les en-têtes de nouvelle tentative lorsqu’ils sont présents De nombreuses API incluent une indication telle que : ``` Retry-After: 12 ``` ou des en-têtes de réinitialisation propres au fournisseur. Si l’API vous indique un délai précis, utilisez-le comme délai minimal : ``` delay = max(header_retry_after_seconds, retry_delay(attempt)) await asyncio.sleep(delay) ``` En pratique, je plafonne également la durée totale des nouvelles tentatives. Une requête de chat destinée à un utilisateur ne devrait pas réessayer silencieusement pendant deux minutes alors que l’utilisateur regarde un indicateur de chargement. Un traitement par lots en arrière-plan peut attendre beaucoup plus longtemps. Exemple de politique : ``` { "interactive_chat": { "max_attempts": 3, "max_total_retry_seconds": 8 }, "batch_summarization": { "max_attempts": 8, "max_total_retry_seconds": 300 } } ``` ## La mise en file d’attente vaut mieux qu’un afflux de workers Si la charge de travail dépasse votre quota, les workers ne doivent pas tous appeler directement l’API du LLM. Placez une file d’attente en amont. Une architecture pratique : ``` App/API -> Job queue -> Rate-aware dispatcher -> Provider client -> Result store ``` Le répartiteur prend en charge : - le suivi des RPM - le suivi des TPM - la sélection du modèle - la politique de nouvelle tentative - le basculement vers un autre fournisseur - les limites de concurrence - la priorité dans la file d’attente Cela rend la surcharge visible. Au lieu de voir des milliers de tâches échouer avec 429, vous voyez la longueur de la file d’attente et le temps d’attente estimé augmenter. Pour les systèmes reposant sur Redis, j’aime séparer les tâches « prêtes à être exécutées » des tâches « à réessayer plus tard » : ``` llm:ready llm:delayed llm:dead_letter ``` Une tâche qui échoue ne retourne pas immédiatement dans ready. Elle reçoit un horodatage de nouvelle tentative planifiée. Un petit ordonnanceur remet les tâches arrivées à échéance dans la file des tâches prêtes. ``` { "job_id": "sum_7f13", "model": "claude-sonnet-4.6", "estimated_tokens": 6200, "attempt": 2, "not_before": "2026-02-03T09:14:22Z" } ``` Ce not_before est un champ qui évite les tempêtes de nouvelles tentatives. ## Le contrôle de la concurrence diffère de la limitation du débit Les limites de concurrence plafonnent le travail en cours. Les limites de débit plafonnent le travail effectué par fenêtre de temps. Vous avez besoin des deux. Imaginez que chaque appel dure 20 secondes et que votre limite en RPM soit de 60. Un limiteur fondé uniquement sur les RPM autorise une requête par seconde. Après 20 secondes, vous avez environ 20 appels simultanés. Si la latence passe à 90 secondes, la même limite en RPM entraîne 90 appels simultanés. Cela peut dépasser les limites du fournisseur ou des ressources locales. Utilisez un sémaphore pour contrôler la concurrence : ``` provider_concurrency = asyncio.Semaphore(20) async def call_model(job): async with provider_concurrency: await limiter.acquire(job.estimated_tokens) return await send_request(job) ``` Un piège courant : si vous acquérez le sémaphore trop tôt, vos workers restent inactifs tout en occupant de précieux créneaux de concurrence, dans l’attente de jetons de débit. En général, réservez d’abord la capacité de débit, puis acquérez le sémaphore juste avant l’appel réseau. Si l’attente imposée par votre limiteur peut être longue, évitez de mobiliser les ressources des workers pendant cette attente. ## Dégradation progressive : consacrer la qualité là où elle compte Lorsque les limites de débit se font sentir, vous avez d’autres options que l’échec. Par exemple, un assistant de support client pourrait dégrader son service ainsi : | Niveau de pression | Stratégie | Impact sur l’utilisateur | Normal | Utiliser le modèle configuré le plus performant | Qualité optimale | Légère pression sur les TPM | Réduire max_tokens, raccourcir le contexte récupéré | Réponses légèrement plus courtes | Pression modérée | Acheminer les tâches simples vers un modèle plus petit | Minime si la classification ou le routage est simple | Forte pression | Mettre les tâches non urgentes en file d’attente | Travail en arrière-plan retardé | Pression extrême | Renvoyer une réponse en cache ou prédéfinie | Qualité inférieure, mais service disponible Exemples concrets : - Confiez l’étiquetage des tickets à Haiku 4.5, à Qwen ou à une petite variante de Llama au lieu d’utiliser un modèle de pointe. - Utilisez Sonnet 4.6 pour le raisonnement courant et réservez Opus 4.8 ou GPT-5.5 aux cas nécessitant une escalade. - Transférez les analyses à contexte long vers Fable 5 ou Gemini 3 uniquement lorsque le prompt nécessite réellement cette fenêtre de contexte. - Pour les traitements par lots, mettez en pause les locataires de faible priorité avant d’affecter le trafic interactif. La contrepartie est la complexité. Chaque voie de repli nécessite des tests, de l’observabilité et une validation par l’équipe produit. Dans certains domaines, une réponse de moindre qualité peut être plus dommageable qu’une réponse retardée. ## Repli avec plusieurs clés et plusieurs fournisseurs Le routage entre plusieurs clés et plusieurs fournisseurs peut améliorer la résilience, mais il est facile de mal le mettre en œuvre. Il existe trois schémas distincts : | Schéma | Problème résolu | Risque | Plusieurs clés, même fournisseur ou compte | Isolation opérationnelle | Peut enfreindre les conditions du fournisseur si utilisé pour contourner les limites | Plusieurs comptes ou régions | Isolation des défaillances | Complexité accrue de la facturation et de la gouvernance | Plusieurs fournisseurs ou modèles | Disponibilité et flexibilité des coûts | Variabilité des résultats, incompatibilité des prompts N’utilisez pas plusieurs clés pour contourner discrètement les limites. Utilisez-les pour assurer une séparation légitime entre locataires, environnements, produits ou périmètres de facturation. Une politique de routage bien définie pourrait ressembler à ceci : ``` { "task": "support_ticket_summary", "primary": { "provider": "anthropic", "model": "claude-sonnet-4.6" }, "fallbacks": [ { "provider": "openai", "model": "gpt-5.5", "when": ["provider_5xx", "rate_limit_after_retry_budget"] }, { "provider": "google", "model": "gemini-3", "when": ["context_length_needed", "primary_unavailable"] } ], "degrade_to": { "provider": "open_model_cluster", "model": "qwen", "when": ["batch_low_priority"] } } ``` Si vous utilisez un agrégateur ou une plateforme comme AI Prime Tech pour accéder à plusieurs modèles, les mêmes règles d’ingénierie s’appliquent : suivez explicitement l’idempotence, les budgets, les conditions de repli et les règles de facturation finale. ## Éviter la double facturation lors d’un repli La difficulté du repli consiste à savoir si la première requête s’est achevée. Considérez cette séquence : ``` Client sends request to Provider A Provider A processes it successfully Network connection drops before client receives response Client retries with Provider B Both providers bill Two completions may exist ``` Vous ne pouvez pas résoudre entièrement ce problème côté client, sauf si le fournisseur prend en charge les clés d’idempotence ou la recherche de requêtes. Mais vous pouvez limiter les dégâts. ### Utiliser des clés d’idempotence lorsqu’elles sont disponibles Générez une clé stable pour chaque opération logique : ``` import hashlib import json def idempotency_key(task_name, tenant_id, input_payload): canonical = json.dumps(input_payload, sort_keys=True, separators=(",", ":")) digest = hashlib.sha256(canonical.encode()).hexdigest() return f"{tenant_id}:{task_name}:{digest}" ``` Enregistrez l’opération avant l’envoi : ``` { "operation_id": "acme:summarize:9d2a...", "status": "in_flight", "provider": "anthropic", "model": "claude-sonnet-4.6", "created_at": "2026-02-03T09:12:01Z" } ``` Puis mettez-la à jour une fois terminée : ``` { "operation_id": "acme:summarize:9d2a...", "status": "completed", "provider_request_id": "req_abc123", "usage": { "input_tokens": 4180, "output_tokens": 612 } } ``` Avant de réessayer ailleurs, consultez la table des opérations. Si un autre worker l’a déjà terminée, renvoyez le résultat enregistré. ### Distinguer les types d’expiration de délai Toutes les expirations de délai ne se valent pas : - Expiration du délai de connexion : la requête n’a probablement jamais atteint le fournisseur - Expiration du délai de lecture avant les en-têtes : situation incertaine - Expiration du délai pendant la diffusion des tokens : le fournisseur l’a presque certainement traitée - Annulation côté client : dépend de la propagation de l’annulation Dans les cas incertains, je privilégie une politique prudente de nouvelle tentative : ``` interactive: - retry connect timeout - do not cross-provider retry after partial stream - show recoverable error if status unknown batch: - retry after reconciliation delay - prefer same provider with idempotency key - fallback only after retry budget expires ``` ### Diffuser en continu avec précaution La diffusion en continu améliore la latence perçue, mais complique les nouvelles tentatives. Une fois que vous avez envoyé une partie de la réponse à l’utilisateur, réessayer avec un autre modèle peut produire une suite différente. Pour le chat en continu, je considère généralement la réception du premier token comme le point à partir duquel aucune nouvelle tentative transparente n’est possible. Au-delà, les échecs deviennent visibles : ``` "The response was interrupted. Retry?" ``` C’est moins élégant qu’une reprise silencieuse, mais cela évite d’assembler des réponses incompatibles provenant de différents modèles. ## Observabilité : les métriques vraiment utiles Vous avez besoin de plus que du taux de réussite. Suivez au minimum : ``` llm_requests_total{provider,model,status} llm_tokens_reserved_total{provider,model} llm_tokens_used_total{provider,model} llm_retries_total{provider,model,error_type} llm_queue_depth{priority,task_type} llm_queue_wait_seconds{priority,task_type} llm_rate_limit_hits_total{provider,model,limit_type} llm_fallbacks_total{from_provider,to_provider,reason} llm_duplicate_suppressed_total{task_type} ``` Lors d’incidents réels, le panneau le plus utile du tableau de bord n’est souvent pas celui de la latence. C’est celui-ci : ``` queue_wait_seconds p50 / p95 rate_limit_hits by provider and model tokens_reserved vs tokens_used fallback rate by reason ``` Si tokens_reserved est systématiquement bien supérieur à tokens_used, vos réservations sont trop prudentes. Si cette valeur est trop proche de l’utilisation réelle ou lui est inférieure, vous surallouez les ressources et atteindrez la limite de TPM de manière inattendue. ## Un wrapper pratique pour les nouvelles tentatives Voici un schéma compact pour ne réessayer que les échecs qui s’y prêtent : ``` import asyncio import random import time RETRYABLE_STATUS = {429, 500, 502, 503, 504} class LLMError(Exception): def __init__(self, status, message="", retry_after=None): self.status = status self.retry_after = retry_after super().__init__(message) def jittered_backoff(attempt, base=0.5, cap=20): return random.uniform(0, min(cap, base * (2 ** attempt))) async def call_with_retries(fn, max_attempts=4, max_elapsed=20): started = time.monotonic() for attempt in range(max_attempts): try: return await fn() except LLMError as error: elapsed = time.monotonic() - started if error.status not in RETRYABLE_STATUS: raise if attempt == max_attempts - 1 or elapsed >= max_elapsed: raise delay = jittered_backoff(attempt) if error.retry_after is not None: delay = max(delay, error.retry_after) remaining = max_elapsed - elapsed await asyncio.sleep(min(delay, remaining)) ``` Le wrapper est volontairement simple. La sophistication doit résider dans la classification, la mise en file d’attente, l’idempotence et le routage, pas dans une boucle magique qui réessaie indéfiniment. ## Conseils pratiques - Modélisez les limites de débit à la fois en RPM et en TPM. Une régulation du débit tenant compte des tokens évite les surprises liées aux prompts volumineux. - Utilisez temporisation exponentielle avec variation aléatoire, et respectez les en-têtes de nouvelle tentative lorsque le fournisseur en fournit. - Placez une file d’attente et un répartiteur entre votre application et les fournisseurs de LLM pour les traitements par lots ou les charges de travail à fort volume. - Levier de contrôle la concurrence séparément des limites de débit ; sinon, les pics de latence peuvent entraîner un nombre excessif d’appels en cours. - Design dégradation contrôlée de manière délibérée : modèles plus petits, contexte plus court, tâches de fond mises en file d’attente ou réponses en cache. - Traitez basculement entre plusieurs fournisseurs comme une fonctionnalité de fiabilité, et non comme une autorisation à multiplier les requêtes en double. - Utilisez clés d’idempotence et enregistrements des opérations pour réduire la double facturation et éliminer les générations en double. - Soyez transparents avec les utilisateurs lorsqu’une réponse en streaming échoue alors que des tokens ont déjà été émis. - Construisez des tableaux de bord autour des indicateurs suivants : temps d’attente en file, réservation de tokens, atteintes des limites de débit, nouvelles tentatives et motifs de basculement. - Gardez une logique de nouvelle tentative simple ; le véritable travail en production consiste à définir des budgets, à planifier et à savoir quand ne pas réessayer. [API](/fr/blog/tag/api/) A [AI Prime Tech](https://aiprimetech.io/fr/) Ce blog est publié par AI Prime Tech. Les articles sont rédigés avec l’aide de l’IA ; vérifiez les détails propres à chaque version, tels que les identifiants de modèles, les prix et les limites, dans la documentation officielle avant de vous y fier. Accédez à l’API Claude à moindre coût Une seule clé API pour Claude Opus 5.5, Sonnet 5, Haiku 4.5 et Fable 5.1, ainsi que GPT-6 modèles. Paiement à l’usage, sans abonnement. [Obtenir votre clé API →](/register) AI Prime Tech est une passerelle API tierce indépendante. Claude™ et Anthropic® sont des marques d’Anthropic, PBC. Aucune affiliation ni approbation n’est sous-entendue.