概述
只有 429 而没有上下文,会迫使客户端盲目重试或猜测退避时间。RateLimit 响应头三件套分别给出配额上限、剩余额度和重置秒数,行为良好的客户端可以在被拒绝前主动降速。Retry-After 仍然是重试时机的权威信号。
步骤
- 使用固定窗口计数,并在任何提前返回之前写入响应头:
const used = await redis.incr(`rl:${req.apiKey}:${windowStart}`);
const remaining = Math.max(0, 100 - used);
res.set({
'RateLimit-Limit': '100',
'RateLimit-Remaining': String(remaining),
'RateLimit-Reset': String(60 - (Math.floor(Date.now() / 1000) % 60)),
});
if (used > 100) {
res.set('Retry-After', '30');
return res.status(429).json({ error: 'rate_limited' });
}
- 网关层自行拒绝请求时也要输出相同响应头,避免客户端看到互相矛盾的配额。
- 用循环请求验证:
for i in $(seq 1 3); do curl -s -D - -o /dev/null https://api.example.com/v1/items | grep -i -E "ratelimit|retry-after"; done。 - 在 OpenAPI 中声明这些响应头,使生成的 SDK 能暴露剩余额度。
验证
预期每次响应的 Remaining 依次递减,Reset 在 1 到 60 之间;第 101 次请求返回 429,且 Retry-After 与 Reset 一致。网关层触发限流时同样应包含这些头,而不是裸 429。
注意事项
- 响应头名称来自 Internet-Draft,未来可能调整,应在文档中标注版本。
- 固定窗口在边界允许突发,相邻两个窗口合计可能达到两倍配额;需要平滑时改用令牌桶或滑动窗口。
- 计数器必须放在共享 Redis 或集中式网关,否则每个副本各自计数,实际配额会成倍放大。
- 提前返回的分支最容易漏设响应头,应在统一中间件中构造。