Pomiń

Ograniczanie AI dla głosu: współbieżność, kolejki i 429

Opublikowano

PosłuchajPosłuchaj tego artykułu

Większość zespołów podchodzi do ograniczania AI dla głosu tak samo jak do innych API: limituje liczbę żądań na minutę, ponawia próbę przy odmowie serwera i idzie dalej. W ElevenLabs to podejście nie działa już przy pierwszym większym ruchu, bo limit dotyczy współbieżności, a nie liczby żądań.

W tym przewodniku wyjaśniamy, dlaczego to współbieżność jest prawdziwym ograniczeniem i pokazujemy wzorce po stronie klienta, które pomagają się w nim zmieścić. Od pul z limitem współbieżności i obsługi 429 po sprawiedliwość dla wielu najemców oraz token i leaky bucket – mamy gotowe rozwiązania do wdrożenia. Każdy wzorzec pokazujemy na działającym przykładzie w TypeScript, który możesz dostosować.

Jeśli budujesz voice agenty, pipeline’y narracji lub inne systemy produkcyjne na naszych modelach i chcesz skalować – ten poradnik jest dla ciebie.

TL;DR

  • Ograniczanie AI dla głosu to kontrola współbieżności, nie liczenie żądań na minutę.
  • Po osiągnięciu limitu żądania nie są odrzucane od razu. Najpierw trafiają do kolejki priorytetowej, co dodaje ok. 50 ms.
  • Przekroczenie pojemności nawet po zakolejkowaniu skutkuje błędem HTTP 429.
  • WebSocket mocno zwiększa efektywną pojemność, bo tylko aktywne generowanie liczy się do limitu.
  • Systemy dla wielu najemców potrzebują dodatkowej warstwy sprawiedliwości: osobne bucket’y, ważone kolejki, rezerwa i sharding po kluczach dla izolacji.
  • Dwa nagłówki w odpowiedzi – current-concurrent-requests i maximum-concurrent-requests – pokazują, gdzie jesteś względem limitu.

Dlaczego limitem jest współbieżność, a nie żądania na minutę

Współbieżność to liczba żądań obsługiwanych jednocześnie. Żądania na minutę to przepustowość w danym oknie. Ta różnica jest kluczowa, bo zmienia, co naprawdę trzyma cię w limicie.

Korzystając z modeli ElevenLabs, obciążenie serwera rośnie wraz z liczbą równoczesnych użytkowników. Generowanie audio blokuje slot na czas generowania, a ten zależy od długości wejścia, modelu i obciążenia.

Limit żądań na minutę nie mówi nic o tym, ile slotów jest zajętych w danym momencie – a tylko to mierzy serwer.

Limity według planu i rodziny modeli

Twój budżet współbieżności to nie jedna liczba. Limity różnią się w zależności od planu i rodziny modeli. Na przykład Speech to Text ma wyższy limit niż Text to Speech, bo transkrypcje są zwykle krótsze i system może obsłużyć ich więcej naraz.

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

Limit dotyczy rodziny modeli. Jeśli używasz Flash dla agentów i Multilingual v2 do narracji, masz dwa osobne budżety. Aktualne limity i sekcję współbieżności znajdziesz na stronie modeli.

Co się dzieje po osiągnięciu limitu współbieżności?

Osiągnięcie limitu nie odrzuca żądań od razu. System najpierw kieruje je do kolejki priorytetowej, a dopiero potem – jeśli nadal jest za dużo – całkowicie je odrzuca.

Póki jesteś poniżej limitu, żądania są obsługiwane od razu. Po osiągnięciu limitu kolejne trafiają do kolejki według priorytetu twojego planu. Kolejka zwykle dodaje ok. 50 ms opóźnienia, więc krótkie przekroczenia są praktycznie niewidoczne.

Jeśli po zakolejkowaniu system nadal jest przeciążony, dostajesz HTTP 429. To sygnał, żeby zwolnić, a nie ponawiać od razu. Poziom priorytetu w tabeli decyduje o kolejności twoich żądań względem innych – wyższe plany szybciej czyszczą kolejkę.

HTTP vs WebSocket: jak każde z nich liczy się do limitu

Wybrany sposób komunikacji bezpośrednio wpływa na limity i budżet. Ta sama rozmowa może zużyć zupełnie inną część budżetu współbieżności w zależności od tego, czy działa przez HTTP, czy WebSocket.

