LLM 限流决定了你的应用在一个时间窗口内可以向模型发送多少流量。工程师最常遇到的两个限制是 RPM(每分钟请求数) 和 TPM(每分钟 token 数)。某个工作负载可能没有超过其中一个限制,却仍然会超过另一个。
这种区别很重要。如果你把每个 429 都当作通用的请求数量问题处理,就可能增加重试,从而提高 token 压力、拉长延迟,并让故障变得更严重。一个适合生产环境的设计,会先识别受限的资源,然后再在节流、排队、重试、减少 token 或路由到其他位置之间做选择。
本指南将解释其工作机制,提供容量规划公式,并包含一个适用于 OpenAI 兼容 API 的有界 TypeScript 重试模式。
RPM 与 TPM:快速答案
| 限制 | 衡量内容 | 最先触发它的工作负载 | 最佳首要响应 |
|---|---|---|---|
| RPM | 在提供方定义的时间窗口内被接纳的请求数 | 大量小调用、agent 工具循环、高分发评估 | 对请求进行节流、批处理工作,或对突发流量排队 |
| TPM | 在一个时间窗口内被接纳的输入和/或输出 token 数 | 长上下文、大输出、重检索提示词、并行评估 | 减少 token 量、限制输出,或路由到有容量的地方 |
| RPD | 每日请求数 | 计划中的爬取、大规模离线评估、免费层工作负载 | 重新安排时间或提高服务层级 |
| 并发请求 | 同一时刻正在进行中的请求数 | 慢生成和流式工作负载 | 限制 worker 数量并施加背压 |
| 429 | 限制或容量策略拒绝了该请求 | 任何超过活动桶上限的工作负载 | 在重试之前先对错误进行分类 |
RPM 控制频率。TPM 控制吞吐量。并发控制同时进行的工作。 它们会相互影响,但并不能互相替代。
为什么请求会在未达到表面限制时失败
像 600 RPM 这样的公开限制,并不一定意味着客户端可以在每分钟的第一秒发送 600 个请求。提供方通常会使用滑动窗口或令牌桶式控制来执行限流。即使按整分钟计算看起来安全,短时间突发也可能耗尽即时可用容量。
429 过早出现的其他原因包括:
- 该限制适用于项目、组织、账户、模型系列或服务层级,而不是某个 API 密钥。
- 输入 token 和输出 token 使用不同的桶。
- 多个 worker、服务或用户共享同一个配额池。
- 之前失败请求的重试正在消耗同一个限制。
- 当流量急剧上升时,提供方正在施加加速或突发限制。
- 即使另一个模型仍有容量,某个特定模型的池也可能已满。
因此,应用不应仅凭自身的请求计数器来推断原因。应读取响应体和响应头,保留提供方请求 ID,并记录模型、账户、token 估算值、尝试次数以及队列延迟。
实用的容量公式
从两个相互独立的上限开始。
request_ceiling = RPM × safety_factor
token_ceiling = (TPM × safety_factor) ÷ average_tokens_per_request
safe_requests_per_minute = min(request_ceiling, token_ceiling)
使用低于 1.0 的安全系数——例如 0.7 到 0.9——以吸收 token 波动、重试、共享消费者以及不均匀的到达模式。
示例
假设某个模型池允许:
- 1,000 RPM
- 2,000,000 TPM
- 每次请求平均总 token 数为 4,000
- 80% 的运行安全系数
request_ceiling = 1,000 × 0.8 = 800 requests/minute
token_ceiling = (2,000,000 × 0.8) ÷ 4,000
= 400 requests/minute
safe_requests_per_minute = min(800, 400) = 400
TPM 是限制性约束。增加更多 worker 不会提高可持续吞吐量;它只会产生更大的队列,或更多 429 响应。
对于在线系统,可使用 Little 定律将吞吐量换算为并发起点:
target_concurrency ≈ requests_per_second × average_request_seconds
如果安全速率是每分钟 400 个请求(每秒 6.67 个),且模型平均延迟为 3 秒,那么起始并发量大约为 20。谨慎增加余量,然后根据真实的 p95 延迟和 token 分布进行调优。
Token 限制往往是隐藏的瓶颈
团队经常监控请求数量,却忽视了 token 量。当你以下操作时,TPM 压力会升高:
- 在每个 prompt 中加入更多检索到的文档。
- 保留更长的对话历史。
- 对每个任务运行多个候选补全。
- 提高输出上限。
- 反复发送相同的大型系统 prompt。
- 针对同一个项目配额启动并行评测套件。
对于每个成功请求,至少测量四个 token 值:
- 输入 tokens。
- 输出 tokens。
- 总 tokens。
- 按工作负载和模型统计的滚动 p50、p95 和最大值。
只使用平均值进行容量规划过于乐观。更安全的调度器会基于高百分位数或特定工作负载的估算进行预留,然后在完成后将预留量与实际使用量进行对账。
429 的含义,以及它不意味着什么
HTTP 429 Too Many Requests 表示服务器在当前限流或容量策略下拒绝了该请求。它并不自动意味着“睡一秒再重试”。
将 429 分类到一个运维桶中:
| 429 类别 | 证据 | 正确操作 |
|---|---|---|
| 短时突发 | 近期出现峰值;重试头很短;队列其余部分运行正常 | 等待服务器提示,然后带抖动重试 |
| 持续性 RPM 耗尽 | 请求速率始终接近上限 | 控制速率或排队;仅靠重试无法解决 |
| 持续性 TPM 耗尽 | Token 速率很高;长提示词或长输出占主导 | 减少 token、延迟工作,或路由到另一个有资格的池 |
| 每日或分层限制 | 错误标识了每日配额、计费或分层限制 | 停止重试;重新安排或更改账户容量 |
| 加速限制 | 流量从较低基线快速上升 | 逐步提升并平滑突发流量 |
| 提供商容量事件 | 客户端速率正常,但反复出现短暂拒绝 | 使用少量重试预算,然后采用合同安全的回退方案 |
不同提供商的正文和响应头会有所不同。若提供了明确的 Retry-After 或限流重置信号,应优先使用。否则,使用带随机抖动的指数退避。
带抖动的指数退避
指数退避会在每次失败尝试后增加延迟。抖动会让该延迟随机化,从而避免成百上千个工作线程在同一时刻同时重试。
一种常见的 full-jitter 公式是:
delay_ms = random(0, min(cap_ms, base_ms × 2^attempt))
生产环境中的重试策略还需要边界条件:
- 最大尝试次数: 通常是较小的次数,而不是无限循环。
- 最大耗时: 当调用方的延迟预算耗尽时停止。
- 可重试状态列表: 通常包括
429、部分5xx响应以及安全的网络故障。 - 服务器提示: 当
Retry-After有效时应遵循它。 - 取消: 当上游请求被中止时立即停止。
- 可观测性: 记录尝试次数、等待时间、最终状态以及提供商请求 ID。
失败请求仍可能消耗限流容量。因此,激进的重试会延长被限流的时间。
TypeScript:一个有边界的重试辅助函数
下面的示例使用与 OpenAI 兼容的 Flatkey 基础 URL。它只会在成功响应正文被消耗之前进行重试,并在达到尝试次数上限或总时间预算耗尽时停止。
type ChatRequest = {
model: string;
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
max_tokens?: number;
};
const sleep = (milliseconds: number) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
function retryAfterMilliseconds(response: Response): number | null {
const value = response.headers.get("retry-after");
if (!value) return null;
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
const date = Date.parse(value);
return Number.isNaN(date) ? null : Math.max(0, date - Date.now());
}
export async function createChatCompletion(
apiKey: string,
request: ChatRequest,
options: { maxAttempts?: number; maxElapsedMs?: number } = {},
) {
const maxAttempts = options.maxAttempts ?? 4;
const maxElapsedMs = options.maxElapsedMs ?? 30_000;
const startedAt = Date.now();
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const response = await fetch(
"https://router.flatkey.ai/v1/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
},
);
if (response.ok) return response.json();
const retryable = response.status === 429 || response.status >= 500;
const finalAttempt = attempt === maxAttempts - 1;
if (!retryable || finalAttempt) {
throw new Error(`LLM 请求失败,状态码 ${response.status}:${await response.text()}`);
}
const hintedDelay = retryAfterMilliseconds(response);
const exponentialCap = Math.min(8_000, 500 * 2 ** attempt);
const jitteredDelay = Math.random() * exponentialCap;
const delayMs = hintedDelay ?? jitteredDelay;
if (Date.now() + delayMs - startedAt >= maxElapsedMs) {
throw new Error("LLM 重试预算已耗尽");
}
await sleep(delayMs);
}
throw new Error("不可达的重试状态");
}
在生产环境中使用时,请添加请求超时、结构化错误类型、指标,以及你应用的取消信号。如果该操作可能产生外部副作用——例如发送电子邮件或执行工具——请先让业务操作具备幂等性,再进行重试。
重试、排队、降级还是路由?
应根据原因,而不是仅凭状态码,来选择控制方式。
| 情形 | 重试 | 排队 | 减少 token | 路由到其他地方 |
|---|---|---|---|---|
一次孤立的瞬时 429 |
是,有限制 | 可选 | 否 | 通常不需要 |
| 重复发生 RPM 耗尽 | 有限 | 是 | 否 | 有时 |
| 重复发生 TPM 耗尽 | 有限 | 是 | 是 | 通常很有用 |
| 每日配额耗尽 | 否 | 留待之后 | 可选 | 是,如果策略允许 |
提供方 5xx 事件 |
是,有限制 | 是 | 否 | 在重试预算后是 |
| 部分流式响应 | 无自动重放 | 取决于应用 | 否 | 仅在明确的恢复语义下 |
重试
当故障是瞬时的,并且调用方仍有时间时进行重试。为每个请求设置重试预算,并设置服务级重试预算,以免提供方事件导致总流量成倍放大。
排队
当到达速率暂时超过可持续服务速率时进行排队。一个有用的队列应暴露:
- 最老项目的等待时长。
- 预计开始时间。
- 按租户的公平性。
- 对过期作业的取消。
- 带有明确丢弃行为的最大深度。
减少 token
当 TPM 成为约束时,移除无关上下文,压缩历史,减少候选数量,降低输出上限,在支持的情况下缓存可复用的提示前缀,并将短交互流量与长批处理作业分开。
路由到其他地方
当另一个模型或提供方满足相同契约并且容量充足时,路由是合适的。备用方案必须保留所需能力,例如结构化输出、工具调用、上下文长度、安全策略和延迟目标。若要了解更深入的实现框架,请参阅 LLM API 备用路由实战指南。
模型评估中的限流
糟糕的节流会使评估失效。
假设模型 A 使用 10 个 worker 进行测试,而模型 B 使用 100 个 worker 进行测试。如果模型 B 花了更多时间在限流上,那么其测得的延迟就包含了队列等待和重试延迟,而模型 A 并未经历这些。结果可能描述的是你的测试框架配置,而不是模型性能。
为了获得有说服力的比较:
- 将模型延迟、队列延迟和重试延迟分开。
- 对每个提供方应用相同的到达过程或归一化利用率水平。
- 当提供方使用加速控制时,逐步预热流量。
- 记录每个完成任务的 token 数和尝试次数。
- 同时报告首次尝试成功率和最终成功率。
- 比较每个被接受任务的成本,而不仅仅是每个 token 的成本。
- 将超载测试与质量和延迟基准分开运行。
如果您正在同时评估供应商的可靠性和成本,可以将此流程与 AI API 定价对比 配合使用。
生产环境的限流架构
一个稳健的请求路径通常包含五层控制:
- 准入控制会拒绝或延后无法满足其截止时间的工作。
- 令牌预留会估算请求可能消耗的配额成本。
- 限流器会对每个提供商、模型池、租户和优先级类别进行节流。
- 重试控制器会使用带抖动的有限重试预算。
- 路由器会在重试预算或容量策略要求切换后,选择一个合同兼容的替代方案。
client
→ admission control
→ priority queue
→ RPM + token reservation limiter
→ provider/model route
→ bounded retry
→ contract-safe fallback
→ usage and latency logs
不要在每个应用工作线程中放置一个无限重试循环。应集中管理策略,使所有调用方都共享对可用容量的同一理解。
值得告警的指标
按提供商、模型、项目、路由、租户和工作负载跟踪以下指标:
- 每分钟请求数和每分钟令牌数。
- 预留的估算令牌数与实际使用令牌数。
- 首次尝试成功率。
- 每个成功请求的重试次数。
- 按分类原因划分的
429比率。 - 队列深度和最老项的年龄。
- 等待限流容量所花费的时间。
- 端到端 p50、p95 和 p99 延迟。
- 回退率和回退结果。
- 每个成功或被接受任务的成本。
仅对总 429 数量发出告警会产生很多噪音。更好的信号是将节流率与队列年龄、重试放大效应以及最终失败率结合起来看。
常见错误
将 RPM 视为并发限制
RPM 衡量的是一段时间内的接纳次数;并发衡量的是正在进行中的工作。缓慢请求即使在较低 RPM 下也可能造成很高的并发。
对每个 429 都立即重试
立即重试会让工作线程同步化并消耗更多容量。如有可用,应遵守服务器给出的时序,并加入抖动。
为每个模型使用同一个限流器
提供商可能使用独立或共享的资源池。特定模型的策略应遵循提供商文档中定义的配额范围以及实际观察到的响应头。
忽略共享消费者
仪表盘、批处理任务和生产 API 可能共享同一个项目配额。应按工作负载预留容量,并在可能的情况下隔离关键流量。
重试部分流式输出
一旦令牌已经到达用户端,重新播放该请求可能会重复内容或工具操作。应明确定义继续或重新开始的语义,而不是静默重试。
Flatkey 如何改变运营模型
Flatkey 提供一个 API 密钥和一个兼容 OpenAI 的基础 URL,用于访问受支持的模型提供商。这为应用提供了一个统一的集成接口,同时仍然允许路由策略考虑模型匹配、容量、可靠性和成本。
网关不会移除上游限流。它让围绕这些限制实施一致的控制变得更容易:一个客户端集成、集中式请求日志,以及在路由受限时将符合条件的流量迁移的选项。有关更广泛的路由设计,请查看 AI API 网关架构指南,或者在选择生产路由前查看 当前 Flatkey 定价。
常见问题
RPM 和 TPM 有什么区别?
RPM 限制单位时间内允许通过的请求数量。TPM 限制允许通过的输入和/或输出 token 数量。小型提示通常先给 RPM 带来压力;大上下文或高输出工作负载通常先给 TPM 带来压力。
为什么在低于 RPM 限额时仍会出现 429 错误?
提供方可能会执行更短的滑动窗口、token 桶、共享项目配额、单独的 token 限额、加速限制,或按模型划分的池。你的本地请求计数器可能并不代表完整的配额范围。
每个 429 都应该重试吗?
不应该。对短暂的限流使用少量重试预算并加上抖动。不要反复重试每日配额耗尽、计费限制,或没有恢复时间的持续过载。
指数退避能保证成功吗?
不能。退避可以减少冲突,并给短暂容量恢复的时间。它不能创造配额。持续性耗尽需要降低需求、增加容量、延后工作,或使用另一个符合条件的路由。
LLM 请求应该重试多少次?
没有统一的数字。应根据用户可见的延迟预算和失败模式来设置重试次数。许多交互式应用在失败前或改路由之前只应允许少量、短时间的尝试;离线任务可以容忍更长的队列等待。
重试会计入限流吗?
会。提供方可能会将失败尝试计入活跃限额,因此必须监控并限制重试放大效应。
最终检查清单
- 分别建模 RPM、TPM、每日限制和并发。
- 根据请求上限与 token 上限中的较小值计算容量。
- 以安全系数在公开最大值以下运行。
- 对持续负载使用队列和节流节奏。
- 遵守
Retry-After,并使用带抖动的指数退避。 - 限制尝试次数和总重试时间。
- 不要自动重放部分流或副作用操作。
- 仅路由到能够保持所需契约的模型。
- 在评估中将队列和重试延迟与模型延迟分开。
- 关注重试放大和队列时长,而不只是原始
429数量。
限流首先是容量规划问题,然后才是重试问题。一旦你独立衡量请求频率、token 吞吐量、并发和重试放大,429 错误就会从不可预测的生产噪音变成可操作的信号。



