Limites de débit Contre-pression Fiabilité API LLM

Limites de débit d'API LLM et contre-pression qui tient

Comment lire les en-têtes de limite de débit des LLM, absorber les 429 avec backoff et jitter, et appliquer une contre-pression côté client via files bornées et plafonds de concurrence pour délester en douceur.

AveMujica API 10 min de lecture

Quand un fournisseur de LLM vous limite en débit, il renvoie un HTTP 429, et la réaction naïve — réessayer immédiatement — aggrave la situation, car les requêtes échouées comptent malgré tout dans votre quota par minute. Le correctif durable repose sur deux couches qui agissent de concert : lire les en-têtes de limite de débit envoyés par le fournisseur pour savoir combien de budget il vous reste, et appliquer une contre-pression de votre côté — files bornées, plafonds de concurrence, backoff avec jitter — de sorte que lorsque la demande dépasse l'offre, votre système ralentisse au lieu de fondre. Cet article couvre ces deux couches et le code pour les implémenter.

Les limites de débit ne sont pas une condition d'erreur à masquer ; ce sont un signal de capacité. Un système qui les traite comme un signal se dégrade de façon prévisible. Un système qui les ignore oscille entre marteler le fournisseur et jeter les requêtes des utilisateurs.

Lisez les en-têtes avant de régler quoi que ce soit

OpenAI renvoie l'état de la limite de débit sur chaque réponse via des en-têtes que vous pouvez inspecter directement. Le jeu basé sur les requêtes est :

  • x-ratelimit-limit-requests — le plafond de requêtes dans la fenêtre
  • x-ratelimit-remaining-requests — combien il vous en reste
  • x-ratelimit-reset-requests — le temps avant la réinitialisation de la fenêtre de requêtes

Il existe un jeu parallèle basé sur les tokens — x-ratelimit-limit-tokens, x-ratelimit-remaining-tokens, x-ratelimit-reset-tokens — et des variantes à l'échelle du projet comme x-ratelimit-remaining-project-tokens. La plupart des limites réelles mordent sur les tokens, pas sur le nombre de requêtes, car un seul appel à long contexte peut consommer une large part de votre budget de tokens. Journalisez ces en-têtes sur chaque réponse et vous verrez l'étranglement arriver avant qu'il ne devienne un mur de 429 :

resp = client.chat.completions.with_raw_response.create(
    model="gpt-5.6",
    messages=[{"role": "user", "content": prompt}],
)
h = resp.headers
print(h.get("x-ratelimit-remaining-requests"), h.get("x-ratelimit-remaining-tokens"))

La journalisation de ces en-têtes est le socle opérationnel : une fois x-ratelimit-remaining-requests et x-ratelimit-remaining-tokens enregistrés pour chaque réponse, vous pouvez voir quel amont approche de son plafond avant l'arrivée des 429. Une passerelle est bien placée pour centraliser cette journalisation puisque toutes les réponses la traversent déjà ; en revanche, une courbe du budget restant par canal se construit à partir des en-têtes bruts, ce n'est pas une valeur précalculée fournie par la réponse.

Réessayez les 429 avec backoff exponentiel et jitter

Quand un 429 arrive bel et bien, temporisez avant de réessayer. Le backoff exponentiel multiplie l'attente après chaque échec ; le jitter la randomise pour qu'une flotte de clients ne réessaie pas toute au même instant et ne se percute pas de nouveau — un effet de troupeau (thundering herd) qui recrée la surcharge dont vous vous remettez.

import random, time

def backoff_sleep(attempt, base=0.5, cap=20.0):
    # full jitter: random point in [0, min(cap, base * 2**attempt))
    ceiling = min(cap, base * (2 ** attempt))
    time.sleep(random.uniform(0, ceiling))

for attempt in range(6):
    r = call_model(prompt)
    if r.status_code != 429:
        break
    backoff_sleep(attempt)

Deux règles gardent cela honnête. Premièrement, bornez à la fois le délai et le nombre de tentatives — les retentatives infinies ne font que déplacer la panne en aval. Deuxièmement, ne réessayez que ce qui est réessayable : 429 et les 5xx/timeouts transitoires valent une retentative ; un 400 pour une requête malformée échouera à l'identique à chaque fois. Comme une écriture réessayée peut dupliquer le travail, associez les retentatives à l'idempotence — le traitement complet est dans Retentatives et idempotence d'API LLM.

