コンテンツへ移動

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

公開日
最終更新日

聴くこの記事を聴く

多くのチームは、音声向けAIレート制限も他のAPIと同じように扱います。1分あたりのリクエスト数に上限を設け、サーバーに拒まれたら再試行し、それで終わりです。しかしElevenLabsのワークロードでは、最初のトラフィック急増でこの考え方は通用しなくなります。実際に達する上限はリクエスト数ではなく、同時実行数だからです。

このガイドでは、なぜ同時実行数が本当の制約なのかを解説し、その範囲内に収めるクライアント側のパターンを紹介します。上限付き同時実行プール、適切な429処理、マルチテナントでの公平性、トークンバケットとリーキーバケットまで、実装可能な実践的システムを取り上げます。各パターンには、カスタマイズできる動作するTypeScript実装を用意しました。

音声エージェントを構築する、ナレーションパイプラインなど、モデル上で本番システムを構築・拡張したい場合は、このプレイブックが役立ちます。

概要

  • 音声向けAIレート制限は、1分あたりのリクエスト数のカウントではなく、同時実行数の制御です。
  • レート制限の上限に達しても、トラフィックが即座に拒否されるわけではありません。リクエストは優先度キューに入り、通常は約50msの遅延が加わります。
  • キューイング後も容量を超えている場合は、HTTP 429エラーが発生します。
  • WebSocketではアクティブな生成のみが上限にカウントされるため、有効容量が大幅に増加します。
  • マルチテナントシステムには、公平性のレイヤーが別途必要です。テナントごとのバケット、重み付き公平キューイング、予約ヘッドルーム、分離のためのキー間シャーディングを活用します。
  • current-concurrent-requestsとmaximum-concurrent-requestsという2つのレスポンスヘッダーで、AIレート制限に対する現在の状況を把握できます。

上限がリクエスト数/分ではなく同時実行数である理由

同時実行数とは、同じ瞬間に処理中のリクエスト数です。1分あたりのリクエスト数は、一定の時間枠におけるスループットです。この違いを理解することが重要なのは、上限内に収めるために調整すべきものが変わるからです。

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

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
Queue weight
Free
3
Starter
4
Creator
5
Pro
5
Scale
5
Business
5
Enterprise
6

上限はモデルファミリーごとに設定されます。エージェントにFlash、ナレーションにMultilingual v2を使用する場合、2つの別々の予算を同時に利用することになります。プラン別の最新値と同時実行数の詳細は、モデルページで確認できます。

同時実行数の上限に達するとどうなりますか?

同時実行数の上限に達しても、トラフィックはすぐには拒否されません。システムは優先度キューによって段階的に処理し、レート制限の総容量をなお超えている場合にのみ完全な拒否へ移行します。

上限未満であれば、リクエストはすぐに実行されます。上限に達すると、後続リクエストはプランの優先度に応じて並べ替えられたキューに入ります。キューによる遅延は通常約50msであるため、短時間の超過はユーザーにはほぼ気づかれません。

キューイング後もシステムが容量を超えている場合、HTTP 429を受け取ります。これは即時再試行ではなく、処理を抑えるべきシグナルです。表の優先度は、他のトラフィックに対するキュー内リクエストの順序を決めます。上位プランほど早くキューを通過できます。

HTTPとWebSocket:上限へのカウント方法

選択するトランスポートは、レート制限と予算に直接影響します。同じ会話でも、HTTPで実行するかWebSocketで実行するかによって、消費する同時実行数の予算は大きく異なる場合があります。

HTTPでは、各リクエストが全期間にわたり個別に同時実行数の上限へカウントされます。WebSocketでは、モデルが実際にオーディオを生成している時間だけがカウントされます。接続が開いていてもアイドル状態のWebSocketは、ほとんどカウントされません。

音声エージェントの会話には、誰も話さずモデルも何も生成していない長い時間があります。HTTPでは、各ターンでリクエスト期間中ずっとスロットを保持します。WebSocketでは、スロットを消費するのはアクティブな生成中の数ミリ秒だけなので、1つの同時実行スロットを多くの会話で共有できます。

