Passer au contenu

Limitation de débit IA pour la voix : Concurrence, files d’attente et erreurs 429

Publié

ÉcouterÉcouter cet article

La plupart des équipes abordent la limitation de débit IA pour la voix comme pour d’autres API : limiter les requêtes par minute, réessayer quand le serveur refuse, et passer à la suite. Sur ElevenLabs, ce modèle ne tient pas dès le premier pic de trafic, car la vraie limite concerne la concurrence, pas le nombre de requêtes.

Ce guide explique pourquoi la concurrence est la vraie contrainte, puis détaille les bonnes pratiques côté client pour rester dans les limites. Pools de concurrence bornée, gestion élégante des erreurs 429, équité multi-locataires, token bucket et leaky bucket : nous proposons des systèmes concrets à mettre en place. Chaque méthode est accompagnée d’un exemple TypeScript prêt à adapter.

Si vous créez des agents vocaux, des pipelines de narration ou tout autre système de production basé sur nos modèles et que vous souhaitez passer à l’échelle, ce guide est fait pour vous.

À retenir

  • La limitation de débit IA pour la voix repose sur la gestion de la concurrence, pas sur le comptage des requêtes par minute.
  • Atteindre la limite de débit ne bloque pas immédiatement le trafic. Les requêtes passent d’abord dans une file d’attente prioritaire qui ajoute environ 50 ms.
  • Dépasser la capacité même après la mise en file d’attente entraîne une erreur HTTP 429.
  • Les WebSockets augmentent fortement la capacité effective, car seule la génération active compte dans votre limite.
  • Les systèmes multi-locataires nécessitent une couche d’équité supplémentaire : buckets par locataire, files d’attente pondérées, marge de sécurité réservée et répartition sur plusieurs clés pour l’isolation.
  • Deux en-têtes de réponse, current-concurrent-requests et maximum-concurrent-requests, vous indiquent où vous en êtes avec la limitation de débit IA.

Pourquoi la vraie limite est la concurrence, pas les requêtes par minute

La concurrence correspond au nombre de requêtes en cours au même instant. Les requêtes par minute mesurent le débit sur une période. Comprendre cette différence est essentiel, car c’est ce qui détermine comment rester dans la limite.

Quand vous utilisez l’un des modèles ElevenLabs, la charge serveur dépend du nombre d’utilisateurs simultanés. La génération audio occupe un slot pendant toute la durée de la génération, qui varie selon la longueur de l’entrée, le modèle et la charge.

Une limite de requêtes par minute ne vous dit rien sur le nombre de slots occupés à l’instant T, qui est pourtant la seule chose mesurée par le serveur.

Les limites par offre et par famille de modèles

Votre budget de concurrence n’est pas un chiffre unique. Les limites de concurrence varient selon l’offre et la famille de modèles. Par exemple, Speech to Text bénéficie d’une limite plus élevée que Text to Speech, car les requêtes de transcription sont généralement plus courtes et le système peut en absorber davantage en même temps.

Multilingual v2
Free
2
Starter
3
Creator
5
Pro
10
Scale
15
Business
15
Enterprise
Elevated
Flash
Free
4
Starter
6
Creator
10
Pro
20
Scale
30
Business
30
Enterprise
Elevated
STT
Free
8
Starter
12
Creator
20
Pro
40
Scale
60
Business
60
Enterprise
Elevated
Realtime STT
Free
6
Starter
9
Creator
15
Pro
30
Scale
45
Business
45
Enterprise
Elevated
Priority
Free
3
Starter
4
Creator
5
Pro
5
Scale
5
Business
5
Enterprise
6

La limite s’applique par famille de modèles. Si vous utilisez Flash pour les agents et Multilingual v2 pour la narration, vous avez deux budgets distincts. Les chiffres actuels par offre et la section sur la concurrence sont détaillés sur la page des modèles.

Que se passe-t-il quand vous atteignez la limite de concurrence ?