Si une réponse inclut un en-tête Retry-After, préférez-le à votre backoff calculé — le serveur vous dit exactement quand la fenêtre rouvre, et honorer cette valeur récupère plus vite tout en évitant de gaspiller des tentatives vouées à l'échec. Considérez votre planning exponentiel comme le repli pour quand le serveur ne fournit pas d'indice explicite.

Contre-pression : ralentissez la source, ne lâchez pas le plancher

Les retentatives gèrent un seul appel échoué. La contre-pression gère la situation où votre débit d'admission dépasse tout simplement ce que le fournisseur accepte. L'idée centrale de la théorie des files d'attente : placez un tampon borné entre les producteurs et le fournisseur, et quand le tampon est plein, faites attendre les producteurs plutôt que d'accepter un travail illimité que vous ne pouvez pas servir.

Trois mécanismes se composent en une stratégie de contre-pression :

MécanismeCe qu'il borneLe mode de défaillance qu'il prévient
Plafond de concurrence (sémaphore)Requêtes en volOuvrir des milliers de sockets qui font tous 429
File bornéeTravail en attente de démarrageCroissance mémoire illimitée sous un pic
Contrôle d'admission / délestageNouveau travail acceptéAccepter des requêtes que vous ne servirez jamais à temps

Un plafond de concurrence est le changement au levier le plus élevé et le plus facile à ajouter. Plafonnez les appels simultanés en vol pour égaler, plutôt que dépasser, la tolérance du fournisseur :

import asyncio

sem = asyncio.Semaphore(20)  # never more than 20 concurrent upstream calls

async def guarded_call(prompt):
    async with sem:
        return await call_model_async(prompt)

Quand le sémaphore est plein, les nouveaux appelants attendent — cette attente est la contre-pression. Elle propage la contrainte jusqu'à la source au lieu de la laisser exploser à la frontière du fournisseur.

Délestez délibérément quand la file est pleine

Une file bornée finit par se remplir lors d'un pic soutenu. Ce qui se passe alors est une décision produit, et l'expliciter est tout l'enjeu :

  • Rejeter vite — renvoyez un 503 avec un Retry-After pour que les appelants réessaient plus tard. Idéal pour le trafic interactif, où une réponse lente est pire qu'un clair « réessayez ».
  • Délester par priorité — abandonnez ou différez le travail à faible priorité (lots, arrière-plan) pour que les requêtes interactives continuent de circuler.
  • Dégrader — routez le débordement vers un modèle moins cher et plus rapide, plus susceptible d'avoir de la marge.

L'anti-modèle est une file non bornée qui accepte tout et gonfle silencieusement la latence jusqu'à ce que les requêtes expirent de toute façon. Cela convertit un rejet visible et récupérable en une panne invisible à l'échelle du système. Bornez la file, et décidez de ce que « tomber du bout » signifie.

Là où une passerelle centralise la politique

Une contre-pression implémentée dans chaque service finit par dériver : chacun choisit des limites différentes, qui interagissent mal. Une passerelle est l'endroit naturel pour appliquer une politique cohérente unique, car tout le trafic la traverse déjà. Dans ce projet, les connexions aux fournisseurs sont enregistrées comme des canaux, chacun avec son propre quota et sa priorité ; la passerelle bascule déjà vers un canal sain lorsque l'un commence à s'étrangler, selon la stratégie de bascule d'API LLM, et journalise l'usage de chaque requête pour guider vos réglages. Les mécanismes d'admission plus riches sont une architecture à ajouter au même point de passage :

  • suivre le budget de tokens et de requêtes restant par canal en conservant les en-têtes de réponse,
  • appliquer des plafonds de concurrence par canal pour qu'un canal très sollicité n'affame pas les autres,
  • maintenir un budget de réessais agrégé afin que le trafic de reprise ne puisse pas amplifier une panne.

Considérez ces trois éléments comme une conception à implémenter dans la passerelle, et non comme des options fournies par défaut ; la valeur de la passerelle tient à l'existence d'un seul endroit où les ajouter. Cette centralisation compte pour une raison concrète : si dix services réessaient chacun indépendamment sur 429, leur trafic de réessai combiné peut à lui seul dépasser votre limite, et seul un budget de réessais partagé empêche cette amplification auto-infligée.

Une défense en couches, par ordre d'impact

