コンテンツにスキップ

音声AIのレート制限:同時実行数、キュー、429エラーについて

公開日

聴くこの記事を聴く

多くのチームは、他のAPIと同じように音声AIのレート制限に対応しようとします。つまり、1分あたりのリクエスト数を制限し、サーバーから制限がかかったらリトライして進める、という方法です。しかしElevenLabsのワークロードでは、この方法は最初のトラフィックの急増で破綻します。実際に制限されるのはリクエスト数ではなく、同時実行数だからです。

このガイドでは、なぜ同時実行数が本当の制約なのかを説明し、それを守るためのクライアント側のパターンを紹介します。同時実行プールや429エラーの適切な処理、マルチテナントの公平性、トークンバケットやリーキーバケットまで、実践的なシステムを提案します。各パターンには、すぐに使えるTypeScriptの実装例も用意しています。

もし音声エージェントを構築したり、ナレーションパイプラインやその他のプロダクションシステムを当社モデル上で運用し、スケールしたい場合は、このプレイブックが役立ちます。

要点まとめ

  • 音声AIのレート制限は、1分あたりのリクエスト数ではなく、同時実行数の管理です。
  • レート制限の上限に達しても、すぐにトラフィックが拒否されるわけではありません。リクエストは優先度付きキューに入り、約50msの遅延が発生します。
  • キューに入れても上限を超えた場合は、HTTP 429エラーが返されます。
  • WebSocketを使うと、実際の生成中のみ制限にカウントされるため、効果的なキャパシティが大幅に増加します。
  • マルチテナントシステムでは、追加の公平性レイヤーが必要です。テナントごとのバケット、重み付きフェアキューイング、予備枠の確保、キーごとの分割などで分離します。
  • current-concurrent-requestsとmaximum-concurrent-requestsという2つのレスポンスヘッダーで、現在のAIレート制限の状況を確認できます。

なぜ上限は同時実行数であり、1分あたりのリクエスト数ではないのか

同時実行数とは、同時に処理中のリクエスト数です。1分あたりのリクエスト数は、一定期間内のスループットです。この違いを理解することが重要で、どのレバーを操作すれば上限内に収まるかが変わります。

いずれかのElevenLabsモデルを使う場合、サーバーの負荷は同時ユーザー数に比例して増加します。オーディオ生成は生成中ずっとスロットを占有し、その時間は入力の長さやモデル、負荷によって変わります。

1分あたりのリクエスト数の上限では、今現在いくつのスロットが埋まっているかは分かりません。サーバーが計測しているのは、まさにその「今」のスロット数です。

プラン・モデルファミリーごとの上限

同時実行数の予算は1つの数字ではありません。プランやモデルファミリーごとに上限が異なります。例えば、スピーチtoテキストは、テキスト読み上げよりも高い上限が設定されています。これは、文字起こしリクエストの方が一般的に短時間で終わり、システムが同時に多く処理できるためです。

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

上限はモデルファミリーごとに設定されています。エージェント用にFlash、ナレーション用にMultilingual v2を使う場合、それぞれ別の予算で動作します。各プランの最新の数値や同時実行数の詳細は、モデルページでご確認いただけます。

同時実行数の上限に達したらどうなる?

同時実行数の上限に達しても、すぐにトラフィックが拒否されるわけではありません。システムは優先度付きキューで段階的に対応し、全体のキャパシティを超えた場合のみ完全に拒否されます。

上限未満ならリクエストは即時処理されます。上限に達すると、以降のリクエストはプランの優先度順にキューに入ります。キューによる遅延は通常50ms程度なので、短時間の超過はユーザーにはほとんど気づかれません。

キューに入れてもキャパシティを超えている場合は、HTTP 429が返されます。これは即時リトライではなく、間隔を空ける合図です。テーブル内の優先度によって、他のトラフィックと比べてキュー内の順番が決まります。上位プランほど早くキューが処理されます。

HTTPとWebSocket:どちらがどのように上限にカウントされるか