リアルタイムTTS WebSocketガイドでプロトコルの詳細をご確認ください。インタラクティブなトラフィックでは、WebSocketが適切なデフォルトです。

同時実行数約5で約100件の配信を支えられる理由

再生時間を考慮するまで、同時実行数の計算は直感に反します。生成は再生よりはるかに速く、スロットが実際に占有されるのはオーディオ生成中だけです。この差によって、小さな予算でも大規模なオーディエンスに対応できます。

生成にほんの数分の1秒かかるリクエストでも、数秒分のオーディオが生成されます。リスナーがそれを再生している間、スロットは解放され、他のリスナーが利用できます。

目安として、同時実行数の上限が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

トークンバケット:バーストを許可し、平均を制限する

トークンバケットは、最大capacity個のトークンを保持し、1秒あたり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を尊重する
  • バックプレッシャーを通知する
  • サーキットブレーカーで再試行ストームを防ぐ

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

1つ目は検出です。HTTP 429と一時的な500、502、503、504はリトライ可能として扱い、400、401、403、422はリトライ不可として扱います。不正な形式または未認証のリクエストは再試行しても成功せず、スロットを無駄にするだけです。

2つ目は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レート制限におけるマルチテナントのクォータパターン

ここまでは、単一の予算に対する単一アプリケーションを前提としていました。ElevenLabs上でSaaSを構築する場合、問題の形は変わります。同時実行数の予算はすべての顧客で共有され、あるテナントのバッチジョブが他のすべてのテナントのライブトラフィックを圧迫してはなりません。テナントと単一のアップストリーム上限の間に、公平性のレイヤーが必要です。

基盤となるのは、テナントごとのトークンバケットです。各テナントの権利に合わせた独自のバケットを割り当て、テナントのバケットとグローバルリミッターの両方が許可した場合にのみリクエストを受け入れます。

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つのテナントのバーストがスロットを独占できます。テナントごとのキューを維持し、それぞれの重みに比例してディスパッチしてください。これにより、有料テナントは無料テナントよりも、競合する容量の大きな割合を得られます。

公平性に加えて、ヘッドルームを確保してください。通常トラフィックに同時実行数の上限を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の割合。
  • 再試行深度。論理リクエストあたりの試行回数です。
  • 最初のオーディオまでの時間(TTFA)。モデル推論値ではなく、アプリケーションから測定します。TTFAに含まれる内容は、レイテンシーの解説を参照してください。

健全なシステムでは、使用率を飽和状態より十分低く保ち、429は時折発生するバースト時にのみ見られます。これらのシグナルをモニタリングすれば、障害になるずっと前にレート制限の圧力を把握できます。

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

クライアント側のパターンは大きな役割を果たしますが、定常状態の需要はいずれその能力を超えます。そのときは、コストと運用負荷の両方に役立つ変更を行うタイミングです。

次の各手順で、追加の容量を確保できます。

まず、インタラクティブなトラフィックではHTTPからWebSocketに切り替えます。エージェントやライブユースケースをHTTPで実行している場合、WebSocketへ移行すると、アクティブな生成だけがカウントされるようになります。会話型のワークロードでは、アイドル中の会話時間がスロットを消費しなくなるため、プランを変更せずに有効容量を何倍にも増やせることがよくあります。

バーストがスパイク状でも平均負荷が予算内に収まる場合は、トークンバケットまたはリーキーバケットと上限付きプールを組み合わせることで、ピークを平均化できます。

次に、適切なモデルを選びます。生成が高速なほど各スロットの保持時間が短くなり、固定の同時実行数上限で維持できる配信数が増えます。Eleven Flash v2.5はリアルタイム作業向けの最小レイテンシーの選択肢です。インスタントボイスクローンまたはデフォルト音声と組み合わせると、Professional Voice Clonesの生成ごとのオーバーヘッドを回避できます。

その後に初めて、プランのアップグレードを検討してください。クライアントが適切に動作している状態でも定常需要が実際に予算を超える場合は、上位プランによりモデルごとの同時実行数上限とキューの優先度の両方が上がります。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レート制限に関するよくある質問

関連記事

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