Pular para o conteúdo

Limitação de taxa com IA para voz: Concorrência, filas e erros 429

Publicado

OuvirOuça este artigo

A maioria das equipes aplica limitação de taxa com IA para voz do mesmo jeito que faz com outras APIs: limita as requisições por minuto, tenta novamente quando o servidor recusa e segue em frente. Para workloads na ElevenLabs, esse modelo falha logo no primeiro pico de tráfego, porque o limite real é de concorrência, não de quantidade de requisições.

Este guia explica por que a concorrência é o verdadeiro limitador e mostra padrões do lado do cliente para manter o uso dentro desse limite. De pools de concorrência controlada e tratamento elegante de erros 429 até justiça em sistemas multiusuário e buckets de tokens ou leaky buckets, trazemos soluções práticas que você pode implementar. Cada padrão vem acompanhado de um exemplo funcional em TypeScript para você adaptar.

Se você cria agentes de voz, pipelines de narração ou qualquer outro sistema de produção usando nossos modelos e quer escalar, este guia é para você.

Resumo rápido

  • A limitação de taxa com IA para voz é controle de concorrência, não contagem de requisições por minuto.
  • Ao atingir o limite de taxa, o tráfego não é rejeitado imediatamente. As requisições entram em uma fila de prioridade que adiciona cerca de 50ms.
  • Se passar da capacidade mesmo após a fila, será gerado um erro HTTP 429.
  • WebSockets aumentam bastante a capacidade efetiva, pois só a geração ativa conta para o seu limite.
  • Sistemas multiusuário precisam de uma camada extra de justiça: buckets por usuário, filas ponderadas, reserva de capacidade e divisão por chaves para isolar o uso.
  • Dois headers de resposta, current-concurrent-requests e maximum-concurrent-requests, mostram sua situação em relação à limitação de taxa com IA.

Por que o limite é de concorrência, não de requisições por minuto

Concorrência é o número de requisições em andamento ao mesmo tempo. Requisições por minuto é o volume ao longo de um período. Entender essa diferença é importante porque muda qual ajuste mantém você dentro do limite.

Ao usar um dos modelos da ElevenLabs, a carga do servidor cresce conforme o número de usuários simultâneos. A geração de áudio ocupa um slot durante todo o tempo de geração, e esse tempo varia conforme o tamanho do texto, o modelo e a carga.

O limite por minuto não diz quantos slots estão ocupados agora, que é o que o servidor realmente controla.

Os limites por plano e por família de modelos

Seu orçamento de concorrência não é um número único. Os limites de concorrência variam conforme o plano e a família de modelos. Por exemplo, Speech to Text tem um limite maior em relação ao Transformar Texto em Áudio, porque pedidos de transcrição costumam ser mais curtos e o sistema consegue absorver mais deles ao mesmo tempo.

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

O limite é por família de modelo. Se você usa Flash para agentes e Multilingual v2 para narração, está usando dois orçamentos separados ao mesmo tempo. Os números atuais por plano e a seção de concorrência estão documentados na página de modelos.

O que acontece ao atingir o limite de concorrência?

Chegar ao limite de concorrência não rejeita o tráfego imediatamente. O sistema faz uma degradação suave usando uma fila de prioridade, só rejeitando totalmente quando você ultrapassa a capacidade máxima.

Enquanto estiver abaixo do limite, as requisições são processadas na hora. Ao atingir o limite, as próximas entram em uma fila ordenada pelo nível de prioridade do seu plano. A fila normalmente adiciona cerca de 50ms de latência, então um pequeno excesso quase não é percebido pelos usuários.

Se o sistema continuar acima da capacidade mesmo após a fila, você recebe um HTTP 429. Esse é o sinal para desacelerar, não para tentar novamente imediatamente. O nível de prioridade na tabela define a ordem das suas requisições na fila em relação ao restante do tráfego; planos mais altos liberam a fila mais rápido.

HTTP vs. WebSocket: Como cada um conta para o seu limite

O transporte escolhido influencia diretamente a limitação de taxa e o orçamento. A mesma conversa pode consumir quantidades bem diferentes do seu orçamento de concorrência dependendo se roda via HTTP ou WebSocket.

