Ir al contenido

Limitación de velocidad con IA para voz: concurrencia, colas y errores 429

Publicado
Última actualización

EscucharEscucha este artículo

La mayoría de equipos aplica la limitación de tasa de IA para voz igual que con otras API: limita las solicitudes por minuto, reintenta cuando el servidor pone límites y sigue adelante. En las cargas de trabajo de ElevenLabs, este modelo deja de funcionar con el primer pico de tráfico, porque el límite que realmente alcanzas es el de concurrencia, no el número de solicitudes.

Esta guía explica por qué la concurrencia es la restricción real y repasa los patrones del lado del cliente que te ayudan a mantenerte dentro del límite. Desde grupos de concurrencia limitada y una gestión adecuada de los errores 429 hasta la equidad entre tenants y los buckets de tokens y con fugas, te mostramos sistemas prácticos que puedes implementar. Hemos acompañado cada patrón de una implementación funcional en TypeScript que puedes adaptar.

Si creas agentes de voz, flujos de narración o cualquier otro sistema de producción basado en nuestros modelos y quieres escalarlo, esta guía es para ti.

Resumen

  • La limitación de tasa de IA para voz consiste en controlar la concurrencia, no en contar solicitudes por minuto.
  • Alcanzar el límite de tasa no rechaza el tráfico de inmediato. En su lugar, las solicitudes entran en una cola de prioridad que añade unos 50 ms.
  • Superar la capacidad incluso después de pasar por la cola genera un error HTTP 429.
  • Los WebSockets aumentan considerablemente la capacidad efectiva, ya que solo la generación activa cuenta para tu límite.
  • Los sistemas multi-tenant necesitan una capa adicional de equidad: buckets por tenant, colas justas ponderadas, margen de capacidad reservado y fragmentación entre claves para garantizar el aislamiento. 
  • Dos cabeceras de respuesta, current-concurrent-requests y maximum-concurrent-requests, te indican tu situación respecto a la limitación de tasa de IA.

Por qué el límite es la concurrencia y no las solicitudes por minuto

La concurrencia es el número de solicitudes en curso en un mismo momento. Las solicitudes por minuto son el rendimiento durante un intervalo. Entender esta diferencia es importante porque cambia qué mecanismo te permite mantenerte dentro de tu límite.

Al utilizar uno de los modelos de ElevenLabs, la carga de trabajo del servidor aumenta con el número de usuarios simultáneos. La generación de audio ocupa una plaza durante toda la generación, y esa duración varía según la longitud de la entrada, el modelo y la carga.

Un límite de solicitudes por minuto no te dice cuántas plazas están ocupadas ahora mismo, que es lo único que el servidor mide.

Límites por plan y familia de modelos

Tu presupuesto de concurrencia no es una única cifra. Los límites de concurrencia varían según el plan y la familia de modelos. Por ejemplo, Voz a Texto tiene un límite superior al de Texto a Voz, porque las solicitudes de transcripción suelen durar menos y el sistema puede procesar más al mismo tiempo.

El límite se aplica por familia de modelos. Si usas Flash para agentes y Multilingual v2 para narración, trabajas con dos presupuestos independientes al mismo tiempo. Las cifras actuales por plan y la sección de concurrencia están documentadas en la página de modelos.

¿Qué ocurre cuando alcanzas el límite de concurrencia?

Alcanzar el límite de concurrencia no rechaza el tráfico de inmediato. El sistema se degrada de forma controlada mediante una cola de prioridad y solo pasa al rechazo total si sigues superando la capacidad total del límite de tasa.

Mientras estés por debajo de tu límite, las solicitudes se ejecutan de inmediato. Cuando lo alcanzas, las siguientes solicitudes entran en una cola ordenada según el nivel de prioridad de tu plan. La cola suele añadir unos 50 ms de latencia, por lo que una superación breve apenas es perceptible para usuarios.

Si el sistema sigue sin capacidad después de pasar por la cola, recibes un HTTP 429. Es la señal para reducir el ritmo en lugar de reintentar de inmediato. El nivel de prioridad de la tabla determina cómo se ordenan tus solicitudes en cola respecto a otro tráfico; los planes superiores vacían la cola antes.

HTTP frente a WebSocket: cómo cuenta cada uno para tu límite

El transporte que elijas influye directamente en la limitación de tasa y el presupuesto. Una misma conversación entrante puede consumir cantidades muy distintas de tu presupuesto de concurrencia según se ejecute mediante HTTP o WebSocket.