Przez HTTP każde żądanie liczy się osobno do limitu na cały czas trwania. Przez WebSocket tylko czas aktywnego generowania audio się liczy – otwarty, ale bezczynny WebSocket prawie nie wpływa na limit.

Dla voice agenta rozmowa ma długie przerwy, gdy nikt nie mówi i model nic nie generuje. W HTTP blokujesz slot na cały czas żądania. W WebSocket slot jest zajęty tylko przez milisekundy aktywnego generowania, więc jeden slot obsługuje wiele rozmów na zmianę.

Zobacz przewodnik po TTS WebSocket w czasie rzeczywistym po szczegóły protokołu. Dla ruchu interaktywnego WebSocket to domyślny wybór.

Dlaczego ~5 współbieżności obsłuży ~100 transmisji

Matematyka współbieżności wydaje się dziwna, dopóki nie uwzględnisz czasu odtwarzania. Generowanie jest dużo szybsze niż odtwarzanie, a slot jest zajęty tylko podczas generowania. Ta różnica pozwala małemu budżetowi obsłużyć dużą widownię.

Żądanie, które generuje się ułamek sekundy, daje kilka sekund audio do odtworzenia – w tym czasie slot jest już wolny dla innych.

W uproszczeniu: limit 5 współbieżności pozwala na ok. 100 jednoczesnych transmisji audio. Dokładna liczba zależy od głosu, tempa mowy i ilości ciszy między wypowiedziami.

Nagłówki, które pokazują twoją sytuację

Nie musisz zgadywać, gdzie jesteś względem limitu. Każda odpowiedź zawiera dwie liczby, które pozwalają mierzyć zapas zamiast szacować.

Zwróć uwagę na te dwa nagłówki:

  • bieżące jednoczesne żądania: ile żądań jest teraz obsługiwanych?
  • maksymalna liczba jednoczesnych żądań: twój limit dla tej rodziny modeli.

Razem te nagłówki dają podgląd na żywo twojego użycia i dostępnej pojemności. Nie musisz zgadywać, zanim trafisz na limity AI.

Strategie po stronie klienta dla ograniczania AI

Cztery podstawowe mechanizmy pokrywają prawie każdy scenariusz ograniczania AI:

  • Token bucket: jeśli są tokeny, żądania przechodzą. Tokeny odnawiają się z czasem, więc system radzi sobie z krótkimi skokami bez przekraczania limitów.
  • Leaky bucket: wygładza ruch do stałego tempa, co chroni systemy po drugiej stronie przed nagłymi skokami.
  • Pula z limitem współbieżności: ogranicza liczbę aktywnych żądań naraz, więc nigdy nie przekraczasz limitu współbieżności.
  • Eksponencjalny backoff z pełnym jitterem: wydłuża przerwy między nieudanymi żądaniami, żeby klienci nie ponawiali wszystkiego naraz.

Poniżej pokazujemy, jak budować te mechanizmy krok po kroku, zaczynając od tego, który najdokładniej odpowiada limitowi współbieżności.

Wszystkie poniższe przykłady zakładają jednego klienta, inicjowanego raz:

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

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

Ograniczona współbieżność: mechanizm pasujący do limitu

Ponieważ serwer mierzy współbieżność, najprostsza kontrola po stronie klienta to pula workerów z limitem aktywnych żądań. Ustaw limit trochę poniżej limitu planu, żeby zostawić miejsce na kolejkę i 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: pozwól na skoki, ogranicz średnią

Token bucket przechowuje do capacity tokenów i odnawia je z refillRate na sekundę. Każde żądanie zużywa token, więc bucket pozwala na krótkie skoki do swojej pojemności, ale ogranicza średnią w dłuższym okresie.

To dobre narzędzie, gdy nagle pojawia się kolejka zadań – nie wysyłasz wszystkiego naraz i nie robisz skoku współbieżności.

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: stały odpływ

Czasem nie chcesz żadnych skoków. Leaky bucket wpuszcza zadania w stałym tempie, niezależnie od tego, jak bardzo wejście jest nierówne. To lepszy wybór, gdy system po drugiej stronie woli przewidywalne obciążenie niż okazjonalne skoki.

Na przykład, gdy celowo trzymasz się daleko od małego budżetu współbieżności dzielonego z innymi usługami.

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

Eksponencjalny backoff z pełnym jitterem

Gdy żądanie kończy się błędem możliwym do ponowienia, natychmiastowe ponawianie pogarsza sprawę. Backoff rozkłada próby w czasie, a pełny jitter losuje opóźnienie w całym przedziale, więc wielu klientów nie ponawia naraz i nie powtarza skoku, który wywołał błąd.