Atteindre la limite de concurrence ne bloque pas immédiatement le trafic. Le système dégrade progressivement via une file d’attente prioritaire, et ne rejette complètement que si la capacité totale est dépassée.

Tant que vous êtes sous la limite, les requêtes sont traitées immédiatement. Une fois la limite atteinte, les requêtes suivantes entrent dans une file d’attente selon la priorité de votre offre. La file ajoute généralement environ 50 ms de latence, donc un léger dépassement passe inaperçu pour les utilisateurs.

Si le système reste surchargé après la mise en file d’attente, vous recevez une erreur HTTP 429. C’est le signal pour ralentir, pas pour réessayer immédiatement. Le niveau de priorité dans le tableau détermine l’ordre de vos requêtes dans la file ; les offres supérieures passent en priorité.

HTTP vs WebSocket : comment chaque mode compte dans votre limite

Le mode de transport choisi influence directement la limitation de débit et votre budget. Une même conversation peut consommer des montants très différents de votre budget de concurrence selon qu’elle passe par HTTP ou WebSocket.

En HTTP, chaque requête compte individuellement dans votre limite de concurrence pendant toute sa durée. En WebSocket, seule la période où le modèle génère activement de l’audio est comptée. Un WebSocket ouvert mais inactif ne compte quasiment pas.

Pour un agent vocal, une conversation comporte de longs moments sans parole ni génération. En HTTP, vous occupez un slot à chaque tour de requête. En WebSocket, le slot n’est utilisé que pendant les millisecondes de génération active, donc un slot de concurrence peut être partagé entre de nombreuses conversations.

Voir le guide WebSocket TTS en temps réel pour les détails du protocole. Pour du trafic interactif, le WebSocket est le bon choix par défaut.

Pourquoi ~5 de concurrence suffisent pour ~100 diffusions

Les calculs de concurrence sont contre-intuitifs tant qu’on ne prend pas en compte le temps d’écoute. La génération est bien plus rapide que la lecture, et un slot n’est occupé que pendant la génération. C’est ce décalage qui permet à un petit budget de servir un large public.

Une requête qui prend une fraction de seconde à générer produit plusieurs secondes d’audio que l’auditeur va écouter, et pendant la lecture, le slot est libéré pour d’autres utilisateurs.

En règle générale, une limite de concurrence de 5 permet de gérer environ 100 diffusions audio simultanées. Le chiffre exact dépend de la voix, du rythme de parole et des silences entre les phrases.

Les en-têtes qui indiquent votre position

Vous n’avez pas besoin de deviner où vous en êtes par rapport à la limite. Chaque réponse contient deux chiffres pour mesurer votre marge, sans estimation.

Surveillez ces deux en-têtes :

  • current-concurrent-requests : combien de requêtes sont en cours en ce moment ?
  • maximum-concurrent-requests : votre limite pour cette famille de modèles.

Ensemble, ces en-têtes donnent une vue en temps réel de votre utilisation actuelle et de la capacité disponible. Vous n’avez pas à deviner avant de rencontrer une limite IA.

Stratégies côté client pour la limitation de débit IA

Quatre mécanismes couvrent presque tous les scénarios de limitation de débit IA :

  • Un token bucket : Si des jetons sont disponibles, les requêtes passent. La capacité se recharge avec le temps, ce qui permet d’absorber les pics courts sans dépasser la limite.
  • Un leaky bucket : Permet de lisser le trafic entrant à un débit fixe, évitant que des pics soudains ne saturent vos systèmes en aval.
  • Un pool de concurrence bornée : Limite le nombre total de requêtes actives en même temps, donc vous ne dépassez jamais la limite de requêtes simultanées.
  • Backoff exponentiel avec jitter complet : Allonge progressivement le délai entre les tentatives échouées pour éviter que tous les clients ne réessaient en même temps.