No HTTP, cada requisição conta individualmente para o limite de concorrência durante todo o tempo de execução. No WebSocket, só o tempo em que o modelo está gerando áudio conta. Um WebSocket aberto, mas ocioso, quase não conta.

Para um agente de voz, uma conversa tem longos períodos sem ninguém falando e sem geração de áudio. Com HTTP, você ocupa um slot durante toda a requisição em cada turno. Com WebSocket, o slot é usado só nos milissegundos de geração ativa, então um slot pode ser compartilhado entre várias conversas.

Veja o guia de WebSocket para TTS em tempo real para detalhes do protocolo. Para tráfego interativo, WebSockets são a melhor escolha.

Por que ~5 de concorrência suportam ~100 transmissões

A matemática da concorrência é contraintuitiva até considerar o tempo de reprodução. A geração é muito mais rápida que a reprodução, e um slot só fica ocupado enquanto o áudio está sendo gerado. Essa diferença é o que permite que um orçamento pequeno atenda um público grande.

Uma requisição que leva frações de segundo para gerar produz vários segundos de áudio, que o ouvinte vai escutar depois, e durante a reprodução o slot já está liberado para outros ouvintes.

Como regra geral, um limite de concorrência de 5 pode suportar cerca de 100 transmissões de áudio simultâneas. O número exato depende da voz, do ritmo da fala e de quanto silêncio há entre as falas.

Os headers que mostram sua situação

Você não precisa adivinhar sua posição em relação ao limite. Toda resposta traz dois números que você pode usar para medir a folga em vez de apenas estimar.

Fique de olho nestes dois headers:

  • requisições simultâneas atuais: quantas requisições estão em andamento agora?
  • máximo de requisições simultâneas: seu limite para aquela família de modelo.

Juntos, esses headers fornecem uma visão em tempo real do seu uso atual e da capacidade disponível. Você não precisa adivinhar antes de encontrar os limites de taxa da IA.

Estratégias do lado do cliente para limitação de taxa com IA

Quatro mecanismos cobrem quase todos os cenários de limitação de taxa com IA:

  • Token bucket: Se houver tokens disponíveis, permite que as requisições prossigam. A capacidade é reabastecida com o tempo, permitindo lidar com picos curtos sem atingir o limite.
  • Leaky bucket: Tenta suavizar o tráfego de entrada para uma taxa fixa de saída, evitando que picos repentinos sobrecarreguem seus sistemas.
  • Pool de concorrência controlada: Limita o número total de requisições ativas ao mesmo tempo, garantindo que você nunca ultrapasse o limite de concorrência.
  • Backoff exponencial com jitter total: Aumenta progressivamente o tempo entre tentativas após falhas, evitando que todos os clientes tentem novamente ao mesmo tempo.

As seções abaixo mostram como construir cada um deles, começando pelo que mais se relaciona com o limite de concorrência.

Todos os exemplos abaixo assumem um único cliente, inicializado uma vez:

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

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

Concorrência controlada: o mecanismo que corresponde ao limite

Como o servidor controla a concorrência, o controle mais direto do cliente é um pool de workers limitado, que restringe quantas requisições você tem em andamento ao mesmo tempo. Defina o limite um pouco abaixo do limite do seu plano para deixar espaço para a fila de prioridade e para 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: permite picos, limita a média

Um token bucket armazena até a capacidade máxima de tokens e reabastece a uma taxa de refillRate tokens por segundo. Cada requisição consome um token, então o bucket permite picos curtos até seu tamanho, mas limita a taxa média no longo prazo.

É a ferramenta ideal para suavizar o momento em que chega uma fila de trabalho de uma vez, evitando disparar tudo ao mesmo tempo e gerar picos de concorrência.

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: mantém o fluxo constante

Em alguns casos, você não quer tolerar picos. Um leaky bucket libera trabalho a uma taxa fixa e constante, independentemente de como o input chega. É melhor quando o sistema de destino prefere uma carga previsível e suave em vez de picos ocasionais.

Por exemplo, quando você faz questão de ficar bem abaixo de um orçamento pequeno de concorrência compartilhado com outros serviços.

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 com jitter total