どのトランスポートを選ぶかで、レート制限や予算の使い方が大きく変わります。同じ会話でも、HTTPかWebSocketかで同時実行数の消費量が大きく異なります。

HTTPでは、各リクエストがその処理時間中ずっと同時実行数にカウントされます。WebSocketでは、モデルが実際にオーディオを生成している時間だけカウントされます。開いているだけでアイドル状態のWebSocketは、ほとんどカウントされません。

音声エージェントの場合、会話中に誰も話していない時間やモデルが何も生成していない時間が長くなります。HTTPだと毎ターンごとにスロットを占有しますが、WebSocketならアクティブな生成中のみスロットを使うため、1つのスロットを複数の会話で時間共有できます。

詳細はリアルタイムTTS WebSocketガイドをご覧ください。インタラクティブなトラフィックには、WebSocketが基本となります。

なぜ同時実行数5で約100件の配信が可能なのか

同時実行数の計算は、再生時間を考慮しないと直感に反します。生成は再生よりもはるかに速く、オーディオ生成中のみスロットが占有されます。このギャップが、小さな予算で大きなオーディエンスに対応できる理由です。

生成に数分の一秒しかかからないリクエストでも、リスナーが再生するのに数秒かかるオーディオが作られます。再生中はスロットが解放され、他のリスナーが利用できます。

目安として、同時実行数5で約100件の同時オーディオ配信が可能です。正確な数は音声や話し方、発話間の無音時間によって変わります。

現在の状況を示すヘッダー

上限に対して自分の状況を推測する必要はありません。すべてのレスポンスに2つの数字が含まれており、単なる推測ではなく余裕を正確に測れます。

以下の2つのヘッダーに注目してください:

  • current-concurrent-requests: 現在同時に処理中のリクエスト数
  • maximum-concurrent-requests: そのモデルファミリーの上限

これらのヘッダーを組み合わせることで、リアルタイムで現在の利用状況と空きキャパシティを把握できます。AIのレート制限にぶつかる前に、推測で動く必要はありません。

AIレート制限のためのクライアント側の戦略

ほとんどのAIレート制限シナリオをカバーする4つの基本パターンがあります:

  • トークンバケット: トークンがあればリクエストを許可します。トークンは時間とともに補充されるため、短時間のバーストにも対応できます。
  • リーキーバケット: 入ってくるトラフィックを一定の出力レートに平滑化し、急激なスパイクで下流システムが圧迫されるのを防ぎます。
  • 同時実行数制限付きプール:同時にアクティブなリクエスト数を制限し、同時実行数の上限を超えないようにします。
  • 指数バックオフ+フルジッター:失敗したリクエストの間隔を徐々に長くし、全クライアントが一斉にリトライしないようにします。

以下のセクションで、これらを1つずつ構築する方法を紹介します。まずは同時実行数の上限に最も直結するものから始めます。

以下のスニペットは、1つのクライアントを初期化して使う前提です:

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

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

同時実行数制限:上限に直結する基本パターン

サーバーが同時実行数を計測しているため、クライアント側で最も直接的な制御は、同時実行数を制限するワーカープールです。プランの上限より少し低めに設定し、優先度付きキューやジッターの余裕を持たせましょう。

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

トークンバケット:バーストを許容しつつ平均を制限

トークンバケットは、最大容量分のトークンを保持し、毎秒refillRateで補充されます。各リクエストは1トークン消費するため、短時間のバーストは許容しつつ、長期的なレートは制限できます。

急に大量の処理が発生したとき、一気にリクエストを送って同時実行数が急増するのを防ぐのに最適です。

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

リーキーバケット:一定の排出レートを維持

バーストを一切許容したくない場合は、リーキーバケットを使います。入力がどれだけバーストしても、一定のレートでしか処理しません。下流システムが安定した負荷を好む場合に適しています。

例えば、他サービスと共有する小さな同時実行数予算をしっかり守りたい場合などです。

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

指数バックオフ+フルジッター

リクエストがリトライ可能なステータスで失敗した場合、すぐにリトライすると状況が悪化します。バックオフでリトライ間隔を空け、フルジッターで遅延をランダム化することで、多数のクライアントが一斉にリトライして再びスパイクを起こすのを防ぎます。

