限流响应头
限流响应头(供应商 X-RateLimit-*、旧版 RateLimit-*、当前草案 RateLimit / RateLimit-Policy 与 Retry-After)告诉客户端当前的限流状态、剩余配额和重试时机。
[!info] related notes
- 所属 MOC: HTTP 与前端网络请求 MOC
- 总览: http-response-headers
- 关联概念: API 认证与安全、Go 限流
- 配合状态码: HTTP 429 Too Many Requests
限流响应头
一句话定义
限流响应头告诉客户端:你的请求配额是多少、还剩多少、什么时候重试。前端或 SDK 根据这些头做退避、提示和重试。
它要解决什么问题
API 限流是保护后端服务的基本手段。429 表示请求受到限流;服务可额外提供以下信息,帮助客户端决定是否及何时重试:
- 我还能发多少请求?(限流上限)
- 我还剩多少配额?(剩余次数)
- 我什么时候可以重试?(重试时机)
字段家族与版本
X-RateLimit-*(非标准但广泛使用)
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1716540000
| 头 | 含义 |
|---|---|
X-RateLimit-Limit | 时间窗口内的总配额 |
X-RateLimit-Remaining | 当前窗口内剩余配额 |
X-RateLimit-Reset | 此类示例常用 Unix 秒时间戳;单位必须查供应商合同 |
RateLimit-*(旧草案与兼容实现)
RateLimit-Limit: 1000
RateLimit-Remaining: 42
RateLimit-Reset: 60
| 头 | 含义 |
|---|---|
RateLimit-Limit | 时间窗口内的总配额 |
RateLimit-Remaining | 当前窗口内剩余配额 |
RateLimit-Reset | 配额重置前的秒数(不是时间戳) |
上述三字段写法属于旧版草案/兼容实现:示例 RateLimit-Reset 用相对秒数,X-RateLimit-Reset 的单位由供应商定义。不能仅凭字段名假定所有服务都遵守同一含义。
RateLimit / RateLimit-Policy(当前草案)
截至 2026-10-06,IETF Datatracker 最新为 draft-ietf-httpapi-ratelimit-headers-11,仍是 Internet-Draft,尚非 RFC。它采用结构化字段:
RateLimit-Policy: "default";q=1000;w=3600
RateLimit: "default";r=42;t=60
q/w 描述配额策略及窗口;第 11 版的 r 是可用配额,t 是有效窗口(秒),表示该窗口内不能使用超过 r 的配额,不是到点保证全部重置。配额单位也不一定等于请求次数。真实客户端应按服务采用的版本解析,不能用 parseInt 读取整个结构化字段;配额大于零也不保证下一请求成功。
Retry-After
RFC 9110 定义 Retry-After 为非负整数秒数或 HTTP 日期;常用于 429/503,也可用于重定向响应。它建议等待时长,不预留配额或保证重试成功。
HTTP/1.1 429 Too Many Requests
Retry-After: 60
或用 HTTP 日期格式:
HTTP/1.1 503 Service Unavailable
Retry-After: Sun, 24 May 2026 10:30:00 GMT
常见搭配
# 限流
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1716540000
# 服务不可用
HTTP/1.1 503 Service Unavailable
Retry-After: 120
前端如何使用
退避重试
下面只自动重试 GET 的 429;maxAttempts 包含首次请求。没有可用提示时指数退避并加抖动;服务器要求等待超过本例 30 秒预算时直接交还响应,不提前重试。写操作需单独设计幂等与请求体重放,不能直接套用。
function retryAfterMs(value, now = Date.now()) {
if (value == null) return null;
const text = value.trim();
if (/^\d+$/.test(text)) {
const ms = Number(text) * 1000;
return Number.isSafeInteger(ms) ? ms : Infinity;
}
// HTTP 日期以星期名开头;避免 Date.parse 把负数等当成日期。
if (!/^[A-Za-z]{3,9}(?:,| )/.test(text)) return null;
const date = Date.parse(text);
return Number.isFinite(date) ? Math.max(0, date - now) : null;
}
function wait(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(signal.reason);
const onAbort = () => {
clearTimeout(timer);
reject(signal.reason);
};
const timer = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, ms);
signal?.addEventListener("abort", onAbort, { once: true });
});
}
async function fetchGetWithRetry(url, { signal, maxAttempts = 3 } = {}) {
if (!Number.isInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 10) {
throw new RangeError("maxAttempts must be 1..10");
}
const started = Date.now();
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = await fetch(url, { method: "GET", signal });
if (res.status !== 429 || attempt === maxAttempts - 1) return res;
const advised = retryAfterMs(res.headers.get("Retry-After"));
const delay = (advised ?? 1000 * 2 ** attempt) + Math.random() * 250;
if (delay > 30_000 - (Date.now() - started)) return res;
// 丢弃的响应释放 body;最后交还的响应留给调用方读取。
await res.body?.cancel();
await wait(delay, signal);
}
}
日期计算依赖客户端时钟;这里用 Date.parse 演示浏览器日期解析,并非严格 HTTP-date 校验器。30 秒仅限制继续退避的预算,不能中断已开始的 fetch;调用方应传入带整体 deadline 的 AbortSignal。网络错误会直接抛出,最后一次 429 会原样返回,调用方仍须检查 status。
显示剩余配额
const res = await fetch("/api/data");
const remaining = res.headers.get("X-RateLimit-Remaining");
if (remaining !== null && /^\d+$/.test(remaining) && Number(remaining) < 10) {
showToast("API 调用次数即将用完,请稍后再试");
}
跨域场景
这些字段不在 CORS 安全列出的响应头集合中,包括标准 Retry-After;跨域 JS 读取需服务端显式暴露实际使用的字段:
Access-Control-Expose-Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, RateLimit, RateLimit-Policy, Retry-After
否则:
res.headers.get("X-RateLimit-Remaining") // null
边界与易混淆点
- X-RateLimit- 不是 RFC 标准*:是约定俗成的实践。不同 API 提供商的实现可能有差异。
- 草案必须标版本:旧 RateLimit-* 与第 11 版 RateLimit / RateLimit-Policy 不可混为一套固定协议。
- Retry-After 的值是秒数或 HTTP 日期:不是毫秒,也不是 Unix 时间戳;X-RateLimit-Reset 是另一个字段。
- 429 不一定带限流头:有些 API 只返回 429 状态码,不提供详细限流信息。这时客户端可按合同选择退避或停止重试,不能无限重试。
- 限流窗口设计影响用户体验:滑动窗口比固定窗口更平滑,但实现更复杂。
依据
- IETF Datatracker:RateLimit header fields:核对最新版本与发布状态。
- RFC 9110 §10.2.3:Retry-After 语法与用途。
- RFC 6585 §4:429 可携带 Retry-After,不是强制要求。