Quando uma requisição falha com um status que permite nova tentativa, tentar novamente na hora só piora. O backoff espaça as tentativas, e o jitter total randomiza cada atraso dentro do intervalo, evitando que vários clientes tentem juntos e recriem o pico que causou a falha.

O exemplo abaixo faz referência à RetryableError, uma pequena classe que carrega o status da falha e qualquer valor de Retry-After. Ela está definida na seção de tratamento elegante de erros 429 abaixo.

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

Tratamento elegante de erros 429: O que fazer ao atingir o limite

Um 429 significa que você estava acima da capacidade mesmo após a fila de prioridade, então a resposta correta é desacelerar, não tentar mais forte. Existem quatro formas de lidar com isso. O segredo está em quatro estratégias:

  • Detecção
  • Respeitar o Retry-After
  • Sinalizar pressão de retorno
  • Evitar tempestades de tentativas com circuit breaker

Vamos detalhar cada uma delas.

A primeira é detecção. Trate HTTP 429 (e 500, 502, 503 e 504 temporários) como passíveis de nova tentativa, e 400, 401, 403 e 422 como não passíveis; tentar novamente uma requisição malformada ou não autorizada nunca vai funcionar e só desperdiça slot.

A segunda é respeitar o Retry-After. Se a resposta trouxer esse header, siga exatamente o valor em vez de calcular seu próprio atraso. O servidor sabe melhor que sua fórmula exponencial quando espera ter capacidade. Só use backoff com jitter se o header não estiver 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}`);
}

A terceira é sinalizar pressão de retorno. Não deixe tentativas acumularem de forma invisível. Se a profundidade da fila ou a folga medida indicar que você não pode atender uma nova requisição logo, rejeite já com um sinal claro para o chamador, em vez de aceitar trabalho que não pode ser feito.

A quarta é evitar tempestades de tentativas com circuit breaker. Se as falhas passarem de um limite, abra o circuito e falhe rapidamente por um tempo de resfriamento, em vez de enviar requisições que já sabe que vão falhar. Depois desse tempo, envie algumas requisições de teste; se der certo, feche o 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();
    }
  }
}

Padrões de quota multiusuário para limitação de taxa com IA

Tudo até aqui assume um único aplicativo usando um único orçamento. Ao construir um SaaS sobre a ElevenLabs, o problema muda: seu orçamento de concorrência é compartilhado entre todos os seus clientes, e um usuário rodando um job em lote não pode prejudicar o tráfego ao vivo dos outros. Você precisa de uma camada de justiça entre seus usuários e o limite único do upstream.

A base são buckets de tokens por usuário. Dê a cada usuário seu próprio bucket, dimensionado conforme o direito dele, e só aceite uma requisição quando tanto o bucket do usuário quanto o limitador global permitirem.

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

Os buckets garantem que nenhum usuário abuse, mas não decidem quem ganha quando vários disputam o limitador global. Para isso, use filas ponderadas.

Não use ordem de chegada, pois isso permite que um pico de um usuário monopolize os slots. Mantenha uma fila por usuário e distribua conforme o peso de cada um, assim clientes pagantes têm mais capacidade que gratuitos.

Além da justiça, reserve uma folga. Nunca deixe o tráfego normal consumir 100% do limite de concorrência. Guarde uma fração, tipo 15-20%, como buffer para requisições interativas sensíveis à latência e para a fila de prioridade.

Quando a justiça dentro de um orçamento só não for suficiente, divida por workspaces ou chaves. Um único orçamento de concorrência acaba virando gargalo, não importa o quão justo seja o uso.

Nesse ponto, separe workloads em workspaces ou chaves de API diferentes, cada uma com seu próprio orçamento: por exemplo, uma chave para tráfego de agentes em tempo real e outra para narração em segundo plano, assim o backlog de narração não afeta a capacidade dos agentes.

Workspaces também permitem aplicar restrição de escopo, cotas de crédito e controles por chave, descritos na documentação de autenticação.

Monitorando o uso da sua concorrência

Nada disso é ajustável sem medição; você não pode gerenciar folga se não medir. Registre current-concurrent-requests e maximum-concurrent-requests em cada resposta, marcados pela família de modelo, e exponha a razão de uso como um 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);
  }
}

Quatro sinais para acompanhar:

  • Utilização (atual / máximo).
  • Taxa de 429 em relação ao total de requisições.
  • Profundidade de tentativas, ou seja, número de tentativas por requisição lógica.
  • Tempo até o primeiro áudio, medido no seu aplicativo, não pelo tempo de inferência do modelo. Veja a explicação sobre latência para entender o que inclui o TTFA.

Um sistema saudável mantém a utilização bem abaixo da saturação e só vê erros 429 em picos ocasionais. Monitorar esses sinais dá visibilidade sobre pressão de limitação de taxa, muito antes de virar um problema de indisponibilidade.

Quando escalar além da limitação de taxa do lado do cliente

Os padrões do lado do cliente ajudam bastante, mas a demanda constante vai crescer além deles. Quando isso acontecer, é hora de fazer mudanças que ajudam tanto no custo quanto no esforço.

Cada um dos passos abaixo aumenta sua capacidade.

Comece trocando HTTP por WebSockets para tráfego interativo. Se seus agentes ou casos ao vivo usam HTTP, migrar para WebSocket muda a contagem para considerar só a geração ativa. Para workloads de conversação, isso normalmente multiplica a capacidade efetiva sem mudar de plano, pois o tempo ocioso não consome slots.

Se seus picos são altos mas a média cabe no orçamento, um token bucket ou leaky bucket junto com um pool limitado suaviza os picos na média.

Depois, escolha o modelo certo. Geração mais rápida ocupa cada slot por menos tempo, aumentando o número de transmissões que um limite fixo de concorrência suporta. O Eleven Flash v2.5 é a opção de menor latência para uso em tempo real; combinando com um Clonagem Instantânea de Voz ou uma voz padrão, você evita o custo extra de clones profissionais.

Só depois disso pense em atualizar o plano. Quando a demanda constante realmente ultrapassar o orçamento, mesmo com o cliente bem ajustado, um plano mais alto aumenta tanto o limite de concorrência por modelo quanto a prioridade da sua fila. Compare os planos na página de preços da API.

Se precisar de limites além do publicado, os planos Enterprise oferecem limites de concorrência maiores e personalizados e a maior prioridade de fila. Há controles extras para casos elegíveis, como whitelist de IP (em preview Enterprise) e modos sem retenção. Fale com seu gerente de conta para aumentar limites.

Resumo: o que lembrar sobre limitação de taxa com IA

O erro mais comum é tratar a limitação de taxa de voz IA como contagem de requisições. Tudo aqui é sobre controle de concorrência. O número que define seu sucesso é quantas requisições estão gerando áudio ao mesmo tempo e quanto tempo cada uma ocupa o slot.

Construa o cliente com base nisso.

Limite requisições em andamento com um pool controlado, regule a entrada com token bucket ou leaky bucket, tente novamente com backoff exponencial e jitter, respeite Retry-After e abra o circuito antes de formar uma tempestade de tentativas.

Para sistemas multiusuário, adicione buckets por usuário, justiça ponderada, reserva de folga e divisão para isolar o uso. Monitore os headers current-concurrent-requests e maximum-concurrent-requests e alerte pela tendência de uso, não só pelas falhas.

Quando realmente precisar de mais capacidade, siga a ordem: WebSockets e melhor comportamento do cliente primeiro, depois o modelo certo, depois upgrade de plano e, por fim, limites Enterprise.

Crie aplicações de voz com ElevenAPI

Limitação de taxa com IA em nível de produção começa com o transporte certo, o modelo certo e headers que mostram exatamente sua situação.

O ElevenAPI oferece modelos de baixa latência como o Eleven Flash v2.5, streaming em tempo real via WebSocket, Speech to Text e APIs de Transformar Texto em Áudio, além de headers de concorrência por resposta que permitem criar agentes de voz que escalam dentro do seu limite.

Combinando com as estratégias de limitação de taxa com IA deste artigo, você entrega experiências de voz responsivas mantendo desempenho previsível (mesmo sob carga).

Explore o ElevenAPI para ver todos os modelos em ação ou crie uma conta para começar a usar a ElevenLabs hoje.

Perguntas frequentes sobre limitação de taxa com IA

Artigos relacionados

Crie com o áudio de IA da mais alta qualidade