Appliquez-les dans cet ordre ; chacune achète du temps pour la suivante :

  1. Mesurer — journalisez les en-têtes de limite de débit pour rendre l'étranglement visible tôt.
  2. Limiter la concurrence — un sémaphore qui égale la tolérance du fournisseur.
  3. Borner la file — un tampon fixe, jamais illimité.
  4. Temporiser avec jitter — sur 429/5xx, jamais sur les erreurs client 4xx.
  5. Délester ou dégrader — une politique explicite pour une file pleine.
  6. Répartir la charge — plusieurs canaux et une bascule pour relever le plafond effectif.

Les deux premiers changements éliminent d'ordinaire la majeure partie de la douleur des 429. Le reste transforme les pics restants en ralentissements gracieux plutôt qu'en pannes.

Budgétisez les tokens, pas seulement les requêtes

Comme les limites de tokens saturent généralement avant celles des requêtes, estimez le coût en tokens d'un appel avant de l'envoyer et réservez le budget en conséquence. Une requête consomme à la fois des tokens d'entrée (votre prompt, plus tout historique chaîné) et des tokens de sortie (bornés par max_tokens/max_output_tokens). Si vous admettez du travail sur le seul décompte de requêtes, une rafale d'appels à long contexte peut crever le plafond de tokens pendant que votre compteur de requêtes semble sain — et chacun de ces appels revient en 429.

Une règle d'admission pratique est une porte consciente des tokens : suivez le budget de tokens restant depuis x-ratelimit-remaining-tokens, estimez le pire cas de tokens de chaque appel en attente, et n'admettez que les appels dont l'estimation tient dans la marge restante. C'est l'idée du token bucket appliquée au trafic LLM — se recharger à l'intervalle de réinitialisation, dépenser à l'admission, et faire attendre les appelants quand le seau est bas.

  • Plafonnez max_tokens/max_output_tokens à la taille que vous attendez réellement ; un plafond surdimensionné réserve un budget que vous n'utilisez jamais et affame les autres appels.
  • Comptez l'historique chaîné : avec un état de conversation stocké, les tokens d'entrée antérieurs se facturent toujours comme tokens d'entrée, si bien qu'un long fil coûte plus par tour qu'il n'y paraît.
  • Préférez de nombreux petits appels à un seul appel géant lorsqu'une requête unique réserverait d'un coup une large part de la fenêtre de tokens.

Bien dimensionner l'estimation de tokens est souvent un levier plus grand que régler la concurrence, car cela empêche les appels qui feraient 429 d'être même admis. Coût et capacité sont ici le même cadran, ce qui explique le lien avec la réduction des coûts d'API LLM.

FAQ

Quel code de statut signifie que je suis limité en débit, et dois-je le réessayer ?

HTTP 429. Il est réessayable, mais seulement avec backoff et jitter, jamais immédiatement — les requêtes échouées comptent toujours dans votre quota par minute, si bien que des boucles de retentative serrées aggravent l'étranglement. Plafonnez les tentatives et le délai.

Ce sont les limites de requêtes ou de tokens que je vais réellement atteindre ?

En général les limites de tokens. Vérifiez x-ratelimit-remaining-tokens en parallèle de x-ratelimit-remaining-requests. Une charge de travail avec de longs prompts ou de grands max_tokens peut épuiser le budget de tokens en touchant à peine le décompte de requêtes, si bien que budgétiser sur le seul décompte de requêtes vous induit en erreur.

Relever mon plafond de concurrence augmente-t-il le débit ?

Seulement jusqu'à la tolérance du fournisseur. Au-delà de ce point, la concurrence supplémentaire produit plus de 429, plus de retentatives, et un débit utile plus faible. Réglez le sémaphore sur la plus grande valeur où le taux de 429 reste proche de zéro, puis arrêtez-vous.

Une file plus grande est-elle un remède à la surcharge soutenue ?

Non. Une file plus grande ne fait que retarder le moment de vérité et gonfler la latence de queue pendant qu'elle se remplit. Si l'admission dépasse la capacité de façon soutenue, aucun tampon ne vous sauve — il vous faut du délestage, une limite de débit plus élevée, ou davantage de canaux. Les files absorbent les rafales, pas les tendances.

Commencez par un sémaphore et la journalisation des en-têtes

Ajoutez un sémaphore de concurrence autour de vos appels de modèle et commencez à journaliser x-ratelimit-remaining-tokens sur chaque réponse. Pendant une journée, tracez ces valeurs face aux 429 : si les creux du budget de tokens coïncident avec les erreurs, abaissez le sémaphore jusqu'à ce qu'ils disparaissent. Cette seule boucle de rétroaction élimine la plupart des incidents de limite de débit avant que vous ne construisiez quoi que ce soit de plus élaboré.

Références