Rate Limit
@vafast/rate-limit 按客户端 key 限制请求频率。每个 key 在时间窗口内最多允许 max 次请求;超限返回 429,并可写入 RateLimit-* / Retry-After 响应头。
先搞清几个概念(给新用户)
限流 key 是什么?
中间件不会「按连接」或「按路由」天然限流,而是把每次请求映射成一个字符串 key,再在 context(默认内存表)里对该 key 计数。
默认 key 来自客户端 IP 相关请求头(见下方顺序)。反代后若头不可信,或你想按用户 / API Key 限流,应自定义 generator。
skip 语义(务必记清)
skip 返回值 | 行为 |
|---|---|
true | 跳过限流:不生成计数(若尚未生成)、不递增、不写限流头 |
false | 应用限流:生成 key(若需要)、递增、可能 429 |
默认 skip: () => false,表示对所有请求都限流。
skip.length 会影响何时生成 key:
skip.length < 2(只接收req):先skip(req);未跳过再generatorskip.length >= 2(接收req, key):先generator,再skip(req, key)
这样「仅按路径跳过」时可避免无谓的 key 计算;「按 key 决定是否跳过」时则先有 key。
计数与超限判定
流程是 先 increment,再判断:
current >= max + 1 → 429例如 max: 10 时,第 11 次请求会被拒绝。窗口长度由 duration(毫秒)决定;Retry-After 约为 ceil(duration / 1000) 秒。
默认内存存储与多实例
DefaultContext 使用进程内 LRU 计数。多进程 / 多副本部署时,各实例计数互不相通,全局 QPS 上限大约是 max × 实例数。需要集群级限流时,实现自定义 Context(如 Redis)并传入 context 选项。
安装
npm install @vafast/rate-limit快速开始
import { Server, defineRoute, defineRoutes, json, serve } from 'vafast'
import { rateLimit } from '@vafast/rate-limit'
const routes = defineRoutes([
defineRoute({
method: 'GET',
path: '/',
handler: () => json({ ok: true }),
}),
])
const server = new Server(routes)
server.use(
rateLimit({
max: 100,
duration: 60_000,
}),
)
serve({ fetch: server.fetch, port: 3000 })用法
全局限流
server.use(
rateLimit({
max: 100,
duration: 60_000,
}),
)单路由限流
defineRoute({
method: 'POST',
path: '/login',
middleware: [rateLimit({ max: 10, duration: 60_000 })],
handler: () => json({ ok: true }),
})跳过探活等路径
skip 返回 true 表示跳过计数与限流:
server.use(
rateLimit({
max: 60,
duration: 60_000,
skip: (req) => {
const path = new URL(req.url).pathname
return path === '/' || path === '/health'
},
}),
)自定义限流 key
rateLimit({
max: 30,
duration: 60_000,
generator: (req) =>
req.headers.get('authorization') ??
req.headers.get('x-forwarded-for') ??
'anonymous',
})自定义超限响应(errorResponse 三种形态)
| 类型 | 行为 |
|---|---|
string | 以 429 文本响应返回该字符串(text(...));可附带限流头 |
Response | clone() 后返回;若 headers: true 会把 RateLimit-* / Retry-After 写到 clone 上 |
Error | 抛出该 Error(由上层错误处理接管) |
| 其它非上述类型 | 回落为文本 'Too Many Requests',状态 429 |
import { err } from 'vafast'
rateLimit({
max: 5,
duration: 60_000,
errorResponse: new Response(JSON.stringify({ error: 'Too Many Requests' }), {
status: 429,
headers: { 'Content-Type': 'application/json' },
}),
// 或抛错:errorResponse: err('Too Many Requests', 429)
// 或纯文本:errorResponse: 'rate-limit reached'
})自定义 Context(存储)
需要 Redis 等外部存储时,实现 Context 接口并传入:
| 方法 | 说明 |
|---|---|
init(options) | 中间件创建时调用;可读取 duration / max 等(不含 context 自身) |
increment(key) | 计数 +1,返回 { count, nextReset } |
decrement(key) | 计数 -1;countFailedRequest: false 且下游抛错时会调用 |
reset(key?) | 重置某个 key,或不传则清空全部 |
kill() | 进程结束时的清理钩子 |
import type { Context } from '@vafast/rate-limit'
import { rateLimit } from '@vafast/rate-limit'
const redisContext: Context = {
init() { /* ... */ },
async increment(key) { /* return { count, nextReset } */ },
async decrement(key) { /* ... */ },
async reset(key) { /* ... */ },
async kill() { /* ... */ },
}
rateLimit({ max: 100, duration: 60_000, context: redisContext })API
导出
| 导出 | 说明 |
|---|---|
rateLimit(options?) | 主入口,返回中间件 |
DefaultContext | 默认内存计数存储(LRU,构造参数 maxSize 默认 5000) |
defaultOptions | 默认配置常量 |
Options / Context / Generator | 相关类型 |
rateLimit(options?: Partial<Options>)
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
duration | number | 60000 | 计数窗口(毫秒);也用于 Retry-After |
max | number | 10 | 窗口内最大请求数 |
errorResponse | string | Response | Error | 'rate-limit reached' | 超限响应,见上节三种形态 |
countFailedRequest | boolean | false | false 时下游 抛错 会 decrement 退还计数 |
generator | (req, server, derived) => string | Promise<string> | defaultKeyGenerator | 限流 key |
context | Context | new DefaultContext() | 计数存储 |
skip | (req, key?) => boolean | Promise<boolean> | () => false | 返回 true 跳过限流 |
headers | boolean | true | 是否写 RateLimit-* / Retry-After |
injectServer | () => any | — | 传给 generator 的 server;一般不需要 |
scoping | 'global' | 'scoped' | 'global' | 兼容字段,当前实现未使用 |
默认 generator 请求头顺序
defaultKeyGenerator 按以下顺序取客户端地址,命中即返回:
x-real-ipx-forwarded-for(取逗号分隔的 第一个,并trim)cf-connecting-ipx-client-ip
若全部缺失:回退为 ua:${user-agent || 'unknown'},并 console.warn。
request 为 undefined 时返回空字符串并告警。
响应头(headers: true)
| 头 | 说明 |
|---|---|
RateLimit-Limit | 窗口上限(max) |
RateLimit-Remaining | 剩余次数 |
RateLimit-Reset | 距重置的秒数(向上取整) |
Retry-After | 仅超限时附加;约为 ceil(duration / 1000) 秒 |
skip 与 key 生成时机
skip.length < 2:先调用skip(req),未跳过再生成 keyskip.length >= 2:先生成 key,再调用skip(req, key)
判断条件为 (await skip(...)) === false 时才进入计数;其它真值均视为跳过。
最佳实践
- 登录、发短信等敏感接口用更小的
max,或单独挂路由级限流。 - 反代后务必保证 IP 相关请求头可信,或自定义
generator(例如按用户 ID / API Key)。 - 探活、静态资源等用
skip排除(返回true),避免误伤监控。 - 多实例部署时默认内存
DefaultContext不共享;需要全局限流请实现自定义Context。
注意事项
scoping仅为兼容保留,不会改变行为。skip返回true才跳过;默认() => false表示全部计数。- 超限判定为
current >= max + 1(先递增再判断)。 countFailedRequest: false时,仅对下游 抛错 退还计数;正常 4xx/5xx 响应仍会计数。- 多实例 + 默认内存存储 ≠ 集群统一限流。