Poniższy przykład używa klasy RetryableError, która przechowuje status błędu i ewentualny Retry-After. Definicję znajdziesz w sekcji o obsłudze 429 poniżej.

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

Łagodna obsługa 429: co robić po osiągnięciu limitu

429 oznacza, że przekroczyłeś pojemność nawet po kolejce, więc trzeba zwolnić, a nie próbować mocniej. Są cztery sposoby na obsługę tego błędu. Dobra obsługa sprowadza się do czterech strategii:

  • Wykrywanie
  • Szacunek dla Retry-After
  • Sygnalizowanie przeciążenia
  • Unikanie burzy ponowień przez circuit breaker

Rozłóżmy to na szczegóły.

Pierwsze to wykrywanie. Traktuj HTTP 429 (i przejściowe 500, 502, 503, 504) jako możliwe do ponowienia, a 400, 401, 403, 422 jako nie – ponawianie błędnych lub nieautoryzowanych żądań tylko marnuje slot.

Drugie to szacunek dla Retry-After. Jeśli odpowiedź ma ten nagłówek, stosuj się do niego dokładnie, zamiast liczyć własne opóźnienie. Serwer wie lepiej, kiedy będzie miał miejsce. Dopiero gdy nagłówka nie ma, użyj backoff z jitterem.

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

Trzecia oś to sygnalizowanie przeciążenia. Nie pozwól, by ponowienia gromadziły się w tle. Jeśli głębokość kolejki lub zapas pokazują, że nie obsłużysz nowego żądania szybko, odrzuć je od razu z jasnym sygnałem, zamiast przyjmować zadania, których nie zrobisz.

Czwarta to unikanie burzy ponowień przez circuit breaker. Jeśli liczba błędów przekroczy próg, otwórz obwód i odrzucaj żądania przez okno schładzania, zamiast wysyłać kolejne, które i tak się nie powiodą. Po oknie wyślij kilka próbnych żądań – jeśli się powiodą, zamknij obwód.

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

Wzorce limitów dla wielu najemców w ograniczaniu AI

Do tej pory zakładaliśmy jedną aplikację i jeden budżet. Budując SaaS na ElevenLabs, sytuacja się zmienia: budżet współbieżności dzielisz między wszystkich swoich klientów, a jeden najemca z batch jobem nie powinien blokować ruchu innych. Potrzebujesz warstwy sprawiedliwości między najemcami a limitem upstream.

Podstawą są osobne token bucket’y dla każdego najemcy. Każdy dostaje bucket dopasowany do swojego limitu i żądanie przechodzi tylko, gdy zarówno bucket najemcy, jak i globalny limiter na to pozwalają.

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

Bucket’y pilnują, by jeden najemca nie zdominował systemu, ale nie decydują, kto wygrywa przy konkurencji o globalny limiter. Do tego użyj ważonej kolejki.

Nie obsługuj na zasadzie „kto pierwszy, ten lepszy”, bo jeden najemca może zająć wszystkie sloty. Utrzymuj osobną kolejkę dla każdego i obsługuj proporcjonalnie do wagi – płacący najemca dostaje większy udział niż darmowy.

Poza sprawiedliwością zostaw rezerwę. Nie pozwól, by zwykły ruch zajął 100% limitu współbieżności. Zostaw 15-20% jako bufor dla interaktywnych żądań i kolejki priorytetowej.

Gdy sprawiedliwość w ramach jednego budżetu już nie wystarcza, rozdzielaj po workspace’ach lub kluczach. Jeden budżet współbieżności w końcu stanie się wąskim gardłem, niezależnie od podziału.

Wtedy rozdziel zadania na osobne workspace’y lub klucze API z własnymi budżetami: np. jeden klucz dla agentów na żywo, drugi dla narracji w tle – backlog narracji nie zablokuje agentów.

Workspace’y pozwalają też na ograniczenia zakresu, limity kredytów i kontrolę per-klucz, opisane w dokumentacji autoryzacji.

Monitorowanie wykorzystania współbieżności

Bez pomiaru nie da się nic ustawić – nie zarządzisz zapasem, którego nie mierzysz. Zapisuj current-concurrent-requests i maximum-concurrent-requests z każdej odpowiedzi, oznaczając rodzinę modelu, i raportuj stosunek użycia jako 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);
  }
}

