HTTP 429 请求过多:读懂 Retry-After 并制定安全的重试策略
HTTP 429 表示服务器正在限制客户端的请求速率。先检查响应内容与 Retry-After,降低共享请求速率,再判断操作是否允许重复执行。重试循环必须设置总截止时间和尝试次数上限。
本指南内容
网站集成在手动测试时正常,批量任务启动后却可能报错。每个工作进程的请求看起来并不多,但合计起来可能超过整个账户的配额。应把 429 当作对总体负载的反馈,而不是让所有进程立刻再次发送相同请求。
本文提供排查记录、队列预算和等待时间计算示例。示例假设某服务公布了明确限制,并不代表任何特定 API 的实际配额。实施之前,应以返回错误的服务文档为准。
先确认究竟是哪一层返回了 429
记录请求时间、接口、HTTP 方法、状态码、请求 ID 以及与重试有关的响应头。不要把授权信息和个人数据写入日志。确认响应来自目标 API、反向代理还是 CDN 规则;仅凭错误页的品牌样式,无法判定具体限流机制。
MDN 说明,限制可能针对 IP、登录用户、应用程序或某个资源。响应正文或服务控制台可能给出配额类型。每分钟请求数、并发数与每日总额度需要不同的处理方式:降低队列速度不能恢复已经耗尽的每日额度。
- 请求增加是因为任务变多,还是某个任务进入了重复重试?
- 多个服务、浏览器标签页或 API 密钥是否共用账户配额?
- 限制针对请求数、活跃请求数、令牌量,还是其他资源?
- 文档规定的恢复时间是否仍在本次用户操作的截止时间以内?
按正确单位解析 Retry-After
Retry-After 可以是非负整数秒数,也可以是 HTTP 日期。例如值为 12 时,表示收到响应后等待十二秒,不是十二毫秒。若为日期,应使用可靠时钟计算剩余间隔;日期已经过去,也不意味着可以无限快速重试。
对于可以等待的队列,应把有效的服务器等待时间作为下限。大量进程同时恢复时,可以再加入少量随机延迟。如果该等待时间超过任务剩余期限,应停止或重新安排任务,不要偷偷把它截短为本地上限后继续过早请求。
| 响应信息 | 含义 | 本次任务的处理 |
|---|---|---|
| Retry-After: 12 | 等待时间为 12 秒 | 期限允许时,至少等待 12 秒 |
| Retry-After: HTTP 日期 | 间隔取决于接收时间与时钟准确性 | 计算非负间隔并检查截止时间 |
| 响应头缺失或无效 | 服务器没有提供可用等待时间 | 使用服务规定的策略或有上限的本地退避 |
| 服务说明每日额度耗尽 | 短时间内重试无法恢复额度 | 停止,待额度恢复后再继续 |
为共享队列做预算,而不是分别限制每个进程
假设整个账户每分钟允许 120 次请求,当前任务需要 600 次。没有其他调用时,单看数量也至少需要五分钟额度。若十秒内发送全部请求,再休眠五十秒,仍然形成了突发流量;即使一分钟平均值符合要求,服务仍可能拒绝。
示例把运行目标设为每分钟 90 次,为其他工作留出空间。这相当于每秒 1.5 次,名义启动间隔约 667 毫秒。600 次请求约需 400 秒,尚未计算响应耗时和重试。这些只是规划数字,不能保证与所有服务的时间窗口一致。
若平均响应耗时为 2 秒,在稳定负载下,利特尔定律给出的平均在途请求约为 1.5 × 2 = 3。信号量可以限制并发,共享速率限制器则控制请求启动间隔;它们解决不同问题。重试也必须消耗同一个请求预算。
Example workload assumptions:
Account allowance: 120 requests/minute
Operating target: 90 requests/minute = 1.5 requests/second
600 requests / 1.5 requests/second = 400 seconds
Average concurrency at 2 seconds/response: 1.5 × 2 = 3未提供等待时间时,使用有界退避与随机抖动
一种可用的回退策略是失败后逐步扩大等待窗口,再在窗口内随机选择延迟。例如窗口依次为一、二、四、八秒,同时设置独立的总截止时间。随机抖动能让多个进程错开,避免它们每次都在同一时刻重新冲击服务。
下面的函数只计算等待时间,不发送请求,也不是完整的重试客户端。调用方还必须负责截止时间、次数上限、取消以及重复操作的安全性。浏览器脚本要读取 Retry-After 时,跨域服务还可能需要通过 CORS 暴露该响应头。
function retryDelayMs(retryAfter, attempt, nowMs = Date.now()) {
const text = String(retryAfter ?? "").trim();
if (/^\d+$/.test(text)) {
const milliseconds = Number(text) * 1000;
return Number.isSafeInteger(milliseconds) ? milliseconds : Infinity;
}
if (/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(text)) {
const dateMs = Date.parse(text);
if (Number.isFinite(dateMs)) return Math.max(0, dateMs - nowMs);
}
const windowMs = Math.min(8000, 1000 * 2 ** attempt);
return Math.floor(Math.random() * windowMs);
}
// Example policy: at most 4 retries and a 30-second total deadline.
// If the required wait exceeds the remaining deadline, stop.仅在重复执行安全时重试
读取资源与创建付款并不是同一种操作。重试写入前,先检查服务的幂等性约定,并判断上次请求是否可能已经生效。服务支持时,为同一次逻辑操作复用文档要求的幂等键;每次重试都生成新键,可能导致创建多个独立操作。
SDK 本身也可能自动重试。在它外面再套一层循环,会放大实际请求数。应明确所有层合计最多会发出多少次网络请求。面向用户的操作应在等待过久时给出可理解的稍后重试状态,而不是让按钮一直转圈。
- 为整个任务设置统一截止时间,并向队列中的工作传播取消信号。
- 在共享配额范围内,同时约束并发数量和启动速率。
- 等待或重试之前,释放已完成响应占用的资源。
- 额度耗尽,或服务说明错误不可恢复时,停止尝试。
- 记录每次尝试和最终结果,让监控能够看到重试成本。
用可控的小批量验证修复
先执行一批可以准确计数的读取任务,记录首次尝试、重试、成功响应和最终失败。确认实际启动间隔与并发数符合计划。随后用已知 Retry-After 的模拟 429 测试等待逻辑,不要反复压测真实服务来验证错误处理。
使用请求速率计算器在每秒与每分钟之间换算目标,再用并发计算器检验延迟假设。把计算结果与服务真实配额逐项核对。若仍有 429,应先调查其他共享调用方和配额类型,而不是盲目延长延迟。为排查记录保留配置版本,便于区分限流规则变化与客户端修改带来的结果。
常见问题
429 是否表示我的 IP 被封禁?
不一定。它说明发生了速率限制,规则可能针对账户、令牌、路由、IP 或更大范围的服务。应先阅读响应信息和服务文档,再决定处理方式。
缺少 Retry-After 时应立即重试吗?
不应仅因缺少该响应头就立刻重试。优先采用服务文档规定的策略;否则使用有次数和时间限制的退避与抖动,并在共享范围内控制总尝试量。
增加工作进程能解决请求过多吗?
更多进程可能进一步挤占共享额度。先确认限制的维度,再协调队列速率与并发。只有服务约定允许增加容量时,扩容才可能解决问题。
来源与定义
方法核验日期: