Skip to content

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);未跳过再 generator
  • skip.length >= 2(接收 req, key):先 generator,再 skip(req, key)

这样「仅按路径跳过」时可避免无谓的 key 计算;「按 key 决定是否跳过」时则先有 key。

计数与超限判定

流程是 increment,再判断

text
current >= max + 1  →  429

例如 max: 10 时,第 11 次请求会被拒绝。窗口长度由 duration(毫秒)决定;Retry-After 约为 ceil(duration / 1000) 秒。

默认内存存储与多实例

DefaultContext 使用进程内 LRU 计数。多进程 / 多副本部署时,各实例计数互不相通,全局 QPS 上限大约是 max × 实例数。需要集群级限流时,实现自定义 Context(如 Redis)并传入 context 选项。

安装

bash
npm install @vafast/rate-limit

快速开始

typescript
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 })

用法

全局限流

typescript
server.use(
  rateLimit({
    max: 100,
    duration: 60_000,
  }),
)

单路由限流

typescript
defineRoute({
  method: 'POST',
  path: '/login',
  middleware: [rateLimit({ max: 10, duration: 60_000 })],
  handler: () => json({ ok: true }),
})

跳过探活等路径

skip 返回 true 表示跳过计数与限流:

typescript
server.use(
  rateLimit({
    max: 60,
    duration: 60_000,
    skip: (req) => {
      const path = new URL(req.url).pathname
      return path === '/' || path === '/health'
    },
  }),
)

自定义限流 key

typescript
rateLimit({
  max: 30,
  duration: 60_000,
  generator: (req) =>
    req.headers.get('authorization') ??
    req.headers.get('x-forwarded-for') ??
    'anonymous',
})

自定义超限响应(errorResponse 三种形态)

类型行为
string429 文本响应返回该字符串(text(...));可附带限流头
Responseclone() 后返回;若 headers: true 会把 RateLimit-* / Retry-After 写到 clone 上
Error抛出该 Error(由上层错误处理接管)
其它非上述类型回落为文本 'Too Many Requests',状态 429
typescript
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()进程结束时的清理钩子
typescript
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>)

参数类型默认说明
durationnumber60000计数窗口(毫秒);也用于 Retry-After
maxnumber10窗口内最大请求数
errorResponsestring | Response | Error'rate-limit reached'超限响应,见上节三种形态
countFailedRequestbooleanfalsefalse 时下游 抛错decrement 退还计数
generator(req, server, derived) => string | Promise<string>defaultKeyGenerator限流 key
contextContextnew DefaultContext()计数存储
skip(req, key?) => boolean | Promise<boolean>() => false返回 true 跳过限流
headersbooleantrue是否写 RateLimit-* / Retry-After
injectServer() => any传给 generator 的 server;一般不需要
scoping'global' | 'scoped''global'兼容字段,当前实现未使用

默认 generator 请求头顺序

defaultKeyGenerator 按以下顺序取客户端地址,命中即返回

  1. x-real-ip
  2. x-forwarded-for(取逗号分隔的 第一个,并 trim
  3. cf-connecting-ip
  4. x-client-ip

若全部缺失:回退为 ua:${user-agent || 'unknown'},并 console.warn

requestundefined 时返回空字符串并告警。

响应头(headers: true

说明
RateLimit-Limit窗口上限(max
RateLimit-Remaining剩余次数
RateLimit-Reset距重置的秒数(向上取整)
Retry-After仅超限时附加;约为 ceil(duration / 1000)

skip 与 key 生成时机

  • skip.length < 2:先调用 skip(req),未跳过再生成 key
  • skip.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 响应仍会计数。
  • 多实例 + 默认内存存储 ≠ 集群统一限流。

相关链接