Cztery sygnały do śledzenia:

  • Wykorzystanie (current / maximum).
  • Częstość 429 względem wszystkich żądań.
  • Głębokość ponowień, czyli liczba prób na jedno żądanie logiczne.
  • Time-to-first-audio, liczony od twojej aplikacji, nie od modelu. Zobacz, co obejmuje TTFA w sekcji o opóźnieniach.

Zdrowy system trzyma wykorzystanie poniżej nasycenia i widzi 429 tylko w krótkich skokach. Monitorowanie tych sygnałów daje ci wgląd w presję limitów, zanim pojawią się problemy.

Kiedy skalować poza ograniczanie po stronie klienta

Wzorce po stronie klienta dużo pomagają, ale stały wzrost zapotrzebowania w końcu je przerośnie. Wtedy czas na zmiany, które pomogą zarówno z kosztami, jak i wysiłkiem.

Każdy z poniższych kroków daje ci dodatkową pojemność.

Zacznij od przejścia z HTTP na WebSocket dla ruchu interaktywnego. Jeśli agenci lub live działają przez HTTP, przejście na WebSocket sprawia, że tylko aktywne generowanie liczy się do limitu. Dla rozmów często oznacza to wielokrotny wzrost pojemności bez zmiany planu, bo czas ciszy nie blokuje slotów.

Jeśli masz skoki, ale średnie obciążenie mieści się w budżecie, token lub leaky bucket plus ograniczona pula wygładza szczyty do średniej.

Potem wybierz odpowiedni model. Szybsze generowanie krócej blokuje slot, więc ten sam limit obsłuży więcej transmisji. Eleven Flash v2.5 to najniższe opóźnienia do pracy na żywo; połącz go z Szybkie klonowanie głosu lub domyślnym głosem, by uniknąć narzutu Professional Voice Clones.

Dopiero potem podnieś plan. Jeśli po optymalizacji klienta nadal przekraczasz budżet, wyższy plan zwiększa limit współbieżności i priorytet w kolejce. Porównaj poziomy na stronie cen API.

Jeśli potrzebujesz wyższych limitów niż podane, plany Enterprise dają wyższe i niestandardowe limity współbieżności oraz najwyższy priorytet w kolejce. Dla wybranych przypadków są dodatkowe opcje, np. whitelistowanie IP (w wersji Enterprise preview) i tryby bez retencji. Skontaktuj się z opiekunem konta, by podnieść limity.

Podsumowanie: o czym pamiętać przy ograniczaniu AI

Najczęstszy błąd to traktowanie ograniczania AI dla głosu jako liczenia żądań. Tu chodzi o kontrolę współbieżności. Liczy się to, ile żądań generuje audio w tej samej chwili i jak długo każdy slot jest zajęty.

Buduj klienta wokół tej zasady.

Ogranicz aktywne żądania pulą, wpuszczaj zadania przez token lub leaky bucket, ponawiaj z ograniczonym backoffem i jitterem, szanuj Retry-After i przerywaj obwód przed burzą ponowień.

Dla systemów multi-tenant dodaj bucket’y per najemca, ważoną sprawiedliwość, rezerwę i sharding dla izolacji. Obserwuj nagłówki current-concurrent-requests i maximum-concurrent-requests i alarmuj na trend użycia, nie na błędy.

Gdy naprawdę potrzebujesz więcej pojemności, idź po kolei: najpierw WebSocket i lepsze zachowanie klienta, potem odpowiedni model, potem wyższy plan, na końcu limity Enterprise.

Buduj aplikacje głosowe z ElevenAPI

Ograniczanie AI na poziomie produkcyjnym zaczyna się od właściwego transportu, modelu i nagłówków, które jasno pokazują twoją sytuację.

ElevenAPI oferuje modele o niskim opóźnieniu jak Eleven Flash v2.5, streaming WebSocket w czasie rzeczywistym, Speech to Text i Text to Speech API, oraz nagłówki współbieżności w każdej odpowiedzi, dzięki którym zbudujesz skalowalne voice agenty w ramach limitów.

W połączeniu ze strategiami z tego artykułu, zapewnisz szybkie doświadczenia głosowe i przewidywalną wydajność (nawet przy dużym ruchu).

Sprawdź ElevenAPI, żeby zobaczyć wszystkie modele w akcji, albo załóż konto i zacznij budować z ElevenLabs już dziś.

FAQ: ograniczanie AI dla głosu

Podobne artykuły

Twórz z najwyższej jakości audio AI