Les sections ci-dessous montrent comment les mettre en place un par un, en commençant par celui qui correspond le plus directement à la limite de concurrence.

Tous les exemples ci-dessous supposent un client unique, initialisé une seule fois :

import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const elevenlabs = new ElevenLabsClient({ apiKey: process.env.ELEVENLABS_API_KEY });

Concurrence bornée : le mécanisme qui colle à la limite

Puisque le serveur mesure la concurrence, le contrôle le plus direct côté client est un pool de workers borné qui limite le nombre de requêtes en cours. Fixez la limite un peu en dessous de celle de votre offre pour garder une marge pour la file d’attente et le jitter.

async function pool<T, R>(
  items: T[],
  maxInFlight: number,
  worker: (item: T) => Promise<R>,
): Promise<R[]> {
  const results: R[] = new Array(items.length);
  let next = 0;

  async function run(): Promise<void> {
    while (next < items.length) {
      const i = next++;
      results[i] = await worker(items[i]); // never more than maxInFlight of these run at once
    }
  }

  await Promise.all(
    Array.from({ length: Math.min(maxInFlight, items.length) }, run),
  );
  return results;
}

async function synthesize(text: string): Promise<Buffer> {
  const stream = await elevenlabs.textToSpeech.stream("JBFqnCBsd6RMkjVDRZzb", {
    text,
    modelId: "eleven_flash_v2_5",
    outputFormat: "mp3_44100_128",
  });
  const chunks: Buffer[] = [];
  for await (const chunk of stream) chunks.push(Buffer.from(chunk));
  return Buffer.concat(chunks);
}

// Plan Flash limit is, say, 10. Stay under it.
const texts = Array.from({ length: 50 }, (_, i) => `Sentence number ${i}.`);
const audio = await pool(texts, 8, synthesize); // never more than 8 in flight

Token bucket : autoriser les pics, limiter la moyenne

Un token bucket contient jusqu’à capacité jetons et se recharge à refillRate jetons par seconde. Chaque requête consomme un jeton, donc le bucket permet des pics courts tout en limitant le débit sur la durée.

C’est l’outil idéal pour lisser l’arrivée soudaine d’un lot de tâches, afin de ne pas tout lancer d’un coup et saturer la concurrence.

class TokenBucket {
  private tokens: number;
  private updated = performance.now();

  constructor(private capacity: number, private refillPerSec: number) {
    this.tokens = capacity;
  }

  private refill(): void {
    const now = performance.now();
    const elapsed = (now - this.updated) / 1000;
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillPerSec);
    this.updated = now;
  }

  tryAcquire(cost = 1): boolean {
    this.refill();
    if (this.tokens >= cost) {
      this.tokens -= cost;
      return true;
    }
    return false;
  }

  timeUntil(cost = 1): number {
    this.refill();
    return this.tokens >= cost ? 0 : ((cost - this.tokens) / this.refillPerSec) * 1000;
  }
}

Leaky bucket : garantir un débit constant

Parfois, vous ne voulez tolérer aucun pic. Un leaky bucket admet le travail à un rythme fixe, peu importe la variabilité de l’entrée. C’est le bon choix si le système en aval préfère une charge régulière à des pics occasionnels.

Par exemple, si vous restez volontairement bien en dessous d’un petit budget de concurrence partagé avec d’autres services.

class LeakyBucket {
  private next = performance.now();
  constructor(private intervalMs: number) {} // admit at most one item per intervalMs

  async acquire(): Promise<void> {
    const now = performance.now();
    const wait = Math.max(0, this.next - now);
    this.next = Math.max(now, this.next) + this.intervalMs;
    if (wait > 0) await new Promise((r) => setTimeout(r, wait));
  }
}

Backoff exponentiel avec jitter complet

Quand une requête échoue avec un statut réessayable, réessayer tout de suite aggrave la situation. Le backoff espace les tentatives, et le jitter complet répartit le délai de façon aléatoire, ce qui évite que tous les clients réessaient en même temps et recréent le pic initial.