Con HTTP, cada solicitud cuenta individualmente para tu límite de concurrencia durante toda su duración. Con WebSocket, solo cuenta el tiempo en que el modelo genera audio activamente. Un WebSocket abierto pero inactivo prácticamente no cuenta.

En un agente de voz, hay largos periodos de conversación en los que nadie habla y el modelo no genera nada. Con HTTP, ocuparías una plaza durante la duración de la solicitud en cada turno. Con WebSocket, la plaza se consume solo durante los milisegundos de generación activa, por lo que una plaza de concurrencia se comparte entre muchas conversaciones. 

Consulta la guía de WebSocket para TTS en tiempo real para conocer los detalles del protocolo. Para tráfico interactivo, WebSockets es la opción predeterminada adecuada.

Por qué ~5 de concurrencia pueden admitir ~100 retransmisiones

Las matemáticas de la concurrencia resultan poco intuitivas hasta que tienes en cuenta el tiempo de reproducción. La generación es mucho más rápida que la reproducción y una plaza solo está ocupada activamente cuando se genera audio. Esa diferencia permite que un presupuesto pequeño atienda a una audiencia amplia.

Una solicitud que tarda una fracción de segundo en generar produce varios segundos de audio que el oyente reproduce después y, durante la reproducción, la plaza se libera y queda disponible para otros oyentes.

Como regla general, un límite de concurrencia de 5 puede admitir unas 100 retransmisiones de audio simultáneas. La cifra exacta depende de la voz, el ritmo del habla y los silencios entre intervenciones.

Las cabeceras que indican tu situación

No necesitas deducir tu posición respecto a tu límite. Cada respuesta incluye dos cifras que puedes usar para medir el margen de capacidad, en lugar de limitarte a estimarlo.

Busca estas dos cabeceras:

  • current-concurrent-requests: ¿cuántas solicitudes están en curso ahora mismo?
  • maximum-concurrent-requests: tu límite para esa familia de modelos.

En conjunto, estas cabeceras ofrecen una visión en tiempo real de tu uso actual y la capacidad disponible. No deberías tener que hacer conjeturas antes de encontrarte con límites de tasa de IA.

Estrategias del lado del cliente para limitar la tasa de IA

Hay cuatro mecanismos básicos que cubren casi todos los escenarios de limitación de tasa de IA:

  • Un bucket de tokens: si hay tokens disponibles, permite que las solicitudes continúen. La capacidad se repone con el tiempo, lo que permite gestionar picos breves sin alcanzar los límites de tasa.
  • Un bucket con fugas: intenta regular el tráfico entrante a una tasa de salida fija para evitar que los picos repentinos saturen tus sistemas posteriores.
  • Un grupo de concurrencia limitada: limita el número total de solicitudes que pueden estar activas simultáneamente, para que nunca superes los límites de solicitudes concurrentes.
  • Backoff exponencial con jitter completo: aumenta progresivamente el tiempo entre solicitudes fallidas para evitar que todos los clientes reintenten a la vez.

Las secciones siguientes muestran cómo crear estos mecanismos uno a uno, comenzando por el que se corresponde más directamente con el límite de concurrencia.

Todos los fragmentos siguientes asumen un único cliente, inicializado una vez:

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

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

Concurrencia limitada: el mecanismo que se ajusta al límite

Puesto que el servidor mide la concurrencia, el control más directo del cliente es un grupo de workers limitado que establece cuántas solicitudes puedes tener en curso a la vez. Define el límite un poco por debajo del de tu plan para dejar margen para la cola de prioridad y el 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

Bucket de tokens: permite picos y limita la media

Un bucket de tokens almacena hasta capacity tokens y se rellena a refillRate tokens por segundo. Cada solicitud consume un token, por lo que el bucket permite picos breves de hasta su tamaño, al tiempo que limita la tasa a largo plazo. 

Es la herramienta adecuada para suavizar el momento en que llega de repente una cola de trabajo, para que no envíes todo a la vez y dispares la concurrencia.

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;
  }
}

Bucket con fugas: aplica una salida constante

En algunos casos no quieres tolerar picos en absoluto. Un bucket con fugas admite trabajo a una tasa fija y constante, independientemente de lo irregular que sea la entrada. Es la mejor opción cuando el sistema posterior prefiere una carga fluida y predecible a picos ocasionales.

Por ejemplo, cuando te mantienes deliberadamente muy por debajo de un presupuesto de concurrencia pequeño que compartes con otros servicios.

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 exponencial con jitter completo

Cuando una solicitud falla con un estado que permite reintentos, reintentar de inmediato empeora las cosas. El backoff separa los reintentos y el jitter completo aleatoriza cada espera en todo el intervalo, lo que evita que muchos clientes reintenten de forma sincronizada y reproduzcan el mismo pico que causó el fallo.

