API 429 Too Many Requests 怎么解决?先定位限流来源再安全重试
简答:API 429 Too Many Requests 表示请求在某个时间窗口内超过了服务端、网关或账户限制,但不等于一定是代码频率过高。先记录状态码、响应体、Retry-After、RateLimit 或 X-RateLimit 响应头和请求 ID,再区分临时限流、Token/并发限制、每日额度与支出上限;临时限流使用带随机抖动的指数退避,额度或支出限制则应降低用量、检查控制台或申请调整,不能无限重试。
定义:API 429 Too Many Requests 是 HTTP 状态码,表示服务端在一定时间内限制了当前客户端、凭证、项目、资源或组织的请求速率;RFC 6585 允许响应通过 Retry-After 告知客户端何时再次尝试。
先做三步分流:不要看到 429 就盲目重试
本站是独立的 AI API 接入指南站,并非 FishXCode 官方站;本文只依据公开文档和通用 HTTP 规则说明排查方法。
- 确认限流层:直接请求上游时查看上游响应;经过客户端、反向代理或 API Router 时,同时保存每一层的状态码、响应头和日志。若客户端日志显示 429、代理日志没有 429,而响应体带有上游请求 ID,优先检查上游;若代理直接返回 429,则检查代理的 IP、Key、路由和并发规则。
- 确认限制维度:检查请求数、并发连接、输入或输出 Token、每日额度、项目额度、组织支出上限,以及是否存在短时突发限制。不同平台的维度并不相同,不能把某个平台的 RPM 直接套到另一个平台。
- 确认是否可重试:有 Retry-After 或明确的短时限流信息时,可以在总截止时间内退避;若响应表示每日额度、月度支出上限或凭证被限制,应该停止自动重试,转为检查控制台、降低用量或申请调整。
HTTP 429 的标准含义和 Retry-After 语义可参考 RFC 6585;通用响应头说明也可参考 MDN 的 429 状态码说明。
429 速查表:临时限流、Token 限制和额度上限要分开
| 现象 | 优先检查 | 处理动作 | 是否适合自动重试 |
|---|---|---|---|
| 短时间集中请求后出现 429 | Retry-After、请求速率、并发数 | 降低并发,加入指数退避和随机抖动 | 可以,但要限制次数和总时间 |
| 请求数量正常但大输入后出现 429 | 输入 Token、输出 Token、模型或路由限制 | 缩短上下文、降低输出上限、排队或拆分任务 | 仅在服务端提示为临时限制时重试 |
| 每天或每月持续失败 | 每日配额、项目额度、组织支出上限 | 查看用量与账单页面,降低用量或申请调整 | 通常不应持续重试 |
| 经过 API 网关后才出现 429 | 网关按 IP、Key、用户或路由设置的规则 | 查看网关日志、桶容量、并发和队列设置 | 先确认网关是否允许重试 |
| 同时伴随 503 或 529 | 服务端容量、上游可用性和请求 ID | 按服务商规则退避,并保留请求记录 | 可按官方错误处理说明重试 |
例如,Google Gemini 文档将 429 的原因细分为 RPM、TPM、RPD、模型限制和支出限制;其官方排障页建议对可重试错误使用指数退避,并设置最大重试次数。请以 Gemini Rate limits 和 Gemini Troubleshooting guide 中当前显示的限制为准。
读取响应头和响应体:先留下能定位问题的证据
每次 429 至少记录以下字段,但不要记录完整 API Key:
- HTTP 状态码、请求方法、目标路径和服务商名称。
Retry-After:可能是秒数,也可能是 HTTP-date,不能只按整数解析。RateLimit-*、X-RateLimit-*或服务商专用限流头:记录原始值,不要假设字段一定存在。- 响应体中的错误类型、消息、配额维度和请求 ID。
- 客户端重试次数、等待时间、总耗时、并发数和请求是否幂等。
OpenAI API 文档列出了可用于排查的请求 ID及多项 x-ratelimit-* 响应头,具体字段应以实际接口返回为准,参考 OpenAI API Reference。Anthropic 的错误响应包含顶层错误对象和 request_id;其 429 可能对应速率限制或支出上限,参考 Claude API errors。
指数退避怎么写:Retry-After 优先,缺失时再用退避公式
推荐的等待顺序是:服务端给出合法的 Retry-After 时优先采用;没有该字段时使用 baseDelay × 2^attempt + random(0,jitter),再限制到最大等待时间,并在等待前与总截止时间比较。随机抖动可以减少多个工作进程同时重试造成的再次拥塞。
- 最大重试次数:按业务设置有限次数,例如 3 至 5 次;不要用无限循环。
- 总截止时间:每次请求、等待和读取响应都要受 deadline 约束。
- 并发控制:使用信号量、令牌桶或队列限制同时在途请求,不能只降低循环速度。
- 熔断:连续 429 达到阈值后短暂停止向该路由发送请求,避免所有线程一起重试。
- 死信队列:达到重试上限后保存任务和错误上下文,交给人工或定时任务处理。
如果具体服务商提供 Batch 能力,可以评估将不要求实时返回的任务改为批处理;是否支持、限制和计费应以该服务商当前文档为准,不能据此假定所有 API 都有相同能力。
Python 可复制示例:处理秒数、HTTP-date、deadline 和幂等判断
下面示例使用标准库,默认只对明确标记为幂等的请求自动重试;非幂等 POST 不应仅因为收到 429 就无条件重复提交。示例会释放响应体,并在最终失败时抛出最后一次 HTTP 错误。
Python:import email.utils, random, time, requests
def retry_after_seconds(value):
if not value: return None
try: return max(0.0, float(value))
except ValueError:
try: return max(0.0, email.utils.parsedate_to_datetime(value).timestamp() - time.time())
except (TypeError, ValueError, OverflowError): return None
def request_with_retry(method, url, *, headers=None, json=None, idempotent=False, max_retries=4, total_timeout=30):
deadline = time.monotonic() + total_timeout
last = None
for attempt in range(max_retries + 1):
remaining = deadline - time.monotonic()
if remaining <= 0: raise TimeoutError('retry deadline exceeded')
response = requests.request(method, url, headers=headers, json=json, timeout=remaining)
if response.status_code != 429 or not idempotent:
response.raise_for_status()
return response
last = response
response.close()
if attempt == max_retries: break
delay = retry_after_seconds(response.headers.get('Retry-After'))
if delay is None: delay = min(8.0, 0.5 * (2 ** attempt)) + random.uniform(0, 0.25)
delay = min(delay, max(0.0, deadline - time.monotonic()))
if delay <= 0: break
time.sleep(delay)
raise RuntimeError(f'429 after {max_retries + 1} attempts; last={last.status_code if last else None}')
JavaScript 可复制示例:Retry-After 支持 HTTP-date,等待不超过总截止时间
该实现使用原生 fetch,显式处理最终失败分支。对于非幂等请求,调用时传入 idempotent: false;若业务需要重试写入操作,应配合服务商支持的幂等键和业务去重。
JavaScript:function retryAfterMs(value) {
if (!value) return null;
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
const date = Date.parse(value);
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : null;
}
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function fetchWithRetry(url, options = {}, { idempotent = false, maxRetries = 4, totalTimeoutMs = 30000 } = {}) {
const deadline = Date.now() + totalTimeoutMs;
let lastResponse = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const remaining = deadline - Date.now();
if (remaining <= 0) throw new Error('retry deadline exceeded');
const response = await fetch(url, { ...options, signal: AbortSignal.timeout(remaining) });
if (response.status !== 429 || !idempotent) {
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
return response;
}
lastResponse = response;
await response.arrayBuffer();
if (attempt === maxRetries) break;
let delay = retryAfterMs(response.headers.get('retry-after'));
if (delay === null) delay = Math.min(8000, 500 * (2 ** attempt)) + Math.random() * 250;
delay = Math.min(delay, Math.max(0, deadline - Date.now()));
if (delay <= 0) break;
await sleep(delay);
}
throw new Error(`429 after ${maxRetries + 1} attempts; last=${lastResponse?.status ?? 'none'}`);
}
Java 与 Go 的实现要点:把重试放在请求边界,不要重复提交未知结果
Java 使用 HttpClient 时,应为每次请求设置 HttpRequest 超时,并用 Instant.now() 计算总 deadline;读取并关闭响应体后,再解析 Retry-After 的秒数或 HTTP-date。Go 使用 context.WithTimeout 传递总截止时间,读取并关闭 resp.Body,用 http.Header.Get("Retry-After") 解析等待值。两种语言都应在重试前判断请求是否幂等,并在循环结束后返回最后一次错误,不能只记录日志后静默返回。
如果请求可能已经在服务端执行但客户端没有收到结果,尤其是支付、创建资源或写入操作,应优先使用服务商提供的幂等键、业务唯一号或查询接口确认结果,再决定是否重试。HTTP 429 本身不能证明上一次非幂等操作没有执行。
完整排查表:429、403、503 和超时分别怎么处理
| 错误 | 通常表示什么 | 首要动作 | 不应直接做什么 |
|---|---|---|---|
| 401 | Key、签名或认证信息有问题 | 核对 Key、请求头和环境变量 | 不要靠重试解决鉴权错误 |
| 403 | 权限、项目状态、策略或资源访问被拒绝 | 检查项目、组织、工作区和权限 | 不要把 403 当成 429 延迟重试 |
| 404 | 路径、版本、资源或模型标识不存在 | 核对 Base URL、API 版本和资源路径 | 不要通过增加并发重试 |
| 429 | 速率、并发、Token、配额或支出限制 | 读取响应头和错误体,分辨临时限制与额度上限 | 不要无限重试或同时放大并发 |
| 503 | 服务暂时不可用或容量不足 | 按官方建议退避,记录请求 ID和时间 | 不要把它简单归因于账户额度 |
| 504 或客户端超时 | 在截止时间内没有拿到响应 | 缩短请求、调整超时并确认上游是否已执行 | 不要对未知结果的非幂等写入盲目重放 |
Anthropic 当前错误文档将 429、503、504 和 529 分开说明,并指出其官方 SDK 对部分瞬时失败会自动退避;如果你使用 SDK,先确认 SDK 的默认重试次数,避免与业务层重试叠加。具体行为以 Anthropic 错误文档 为准。Google 的错误文档也将 429 的速率限制、每日配额和支出限制分开列出,参考 Gemini API errors。
修复顺序:从最小改动开始,避免把问题扩大
- 先用单请求复现,并保存状态码、响应体、响应头和请求 ID。
- 确认 Base URL、模型、Key 和请求路径没有在多个配置层重复拼接或错误路由。
- 把并发降到单线程或小队列,观察 429 是否消失。
- 加入有限次数的指数退避、随机抖动、单请求超时和总 deadline。
- 对输入 Token、输出上限、分页和轮询频率做减量。
- 若仍持续返回 429,检查项目、组织或工作区的用量、配额和支出控制;这类情况通常不是等待几秒就能解决。
- 将最终失败任务放入死信队列,保留原始请求摘要、错误类型和追踪 ID,避免丢失任务或重复执行。
对于 OpenAI 等平台,响应头中的限流字段和请求 ID适合接入监控;对于 Anthropic 和 Gemini,应同时解析官方错误体中的类型、消息及配额提示。字段可能随 API 版本变化,生产代码应对缺失字段保持兼容。
参考资料
- RFC 6585:Additional HTTP Status Codes
- MDN:429 Too Many Requests
- OpenAI:Rate limits
- OpenAI:API Reference
- Anthropic:Rate limits
- Anthropic:Claude API errors
- Google Gemini:Rate limits
- Google Gemini:Troubleshooting guide
- Google Gemini:API errors
常见问题
API 429 Too Many Requests 一定是请求太快吗?
不一定。429 可能来自请求速率、并发数、输入或输出 Token、每日配额、模型限制、项目额度或支出上限。应先查看 Retry-After、限流响应头、响应体和控制台用量,不能只降低循环速度。
没有 Retry-After 时,429 应该等待多久?
没有统一秒数。可以使用带随机抖动的指数退避,例如从较短等待开始逐步增加,并设置最大等待、最大重试次数和总截止时间;如果错误体明确表示额度或支出上限,则停止自动重试。
429 需要换 Key 或换模型吗?
不要把换 Key 或换模型当作默认修复方案。先确认限制是按 IP、Key、项目、组织、模型还是路由计算;如果是账户额度或组织支出上限,换 Key 可能无效,也可能造成用量记录混乱。
POST 请求收到 429 可以直接重试吗?
只有在请求幂等、服务商支持幂等键,或你能通过查询接口确认上一次没有执行时才适合自动重试。支付、创建资源和写入操作可能出现客户端超时但服务端已执行的情况,应先做去重或查询。
为什么加了 SDK 重试后仍然持续 429?
SDK 的重试通常只适合临时错误;如果触发的是每日配额、月度支出上限或持续的加速限制,继续重试不会改变限制。还要检查 SDK 默认重试与业务层重试是否叠加,避免实际请求量进一步增加。
API 网关返回 429,应该先查上游平台吗?
先看响应来自哪一层。若网关日志显示本地规则直接拒绝,应查 IP、Key、用户、路由和并发配置;若网关只是转发上游 429,则结合上游响应体、请求 ID和限流头排查。