速率限制 如何避免被你接触的每个API返回429错误
HTTP 429 请求过多 — 如何解读 Retry-After 头部,实现带抖动的指数退避,理解令牌桶与漏桶算法的区别,并在自己的 API 上实现速率限制。
你触发了429错误。你的脚本在过去一小时内持续高频调用API,日志中充满了红色警告,而部署将在20分钟内完成。这一刻,这已不再是抽象概念。
HTTP 429 请求过多表示你在给定时间窗口内发送的请求数量超过了服务器允许的上限。正确解读响应内容,并以正确方式重试,是一项大多数开发者在遭遇问题后才掌握的技能。以下是完整情况。
429错误实际上告诉你什么
状态码只是信息的一半。真正重要的信息在响应头中:
Retry-After: 30— 重试前等待30秒,也可以是HTTP日期:Retry-After: Mon, 08 Jun 2026 15:00:00 GMTX-RateLimit-Limit: 100— 每个时间窗口允许的最大请求数X-RateLimit-Remaining: 0— 当前时间窗口内剩余的请求数(你现在已用尽)X-RateLimit-Reset: 1749391200— 时间戳,表示时间窗口重置的时间
并非所有API都会发送所有这些信息。GitHub会发送完整信息集。Stripe会发送 Retry-After。一些REST API不发送任何信息,要求你自行猜测。如果你有 Retry-After,请使用它——这是服务器告知你安全等待的最短时间。如果你不这样做,指数退避就是你的后备方案。
错误的重试方式
最简单的实现方式如下:
async function fetchWithoutBackoff(url) {
while (true) {
const res = await fetch(url);
if (res.ok) return res;
if (res.status === 429) continue; // immediately retry
}
}
这种方式是有害的。如果你的服务有10个实例同时触发429错误,并且都立即重试,那么所有重试请求将同时到达——这就是“群体冲击”问题。你将再次被限速,陷入一个可能无限循环的紧循环,使你的客户端看起来像是故意滥用API。
带抖动的指数退避
正确的模式是:每次重试的等待时间都比上一次更长(指数增长),并加入一个随机偏移量,以防止多个客户端的重试同步发生(抖动)。
async function fetchWithBackoff(url, options = {}, maxRetries = 5) {
let attempt = 0;
while (attempt <= maxRetries) {
const res = await fetch(url, options);
if (res.ok) return res;
if (res.status !== 429) {
throw new Error(`Request failed: ${res.status}`);
}
if (attempt === maxRetries) {
throw new Error(`Rate limited after ${maxRetries} retries`);
}
// Use Retry-After if provided; otherwise exponential backoff + jitter
const retryAfter = res.headers.get('Retry-After');
let waitMs;
if (retryAfter) {
const seconds = isNaN(retryAfter)
? (new Date(retryAfter) - Date.now()) / 1000 // HTTP date
: Number(retryAfter); // seconds
waitMs = seconds * 1000;
} else {
const baseDelay = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s, 8s, 16s
const jitter = Math.random() * 1000; // 0–1000ms random offset
waitMs = baseDelay + jitter;
}
console.log(`Rate limited. Waiting ${Math.round(waitMs / 1000)}s (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, waitMs));
attempt++;
}
}
抖动部分是大多数实现所忽略的。没有抖动,来自多个并行进程的重试请求仍会聚集成簇。有了抖动,它们会分散在等待窗口内。
对于返回 Retry-After的API floor ——如果在指定等待时间后仍然收到429错误,则在基础上叠加指数退避。
令牌桶与漏桶算法
两种算法主导了速率限制器的实现。理解你面对的是哪种算法,能告诉你API在压力下的行为方式,以及当你自己构建API时应选择哪种算法。
令牌桶
桶最多可容纳N个令牌。每次请求消耗1个令牌。令牌以固定速率补充(例如每秒10个)。如果桶为空,请求将被拒绝或排队。
支持突发请求。 如果你长时间未发送请求,你已积累令牌,可以一次性发送大量请求而不触发限制。GitHub的API就是如此——每小时最多5000次请求,但如果你长时间未使用API,可以一次性使用全部请求。适用于交互式场景,流量波动较大。
漏桶
请求进入队列,并以固定速率流出,无论请求到达速度如何。如果队列满载,新请求将被丢弃。
输出平稳,不支持突发。 即使你还有配额,请求也会以配置的速率逐步流出。Nginx的 limit_req 模块使用这种机制。更适合保护下游系统免受流量高峰冲击——适用于Webhook传递、外部API调用,以及任何对可预测吞吐量要求高于突发容忍度的场景。
当你自己实现时应选择哪种: 面向用户的端点需要突发容忍能力 → 令牌桶。Webhook传递或第三方API调用 → 漏桶。后台任务中对平稳吞吐量要求较高 → 漏桶。
计算安全的请求速率
在编写任何重试逻辑之前,先明确你实际被允许的请求速率。如果API说明“每小时1000次请求”,那就是每分钟16.67次或每秒0.278次。加上20%的安全缓冲,你将达到约每分钟13次——足以避免因两个时间窗口重叠导致的边缘情况。
使用 速率限制计算器 可将配额数字转换为每秒和每分钟的速率,确定请求之间的合适等待时间,并查看你的并发级别如何影响突发风险。
在你自己的API上实现速率限制
如果你是服务提供方,希望在你的API中加入正确的429行为:
- 选择合适的粒度。 按IP地址划分简单但对NAT或共享出口服务无效。按API密钥划分更好,但需要认证。按用户ID划分是理想选择,前提是拥有用户ID。不要在不了解哪种粒度更优的情况下混合使用。
- 始终返回
Retry-After. 一个429错误没有Retry-After会迫使每个客户端自行实现退避策略。你将获得更多的“群体冲击”,而不是减少。 - 使用Redis实现分布式速率限制。 内存中的计数器无法跨多个服务器实例工作。Redis是标准方案。像
INCR+EXPIRE这样的库 rate-limiter-flexible (Node) 和 slowapi (Python/FastAPI) 能正确抽象这一功能。 - 记录每次发出的429响应。 来自单一密钥的429数量激增,要么是客户端错误,要么是故意滥用。这两种情况都值得实时监控。
- 不要在认证失败时进行速率限制。 对于错误的凭据,返回401,而不是429。在认证失败时进行速率限制,会导致你在凭据轮换期间意外锁住自己的用户。
你现在应该做什么
如果你遇到429错误:
- 确认
Retry-After首先——如果API提供了,就使用它,不要自行发明延迟 - 实现带抖动的指数退避——上面的代码可以直接复制使用
- 记录每次响应中的
X-RateLimit-Remaining头信息——你可能比想象中更快地消耗了配额 - 缓存数据变化不频繁的响应
如果你在实现速率限制:选择基于Redis的库,每次429都返回 Retry-After ,监控每个密钥的429频率,并且不要在认证失败时进行速率限制。
429错误并非敌人——它是API告诉你确切出错原因以及(通常)需要等待多久的信号。大多数速率限制问题源于忽视这条信息并立即重试。不要这样做。