El siguiente fragmento hace referencia a RetryableError, una clase pequeña que incluye el estado fallido y cualquier valor de Retry-After. Se define en la sección sobre gestión adecuada de errores 429 más abajo.

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));
    }
  }
}

Gestión adecuada de errores 429: qué hacer al alcanzar el límite

Un 429 significa que superaste la capacidad incluso después de la cola de prioridad, así que la respuesta correcta es reducir el ritmo en vez de reintentar con más insistencia. Hay cuatro formas de gestionarlo, que se resumen en cuatro estrategias:

  • Detección
  • Respetar Retry-After
  • Mostrar la contrapresión
  • Evitar tormentas de reintentos con un disyuntor

Veámoslas con más detalle.

La primera es la detección. Trata HTTP 429 (y los estados transitorios 500, 502, 503 y 504) como reintentables, y 400, 401, 403 y 422 como no reintentables; reintentar una solicitud malformada o no autorizada nunca funcionará y solo desperdicia una plaza.

La segunda es respetar Retry-After. Si la respuesta incluye esa cabecera, respétala exactamente en lugar de calcular tu propia espera. El servidor te indica cuándo espera tener capacidad y lo sabe mejor que tu fórmula exponencial. Recurre al backoff con jitter solo cuando la cabecera no esté presente.

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}`);
}

El tercer aspecto es mostrar la contrapresión. No permitas que los reintentos se acumulen de forma invisible. Si la profundidad de tu cola o el margen de capacidad medido indica que no puedes atender pronto una nueva solicitud, recházala en el perímetro con una señal clara para quien la realiza, en lugar de aceptar trabajo que no puedes hacer.

El cuarto es evitar tormentas de reintentos con un disyuntor. Si los fallos superan un umbral, abre el circuito y falla rápidamente durante un periodo de enfriamiento, en lugar de enviar solicitudes que esperas que fallen. Después del periodo, envía unas cuantas solicitudes de prueba; si tienen éxito, cierra el circuito.

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();
    }
  }
}

Patrones de cuota multi-tenant para limitar la tasa de IA

Todo lo anterior presupone una sola aplicación con un único presupuesto. Al crear un SaaS sobre ElevenLabs, el problema cambia: tu presupuesto de concurrencia se comparte entre todos tus clientes y un tenant que ejecuta un trabajo por lotes no debería dejar sin recursos al tráfico en directo de todos los demás. Necesitas una capa de equidad entre tus tenants y el único límite ascendente.

La base son los buckets de tokens por tenant. Asigna a cada tenant su propio bucket, dimensionado según lo que le corresponde, y admite una solicitud solo cuando lo permitan tanto el bucket del tenant como un limitador global.

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();
  }
}

Los buckets mantienen a raya a cada tenant, pero no deciden quién gana cuando los tenants compiten por el limitador global. Para eso, usa colas justas ponderadas. 

No atiendas por orden de llegada, porque un pico de un tenant puede monopolizar las plazas. Mantén una cola por tenant y distribuye en proporción al peso de cada uno, para que un tenant de pago reciba una parte mayor de la capacidad disputada que uno gratuito.

Además de la equidad, reserva margen de capacidad. No dejes nunca que el tráfico normal consuma el 100 % del límite de concurrencia. Reserva una fracción, por ejemplo el 15-20 %, como búfer para solicitudes interactivas sensibles a la latencia y para la cola de prioridad.

Cuando la equidad dentro de un único presupuesto deja de ser suficiente, fragmenta entre workspaces o claves. Un único presupuesto de concurrencia acaba convirtiéndose en el cuello de botella, por muy equitativamente que lo repartas. 

En ese momento, separa las cargas de trabajo en workspaces o claves de API distintos con sus propios presupuestos: por ejemplo, una clave para el tráfico de agentes en tiempo real y otra para la narración en segundo plano, para que una acumulación de narración no afecte a la capacidad de los agentes.

Los workspaces también permiten aplicar restricciones de alcance, cuotas de créditos y controles por clave, descritos en la documentación de autenticación.

Supervisa tu uso de concurrencia

Nada de esto se puede ajustar sin medir; no puedes gestionar un margen de capacidad que no mides. Registra current-concurrent-requests y maximum-concurrent-requests en cada respuesta, etiquetados por familia de modelos, y envía la ratio de uso como una métrica gauge.

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);
  }
}

Cuatro señales que debes monitorizar:

  • Uso (actual / máximo).
  • Tasa de 429 como proporción del total de solicitudes.
  • Profundidad de reintentos, el número de intentos por solicitud lógica.
  • Tiempo hasta el primer audio, medido desde tu aplicación y no a partir de las cifras de inferencia del modelo. Consulta la explicación de la latencia para saber qué incluye TTFA.

Un sistema saludable mantendrá el uso cómodamente por debajo de la saturación y solo registrará 429 en picos ocasionales. Monitorizar estas señales te da visibilidad sobre la presión de los límites de tasa mucho antes de que se convierta en un problema de interrupción del servicio.

Cuándo escalar más allá de la limitación de tasa del lado del cliente

Los patrones del lado del cliente pueden resolver mucho, pero la demanda sostenida acabará superándolos. Cuando ocurra, es momento de hacer cambios que ayuden tanto con el coste como con el esfuerzo. 

Cada uno de los siguientes pasos te aporta capacidad adicional.

Empieza cambiando de HTTP a WebSockets para el tráfico interactivo. Si tus agentes o casos de uso en directo funcionan con HTTP, pasar a WebSocket cambia el cómputo para que solo cuente la generación activa. En las cargas de trabajo conversacionales, esto suele multiplicar la capacidad efectiva sin cambiar de plan, porque el tiempo de conversación inactivo deja de consumir plazas.

Si tus picos son irregulares pero tu carga media se ajusta al presupuesto, un bucket de tokens o con fugas junto con un grupo limitado aplana los picos hasta la media.

Después, elige el modelo adecuado. Una generación más rápida mantiene cada plaza ocupada durante menos tiempo, lo que aumenta el número de retransmisiones que puede sostener un límite de concurrencia fijo. Eleven Flash v2.5 es la opción de menor latencia para trabajo en tiempo real; combinarla con un Clon de Voz Instantáneo o una voz predeterminada evita la sobrecarga por generación de los Clones de Voz Profesionales.

Solo después deberías mejorar el plan. Cuando tu demanda sostenida supere realmente el presupuesto después de que el cliente se comporte correctamente, un plan superior aumenta tanto el límite de concurrencia por modelo como la prioridad de tu cola. Compara los niveles en la página de precios de la API.

Si necesitas límites superiores a los publicados, los planes Enterprise ofrecen límites de concurrencia más altos y personalizados, así como la máxima prioridad en la cola. Hay controles adicionales disponibles para casos de uso que cumplan los requisitos, como listas de permitidos de IP (en vista previa para Enterprise) y modos sin retención. Ponte en contacto con tu gestor de cuenta para aumentar los límites.

Resumen de lo que debes recordar sobre la limitación de tasa de IA 

El error fundamental es tratar la limitación de tasa de IA para voz como un recuento de solicitudes. Aquí todo gira en torno al control de concurrencia. La cifra que determina si tendrás éxito es cuántas solicitudes generan audio en el mismo instante y cuánto tiempo mantiene cada una su plaza.

Diseña el cliente en torno a ese hecho. 

Limita las solicitudes en curso con un grupo limitado, regula la admisión con un bucket de tokens o con fugas, reintenta con backoff exponencial limitado y jitter completo, respeta Retry-After y abre el circuito antes de que se forme una tormenta de reintentos. 

Para sistemas multi-tenant, añade buckets por tenant, equidad ponderada, margen de capacidad reservado y fragmentación para lograr aislamiento. Vigila las cabeceras current-concurrent-requests y maximum-concurrent-requests y alerta sobre la tendencia de uso, no sobre los fallos. 

Cuando realmente necesites más capacidad, sigue la lista en orden: primero WebSockets y un mejor comportamiento del cliente; después, el modelo adecuado; luego, una mejora de plan; y finalmente, los límites Enterprise.

Crea aplicaciones de voz con ElevenAPI

La limitación de tasa de IA para producción empieza con el transporte adecuado, el modelo adecuado y cabeceras que te indican exactamente tu situación.

ElevenAPI ofrece modelos de baja latencia como Eleven Flash v2.5, streaming por WebSocket en tiempo real, Voz a Texto y API de Texto a Voz, además de cabeceras de concurrencia por respuesta que te permiten crear agentes de voz que escalan dentro de tus límites. 

Combinadas con las estrategias de limitación de tasa de IA de este artículo, te permitirán ofrecer experiencias de voz ágiles y mantener un rendimiento predecible, incluso con carga. 

Explora ElevenAPI para ver toda la gama de modelos en acción o crea una cuenta para empezar a crear con ElevenLabs hoy.

Preguntas frecuentes sobre la limitación de tasa de IA

Artículos relacionados

Crea con el audio IA de la más alta calidad