L’exemple ci-dessous fait référence à RetryableError, une petite classe qui contient le statut d’échec et la valeur Retry-After. Elle est définie dans la section gestion élégante des erreurs 429 ci-dessous.

async function withBackoff<T>(
  call: () => Promise<T>,
  opts: { maxAttempts?: number; baseMs?: number; capMs?: number } = {},
): Promise<T> {
  const { maxAttempts = 5, baseMs = 500, capMs = 20_000 } = opts;
  let attempt = 0;
  for (;;) {
    try {
      return await call();
    } catch (e) {
      if (!(e instanceof RetryableError) || ++attempt >= maxAttempts) throw e;
      // honor Retry-After if present; otherwise capped exponential growth with full jitter
      const delay =
        e.retryAfterMs ?? Math.random() * Math.min(capMs, baseMs * 2 ** attempt);
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}

Gestion élégante des erreurs 429 : que faire quand vous atteignez la limite

Une erreur 429 signifie que vous avez dépassé la capacité même après la file d’attente prioritaire, donc la bonne réaction est de ralentir, pas de réessayer plus fort. Il y a quatre façons de gérer cela :

  • Détection
  • Respect du Retry-After
  • Remonter la pression
  • Éviter les tempêtes de retries avec un circuit breaker

Voyons ces points plus en détail.

La première étape est la détection. Considérez HTTP 429 (et les erreurs transitoires 500, 502, 503, 504) comme réessayables, et 400, 401, 403, 422 comme non réessayables ; réessayer une requête mal formée ou non autorisée ne sert à rien et gaspille un slot.

La deuxième étape est de respecter Retry-After. Si la réponse contient cet en-tête, respectez-le à la lettre au lieu de calculer votre propre délai. Le serveur sait mieux que votre formule quand il aura de la capacité. N’utilisez le backoff avec jitter que si l’en-tête est absent.

class RetryableError extends Error {
  constructor(public status: number, public retryAfterMs?: number) {
    super(`retryable ${status}`);
  }
}

function classify(resp: Response): void {
  if ([429, 500, 502, 503, 504].includes(resp.status)) {
    const ra = resp.headers.get("retry-after");
    throw new RetryableError(resp.status, ra ? Number(ra) * 1000 : undefined);
  }
  if (!resp.ok) throw new Error(`non-retryable ${resp.status}`);
}

Troisième point : remonter la pression. Ne laissez pas les retries s’accumuler en silence. Si la profondeur de votre file ou la marge mesurée indique que vous ne pourrez pas traiter une nouvelle requête rapidement, refusez-la d’emblée avec un signal clair plutôt que d’accepter un travail que vous ne pouvez pas faire.

Quatrième point : éviter les tempêtes de retries avec un circuit breaker. Si les échecs dépassent un seuil, ouvrez le circuit et échouez rapidement pendant une période de refroidissement au lieu d’envoyer des requêtes vouées à l’échec. Après ce délai, envoyez quelques requêtes de test ; si elles réussissent, refermez le circuit.

class CircuitBreaker {
  private failures = 0;
  private openedAt: number | null = null;
  constructor(private threshold = 5, private cooldownMs = 10_000) {}

  allow(): boolean {
    if (this.openedAt === null) return true;
    if (performance.now() - this.openedAt >= this.cooldownMs) {
      this.openedAt = null; // half-open: allow a probe
      this.failures = 0;
      return true;
    }
    return false;
  }

  record(ok: boolean): void {
    if (ok) {
      this.failures = 0;
      this.openedAt = null;
    } else if (++this.failures >= this.threshold) {
      this.openedAt = performance.now();
    }
  }
}

Modèles de quotas multi-locataires pour la limitation de débit IA

Tout ce qui précède suppose une seule application avec un seul budget. Si vous construisez un SaaS sur ElevenLabs, le problème change : votre budget de concurrence est partagé entre tous vos clients, et un locataire qui lance un traitement ne doit pas bloquer le trafic en direct des autres. Il vous faut une couche d’équité entre vos locataires et la limite globale.

La base, ce sont les token buckets par locataire. Donnez à chaque locataire son propre bucket dimensionné selon ses droits, et acceptez une requête seulement si le bucket du locataire et le limiteur global l’autorisent.

class MultiTenantAdmission {
  private tenantBuckets = new Map<string, TokenBucket>();
  constructor(private globalMaxInFlight: number) {}

  private bucket(tenant: string): TokenBucket {
    let b = this.tenantBuckets.get(tenant);
    if (!b) {
      // Each tenant: burst of 5, sustained 2 starts/sec. Tune per tier.
      b = new TokenBucket(5, 2);
      this.tenantBuckets.set(tenant, b);
    }
    return b;
  }

  async run<R>(tenant: string, work: () => Promise<R>): Promise<R> {
    const b = this.bucket(tenant);
    if (!b.tryAcquire()) {
      throw new RetryableError(429, b.timeUntil());
    }
    // ... then admit through the global limiter (e.g. the bounded pool above)
    return work();
  }
}

Les buckets empêchent un locataire d’abuser, mais ne décident pas qui passe en cas de concurrence. Pour cela, utilisez une file d’attente pondérée.

Ne servez pas en premier arrivé, premier servi, car un pic d’un locataire pourrait monopoliser les slots. Maintenez une file par locataire et répartissez selon le poids de chacun, pour qu’un client payant ait une part plus grande de la capacité que les gratuits.

En plus de l’équité, gardez une marge de sécurité. Ne laissez jamais le trafic normal consommer 100 % de la limite de concurrence. Gardez 15 à 20 % en réserve pour les requêtes interactives sensibles à la latence et pour la file prioritaire.

Quand l’équité sur un seul budget ne suffit plus, répartissez sur plusieurs espaces de travail ou clés. Un seul budget finit toujours par devenir le goulot d’étranglement, peu importe la répartition.

À ce stade, séparez les charges sur différents espaces de travail ou clés API avec leurs propres budgets : par exemple, une clé pour le trafic agent en temps réel et une autre pour la narration en arrière-plan, pour qu’un retard de narration n’impacte pas la capacité des agents.

Les espaces de travail permettent aussi d’appliquer des restrictions de périmètre, des quotas de crédits et des contrôles par clé, décrits dans la documentation d’authentification.

Surveiller votre utilisation de la concurrence

Tout cela n’est pas ajustable sans mesure ; vous ne pouvez pas gérer une marge que vous ne mesurez pas. Enregistrez current-concurrent-requests et maximum-concurrent-requests à chaque réponse, identifiés par famille de modèles, et publiez le ratio d’utilisation comme une jauge.

function recordHeadroom(resp: Response, metrics: Metrics): void {
  const cur = Number(resp.headers.get("current-concurrent-requests"));
  const max = Number(resp.headers.get("maximum-concurrent-requests"));
  if (Number.isFinite(cur) && Number.isFinite(max)) {
    metrics.gauge("el.concurrency.current", cur);
    metrics.gauge("el.concurrency.max", max);
    if (max > 0) metrics.gauge("el.concurrency.utilization", cur / max);
  }
}

Quatre indicateurs à suivre :

  • Utilisation (actuel / maximum).
  • Taux de 429 par rapport au total des requêtes.
  • Profondeur des retries, soit le nombre de tentatives par requête logique.
  • Time-to-first-audio, mesuré depuis votre application, pas depuis l’inférence du modèle. Voir la section sur la latence pour ce que TTFA inclut.

Un système sain garde l’utilisation bien en dessous de la saturation et ne voit des 429 qu’en cas de pics occasionnels. Surveiller ces signaux vous donne de la visibilité sur la pression de limitation de débit, bien avant que cela ne devienne un problème.

Quand passer à l’échelle au-delà de la limitation côté client

Les bonnes pratiques côté client sont très efficaces, mais la demande finira par dépasser leur capacité. Quand cela arrive, il est temps d’apporter des changements qui facilitent la gestion des coûts et des efforts.

Chacune des étapes suivantes vous donne de la capacité supplémentaire.

Commencez par passer le trafic interactif de HTTP à WebSocket. Si vos agents ou cas d’usage en direct utilisent HTTP, passer en WebSocket change la comptabilisation : seule la génération active compte. Pour les conversations, cela multiplie souvent la capacité sans changer d’offre, car les temps morts ne consomment plus de slots.

Si vos pics sont ponctuels mais que la charge moyenne reste dans le budget, un token bucket ou leaky bucket combiné à un pool borné aplatit les pics dans la moyenne.

Ensuite, choisissez le bon modèle. Une génération plus rapide occupe chaque slot moins longtemps, ce qui augmente le nombre de diffusions possibles avec une même limite de concurrence. Eleven Flash v2.5 est l’option la plus rapide pour le temps réel ; l’associer à un Clonage instantané de voix ou une voix par défaut évite la surcharge des Professional Voice Clones à chaque génération.

Ce n’est qu’après cela que vous devriez changer d’offre. Si la demande dépasse vraiment le budget alors que le client est bien optimisé, une offre supérieure augmente la limite de concurrence par modèle et la priorité dans la file. Comparez les offres sur la page des tarifs API.

Si vous avez besoin de limites supérieures à celles publiées, les offres Enterprise proposent des limites de concurrence plus élevées et personnalisées, ainsi que la priorité maximale dans la file. D’autres contrôles sont disponibles pour certains cas, comme le whitelistage IP (en preview Enterprise) et les modes sans rétention. Contactez votre responsable de compte pour augmenter les limites.

Résumé des points clés pour la limitation de débit IA

L’erreur principale est de traiter la limitation de débit voix IA comme un simple comptage de requêtes. Tout tourne autour de la gestion de la concurrence. Le chiffre qui compte, c’est le nombre de requêtes générant de l’audio au même instant et la durée d’occupation de chaque slot.

Construisez votre client autour de ce principe.

Limitez les requêtes en cours avec un pool borné, gérez l’admission avec un token ou leaky bucket, réessayez avec un backoff exponentiel limité et du jitter, respectez Retry-After, et coupez le circuit avant qu’une tempête de retries ne se forme.

Pour les systèmes multi-locataires, ajoutez des buckets par locataire, de l’équité pondérée, une marge de sécurité et du sharding pour l’isolation. Surveillez les en-têtes current-concurrent-requests et maximum-concurrent-requests et alertez sur la tendance d’utilisation, pas sur les échecs.

Si vous avez vraiment besoin de plus de capacité, suivez l’ordre : WebSockets et bon comportement client d’abord, puis le bon modèle, puis un changement d’offre, puis les limites Enterprise.

Créez des applications vocales avec ElevenAPI

Une limitation de débit IA robuste commence par le bon transport, le bon modèle et des en-têtes qui vous indiquent exactement où vous en êtes.

ElevenAPI propose des modèles à faible latence comme Eleven Flash v2.5, du streaming WebSocket en temps réel, Speech to Text et des API Text to Speech, ainsi que des en-têtes de concurrence par réponse pour créer des agents vocaux qui évoluent dans vos limites.

Combiné aux stratégies de limitation IA de cet article, vous offrirez des expériences vocales réactives tout en gardant des performances prévisibles (même sous charge).

Découvrez ElevenAPI pour voir tous les modèles en action, ou créez un compte pour commencer à utiliser ElevenLabs dès aujourd’hui.

FAQ sur la limitation de débit IA

Articles similaires

Créez avec l'audio IA de la plus haute qualité