以下のスニペットでは、失敗したステータスやRetry-After値を持つ小さなクラスRetryableErrorを参照しています。これは下記の429エラーの適切な処理セクションで定義されています。

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

429エラーの適切な処理:上限に達したときの対応

429は、優先度付きキューを経てもキャパシティを超えた場合に返されます。正しい対応は、リトライを増やすのではなく、ペースを落とすことです。主な対応方法は4つあります:

  • 検知
  • Retry-Afterの尊重
  • バックプレッシャーの可視化
  • サーキットブレーカーによるリトライ嵐の回避

それぞれ詳しく見ていきましょう。

まずは検知です。HTTP 429(および一時的な500、502、503、504)はリトライ可能とし、400、401、403、422はリトライ不可とします。不正なリクエストや認証エラーはリトライしても成功せず、スロットの無駄遣いです。

次にRetry-Afterの尊重です。レスポンスにこのヘッダーがあれば、独自の遅延計算はせず、その値を正確に守りましょう。サーバーがキャパシティ回復のタイミングを最もよく知っています。ヘッダーがない場合のみ、ジッター付きバックオフに切り替えます。

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

3つ目はバックプレッシャーの可視化です。リトライが見えないところで積み上がらないようにしましょう。キューの深さや余裕がない場合は、できないリクエストは早めに拒否し、呼び出し元に明確なシグナルを返します。

4つ目はサーキットブレーカーによるリトライ嵐の回避です。失敗が一定数を超えたら回路を開き、クールダウン期間中は即座に失敗させます。期間後に少数のプローブリクエストを送り、成功したら回路を閉じます。

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

AIレート制限におけるマルチテナントのクォータパターン

ここまでの内容は、1つのアプリケーションが1つの予算で動作する前提です。ElevenLabs上でSaaSを構築する場合、問題は変わります。同時実行数の予算が自社の全顧客で共有され、1つのテナントがバッチ処理を走らせると他のテナントのライブトラフィックが圧迫されることも。テナントごとと上流の単一上限の間に公平性レイヤーが必要です。

基本はテナントごとのトークンバケットです。各テナントに権利に応じたバケットを割り当て、テナントバケットとグローバルリミッターの両方が許可した場合のみリクエストを受け付けます。

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

バケットで1つのテナントの暴走は防げますが、複数テナントがグローバルリミッターを争う場合の優先順位は決まりません。その場合は重み付きフェアキューイングを使いましょう。

先着順で処理すると、1つのテナントのバーストがスロットを独占してしまいます。テナントごとにキューを持ち、重みに応じて配信することで、有料テナントが無料テナントより多くのキャパシティを確保できます。

公平性に加えて、予備枠も確保しましょう。通常トラフィックで同時実行数の100%を使い切らず、15~20%程度をインタラクティブなリクエストや優先度付きキューのために残しておきます。

単一予算内の公平性だけでは足りなくなったら、ワークスペースやキーごとに分割します。どれだけ公平に分けても、単一の同時実行数予算がボトルネックになります。

その場合は、ワークロードごとに別のワークスペースやAPIキーを使い、それぞれに予算を割り当てます。例えば、リアルタイムエージェント用とバックグラウンドナレーション用でキーを分ければ、ナレーションのバックログがエージェントのキャパシティを圧迫しません。

ワークスペースを使えば、スコープ制限やクレジットクォータ、キーごとの制御も適用できます。詳細は認証ドキュメントをご覧ください。

同時実行数の利用状況をモニタリングする

計測なしでは調整できません。余裕を測らなければ管理もできません。すべてのレスポンスでcurrent-concurrent-requestsとmaximum-concurrent-requestsをモデルファミリーごとに記録し、利用率をゲージとして出力しましょう。

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

追跡すべき4つの指標:

  • 利用率(current / maximum)
  • 全リクエストに対する429発生率
  • リトライ深度(論理リクエストあたりの試行回数)
  • Time-to-first-audio(TTFA):アプリケーション側で計測。モデル推論の数値ではなく、レイテンシーの理解についてはTTFAの説明を参照してください。

