跳至内容

语音 AI 限流:并发、队列与 429 错误

发布时间
最近更新

收听收听本文

大多数团队处理语音 AI 限流的方式与其他 API 一样:限制每分钟请求数,服务器拒绝时重试,然后继续。但对于 ElevenLabs 上的工作负载,这种模式会在第一次流量激增时失效,因为实际触及的限制是并发数,而非请求数。

本指南将说明为何并发才是真正的约束,并介绍让客户端保持在限制范围内的模式。从有界并发池和优雅处理 429,到多租户公平性、令牌桶和漏桶,我们提供了可直接实现的实用方案。每种模式都配有可调整的 TypeScript 实现。

如果你要构建语音智能体、旁白生成管道,或基于我们模型构建其他需要扩展的生产系统,本指南适合你。

摘要

  • 语音 AI 限流的核心是并发控制,而不是按每分钟计算请求数。
  • 触及限流上限不会立即拒绝流量。请求会进入优先级队列,通常增加约 50ms 延迟。
  • 排队后仍超出容量,将产生 HTTP 429 错误。
  • WebSocket 能大幅提高有效容量,因为只有活跃生成会计入限制。
  • 多租户系统需要额外的公平层:按租户分配桶、加权公平队列、预留余量,以及通过多个密钥分片来隔离。
  • 通过 current-concurrent-requests 和 maximum-concurrent-requests 两个响应头,可以了解当前 AI 限流状态。

为何限制是并发数,而不是每分钟请求数

并发数是同一时刻正在处理的请求数。每分钟请求数则是一个时间窗口内的吞吐量。理解这一区别很重要,因为它决定了该用什么手段保持在限制范围内。

使用ElevenLabs 模型时,服务器工作负载会随并发用户数增加。音频生成会在整个生成期间占用一个槽位,时长则取决于输入长度、模型和负载。

每分钟请求数上限无法说明当前有多少槽位被占用,而这才是服务器唯一计量的指标。

按套餐和模型系列划分的限制

并发预算并非单一数值。并发限制因套餐和模型系列而异。例如,语音转文本 的限制高于文本转语音,因为转写请求通常持续时间更短,系统可以同时承载更多请求。

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,则同时使用两个独立预算。各套餐的最新数值及并发说明见模型页面

触及并发限制时会发生什么?

达到并发限制不会立即拒绝流量。系统会通过优先级队列优雅降级,只有在仍超出限流总容量时才会完全拒绝。

未达到限制时,请求会立即执行。达到限制后,后续请求会按套餐优先级进入队列。队列通常只增加约 50ms 延迟,因此短暂超限对用户几乎无感。

如果排队后系统仍超出容量,将收到 HTTP 429。这表示应降低速率,而非立即重试。表中的优先级决定排队请求相对其他流量的顺序;更高套餐能更快清空队列。

HTTP 与 WebSocket:二者如何计入限制

所选传输方式会直接影响限流和预算。同一段对话通过 HTTP 或 WebSocket 运行,消耗的并发预算可能相差很大。

通过 HTTP,每个请求在整个持续期间都会单独计入并发限制。通过 WebSocket,则只有模型主动生成音频的时间会被计入。已打开但空闲的 WebSocket 通常不会计入。

对于语音智能体,一段对话中常有很长时间无人说话,模型也未生成内容。使用 HTTP,每一轮都会在请求持续期间占用槽位。使用 WebSocket,只有活跃生成的几毫秒会消耗槽位,因此一个并发槽位可在多段对话间共享。

协议详情请参阅实时 TTS WebSocket 指南。对于交互式流量,WebSocket 是合适的默认选择。

为何约 5 个并发可支持约 100 路播放

在考虑播放时间前,并发的计算逻辑并不直观。生成速度远快于播放速度,而且槽位只有在生成音频时才会被主动占用。这段差距正是小预算能服务大量听众的原因。

只需几分之一秒生成的请求,可能产生数秒音频供听众播放;播放期间,槽位会被释放,供其他听众使用。

经验来看,5 的并发限制大约可支持 100 路同时音频播放。实际数值取决于音色、语速以及话语间的静音时长。

说明当前状态的响应头

无需推测与限制的差距。每个响应都包含两个数值,可用来衡量余量,而不只是估算。

注意以下两个响应头:

  • current-concurrent-requests: 当前正在处理多少请求?
  • maximum-concurrent-requests: 该模型系列的限制。

这两个响应头结合起来,可实时了解当前用量和可用容量。无需等到触及 AI 限制后再猜测。

AI 限流的客户端策略

以下 4 种基本模式几乎覆盖所有 AI 限流场景:

  • 令牌桶: 有可用令牌时允许请求继续。容量会随时间补充,因此可处理短暂流量激增而不触及限流。
  • 漏桶: 将输入流量平滑为固定输出速率,避免突发峰值压垮下游系统。
  • 有界并发池: 限制可同时活跃的请求总数,确保不会超出并发请求限制。
  • 带完全抖动的指数退避: 逐步增加失败请求之间的等待时间,防止所有客户端同时重试。

下文将逐一介绍如何构建这些模式,先从最直接对应并发限制的模式开始。

以下代码片段均假设只初始化一次单个客户端:

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 个令牌,并以每秒 refillRate 个令牌的速度补充。每个请求消耗一个令牌,因此桶可允许不超过容量的短暂突发,同时限制长期速率。

它适合在工作队列突然到来时平滑流量,避免一次性全部发送而导致并发飙升。

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

带完全抖动的指数退避

请求以可重试状态失败时,立即重试只会让情况更糟。退避会拉开重试间隔,完全抖动则会在整个区间内随机化每次延迟,避免大量客户端同步重试,再次造成导致失败的峰值。

以下片段引用 RetryableError,这是一个携带失败状态和 Retry-After 值的小型类。其定义见下文的优雅处理 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}`);
}

第三项是暴露背压。不要让重试在后台悄然堆积。如果队列深度或测得的余量表明无法很快处理新请求,应在边缘明确拒绝并告知调用方,而不是接受无法完成的工作。

第四项是使用断路器避免重试风暴。如果失败次数超过阈值,打开断路器并在冷却窗口内快速失败,不要发送预期会失败的请求。窗口结束后,发送少量探测请求;若成功,则关闭断路器。

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

桶可以约束单个租户,但无法决定多个租户竞争全局限流器时谁优先。为此,应使用加权公平队列。

不要采用先到先服务,否则一个租户的突发流量会垄断槽位。为每个租户维护队列,并按其权重比例分发,使付费租户获得比免费租户更大的竞争容量份额。

除了公平性,还应预留余量。绝不要让常规流量消耗 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 包含的内容,请参阅延迟说明。

健康的系统会让利用率舒适地低于饱和状态,仅在偶发突发中出现 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 流式传输、语音转文本文本转语音 API,以及每个响应中的并发响应头,帮助你构建可在限制范围内扩展的语音智能体

结合本文的 AI 限流策略,即使在负载下,也能提供响应迅速且性能可预测的语音体验。

探索ElevenAPI,查看完整模型阵容的实际效果,或创建账户,立即开始使用 ElevenLabs 构建应用。

AI 限流常见问题

相关内容

用高质量 AI 音频创作