健全なシステムでは、利用率は余裕を持って上限未満に保たれ、429はたまにバースト的に発生する程度です。これらの指標を監視することで、障害になる前にレート制限の圧力を可視化できます。

クライアント側レート制限を超えてスケールすべきタイミング

クライアント側のパターンだけでも多くの負荷に対応できますが、安定した需要が増えればいずれ限界が来ます。そのときはコストと運用負荷の両方を軽減するための変更を検討しましょう。

以下のステップを実施することで、さらにキャパシティを確保できます。

まず、インタラクティブなトラフィックはHTTPからWebSocketに切り替えましょう。エージェントやライブ用途がHTTPの場合、WebSocketにすることでアクティブな生成中のみカウントされるようになり、同じプランでも実効キャパシティが大幅に増えます。会話のアイドル時間がスロットを消費しなくなるためです。

バーストが激しくても平均負荷が予算内なら、トークンバケットやリーキーバケット+同時実行数制限プールでピークを平滑化できます。

次に、適切なモデルを選びましょう。生成が速いほどスロットの占有時間が短くなり、同じ同時実行数でより多くの配信が可能です。Eleven Flash v2.5は最小レイテンシーの選択肢でリアルタイム用途に最適です。これをインスタントボイスクローンやデフォルト音声と組み合わせれば、プロフェッショナルボイスクローンの生成ごとのオーバーヘッドを回避できます。

それでも足りない場合はプランのアップグレードを検討しましょう。クライアント側が適切に動作しても安定した需要が予算を超える場合、上位プランでモデルごとの同時実行数上限とキュー優先度が上がります。API料金ページで各プランを比較できます。

公開されている上限を超える必要がある場合は、エンタープライズプランでさらに高い・カスタムの同時実行数上限や最上位のキュー優先度が利用できます。IPホワイトリスト(エンタープライズプレビュー)やゼロリテンションモードなど、対象用途向けの追加制御も可能です。上限引き上げはアカウントマネージャーまでご相談ください。

AIレート制限で押さえておくべきポイントまとめ

最大の誤解は、音声AIのレート制限をリクエスト数のカウントだと考えることです。ここで解説したのはすべて同時実行数の管理です。成功を左右するのは、同時にオーディオを生成しているリクエスト数と、それぞれがスロットを占有する時間です。

この事実を前提にクライアントを設計しましょう。

同時実行数制限プールでリクエスト数を抑え、トークンバケットやリーキーバケットで受付を調整し、指数バックオフ+フルジッターでリトライ、Retry-Afterを尊重し、リトライ嵐の前にサーキットを切る、という流れです。

マルチテナントシステムでは、テナントごとのバケット、公平性、予備枠、分割による分離を重ねましょう。current-concurrent-requestsとmaximum-concurrent-requestsヘッダーを監視し、失敗ではなく利用率の傾向でアラートを出しましょう。

本当にキャパシティが必要な場合は、順番に対策を進めましょう。まずWebSocketとクライアント側の最適化、次に適切なモデル、プランのアップグレード、最後にエンタープライズ上限です。

ElevenAPIで音声アプリを構築

本格的なAIレート制限は、適切なトランスポート・モデル・状況を正確に示すヘッダーから始まります。

ElevenAPIは、Eleven Flash v2.5のような低レイテンシーモデル、リアルタイムWebSocketストリーミング、スピーチtoテキストテキスト読み上げAPI、レスポンスごとの同時実行数ヘッダーを提供し、上限内でスケールする音声エージェントを構築できます。

この記事で紹介したAIレート制限戦略と組み合わせれば、負荷がかかっても予測可能なパフォーマンスを維持しつつ、応答性の高い音声体験を提供できます。

ぜひElevenAPIで全モデルラインナップを体験するか、アカウントを作成して、今日からElevenLabsで開発を始めましょう。

AIレート制限に関するFAQ

関連記事

最高品質のAIオーディオで創造する