使用 Codex 开发登录、搜索、AI接口、短信验证码或公开 API 时,限流几乎是绕不开的一环。

最简单的写法通常是:

if (requestCount > 100) {
  throw new TooManyRequestsError();
}

刚开始看起来没有问题,但项目真正上线以后,经常会遇到:

  • 正常用户突然收到 429 Too Many Requests

  • 一个公司网络里的几十个人互相影响;

  • 页面同时请求多个接口,很快达到限制;

  • 用户刚到一分钟边界,额度突然全部恢复;

  • Codex为了防刷把限制越调越低,误伤反而越来越严重;

  • 客户端收到429以后立即重试,导致接口压力更大。

这类问题通常不是“限流值设置得不够大”,而是限流维度和算法选错了


一、最简单的固定窗口有什么问题?

假设规则是:

每个用户每分钟最多100次请求

实现方式:

10:00:00 - 10:00:59
最多100次

到了:

10:01:00

计数直接清零。

这就是 Fixed Window。

问题在于,用户可以在:

10:00:59
发送100次

然后:

10:01:00
再次发送100次

理论上一秒左右就产生:

200次请求

虽然每个自然分钟都没有超过100次,但瞬时流量已经远超预期。

所以限流不能只看“一个时间段总共多少次”,还要考虑请求在时间上的分布。


二、为什么按IP限流特别容易误伤?

Codex 很容易生成:

const key = `rate-limit:${req.ip}`;

对于公开接口,这确实简单。

但现实中很多用户可能共享同一个出口 IP:

公司办公室
学校
酒店
移动网络
NAT
代理网关

假设一家公司有:

50名员工

共享同一个公网 IP。

如果规则是:

每IP每分钟100次

平均每个人只能:

2次/分钟

结果就是:

真正的正常用户互相抢额度。

如果用户已经登录,通常更合理的是优先使用:

userId
API Key
tenantId

作为限流维度。

IP可以作为辅助,而不是所有业务唯一依据。


三、不同接口不能使用同一个限流规则

例如:

GET /profile
POST /login
POST /sms/send
POST /ai/generate

这些接口的成本完全不同。

如果统一:

100次/分钟

并不合理。

例如用户资料查询可能很轻:

200次/分钟

都没有明显压力。

但短信验证码如果允许:

100次/分钟

风险非常高。

可以根据接口成本划分:

普通读取:
较宽松

登录:
中等限制

验证码:
严格限制

AI生成:
按照用户额度与并发限制

限流应该围绕资源成本和滥用风险设计。


四、什么是Token Bucket?

Token Bucket,也就是令牌桶,是非常常用的限流算法。

可以把它想象成一个桶:

桶容量:10个Token

系统按照固定速度往桶里增加令牌:

每秒增加2个

每个请求需要消耗:

1个Token

如果还有令牌:

允许请求

如果桶已经空了:

拒绝或等待

它的优势是:

既能限制长期平均速率,又允许短时间合理突发。


五、为什么Token Bucket比固定窗口更自然?

假设正常用户平时:

每秒1个请求

但打开一个复杂页面时可能同时产生:

5个接口请求

固定规则如果只允许:

每秒2次

就会马上产生429。

Token Bucket可以设置:

桶容量:10
补充速度:2 Token/s

用户短时间一次发5个请求:

直接消耗5个Token

仍然可以正常工作。

但如果持续高速请求:

每秒20次

令牌很快耗尽,随后开始限流。

这通常更符合真实用户行为。


六、滑动窗口适合更精确的请求次数限制

另一个常见方案是 Sliding Window。

假设规则:

任意连续60秒最多100次

它不是简单按照:

10:00
10:01

切窗口。

而是每次请求都观察:

当前时间往前60秒

有多少请求。

这样可以避免固定窗口边界产生的流量突刺。

代价是实现和存储成本通常比固定窗口高。

因此可以根据场景选择:

简单内部接口
→ 固定窗口可能够用

公开API
→ 滑动窗口更精确

需要允许突发
→ Token Bucket

七、不要让所有请求消耗相同额度

如果接口中存在不同操作:

查看列表
导出Excel
生成报表
AI推理

资源消耗完全不同。

可以设计 Weight:

普通查询:1 Token
复杂搜索:3 Token
文件导出:10 Token
AI任务:20 Token

这样不会出现:

一个昂贵AI任务
=
一个简单健康检查

都只计算一次请求。

对于高成本服务,按“成本”限流通常比只按“请求次数”更合理。


八、并发限制和频率限制不是一回事

假设用户每分钟只请求20次,但每个请求需要30秒。

最终可能同时存在:

20个长任务

服务器仍然会压力很大。

所以除了:

每分钟多少次

还需要考虑:

同时运行多少个任务

例如:

每用户:
每分钟60次
同时最多3个任务

这两个规则解决的是不同问题。

Rate Limit

控制:

一段时间发多少请求

Concurrency Limit

控制:

同时运行多少请求

AI生成、导出、视频处理等长任务尤其需要并发限制。


九、429响应应该告诉客户端什么时候再试

不要只返回:

{
  "error": "Too many requests"
}

可以增加:

Retry-After: 10

或者返回:

{
  "code": "RATE_LIMITED",
  "retryAfter": 10
}

客户端就知道:

10秒后再尝试

而不是立即重新请求。

这对防止重试风暴非常重要。


十、客户端不要收到429就立即循环重试

错误写法:

try {
  return await request();
} catch (error) {
  if (error.status === 429) {
    return request();
  }
}

如果服务端正在限流:

失败
→ 立即重试
→ 再失败
→ 再重试

反而进一步增加压力。

更合理的是指数退避:

第一次:1秒
第二次:2秒
第三次:4秒
第四次:8秒

并增加随机抖动:

const delay =
  baseDelay * 2 ** attempt
  + Math.random() * 500;

这样大量客户端不会同时再次冲击服务器。


十一、不要让登录失败和登录成功共用一个计数器

登录接口比较特殊。

真正需要限制的是:

连续失败尝试

如果用户密码正确,却因为前面输错几次导致:

账号直接无法登录

体验会比较差。

可以结合:

IP
账号
失败次数
设备
时间窗口

设计不同策略。

例如:

同一个账号
连续失败5次
→ 延迟

同一个IP
大量不同账号登录
→ 更严格限制

这样比简单的:

/login 每分钟10次

更精细。


十二、验证码接口需要多层限流

短信或邮件验证码不能只按用户限制。

因为攻击者可以不断换手机号。

可以同时设置:

单手机号:
60秒1次

单手机号:
每天最多10次

单IP:
每小时最多一定数量

单设备:
额外限制

不同维度一起工作。

否则一个维度很容易被绕过。


十三、Redis限流Key一定要设置过期时间

例如:

rate:user:1001

如果只增加计数:

await redis.incr(key);

却从不设置TTL,Redis里会留下大量永久Key。

更合理:

const count = await redis.incr(key);

if (count === 1) {
  await redis.expire(key, 60);
}

当然实际生产中应尽量使用原子操作或 Lua Script,避免并发情况下:

INCR成功
EXPIRE失败

形成没有TTL的Key。


十四、分布式限流不能放在单机内存里

单实例开发时:

const requestCounts = new Map();

看起来能工作。

但如果生产部署了:

实例A
实例B
实例C

用户的请求可能被负载均衡到不同机器。

每台服务器都认为:

当前只请求30次

实际用户可能已经请求:

90次

所以多实例环境通常需要:

Redis
API Gateway
集中式限流服务

维护统一状态。


十五、限流本身也要避免成为性能瓶颈

如果每个请求都要:

访问Redis多次
执行复杂Lua
写大量日志

限流系统自己也可能变慢。

需要关注:

限流Redis延迟
限流Key数量
命中率
拒绝率
Lua执行耗时

不要为了保护一个 10ms 的接口,增加一个 30ms 的限流流程。


十六、429数量必须监控

上线限流以后,不要只看:

服务没有崩

应该观察:

429比例
被限流用户数量
接口维度分布
IP维度分布
用户维度分布
限流后的重试量

例如:

正常情况:
429 = 0.2%

上线新规则:
429 = 18%

很可能不是突然出现大量攻击,而是限流策略误伤了真实用户。


十七、给不同用户等级配置不同额度

部分系统可以根据业务套餐或角色提供不同额度:

普通用户:
60次/分钟

高级用户:
300次/分钟

内部服务:
独立额度

但规则不要散落在代码里:

if (user.plan === "x") ...

更适合集中配置:

const limits = {
  standard: {
    rate: 60,
    burst: 10
  },
  high: {
    rate: 300,
    burst: 50
  }
};

这样后续调整更清晰。


十八、让Codex先做限流地图

遇到429问题时,可以先要求:

请先不要修改代码。

分析当前API限流:

1. 当前按IP、用户还是API Key限流;
2. 使用什么算法;
3. 时间窗口多长;
4. 是否允许瞬时突发;
5. 哪些接口共享额度;
6. 是否存在并发任务限制;
7. 429后客户端如何重试;
8. 多实例之间是否共享限流状态。

先找到为什么误伤,再修改阈值。

不要看到429多就简单把:

100

改成:

1000

这样可能只是把真正的流量风险向后推。


十九、测试限流不能只循环调用101次

最简单的测试:

连续请求101次
→ 第101次返回429

还不够。

应该增加:

突发请求

1秒内连续10次

检查 Token Bucket 是否允许合理突发。

窗口边界

检查固定窗口是否出现双倍瞬时流量。

多用户

用户A达到限制后:

用户B必须仍然正常

多IP同用户

确认业务是否应该共享额度。

多实例

请求分配到不同服务实例,仍然不能突破全局限制。

429重试

确认客户端遵守:

Retry-After

而不是立即重试。


二十、把限流规则写进AGENTS.md

# API限流规则

- 已登录接口优先按userId或API Key限流
- IP限流不得默认作为唯一业务维度
- 高成本接口必须独立设置限制
- 长任务必须同时评估并发限制
- 限流算法需要评估突发流量
- 429响应必须提供合理的重试信息
- 客户端429重试必须使用退避策略
- 多实例环境禁止仅使用单机内存限流
- Redis限流Key必须有TTL
- 修改限流规则后必须监控429比例

这样 Codex 后续增加 Rate Limit 时,就不会只是加一个简单计数器。


二十一、Plus还是Pro?

如果主要使用 Codex 处理:

  • 单个接口限流;

  • Redis计数器;

  • 普通429问题;

  • 中小型后端服务;

Plus 通常已经能够覆盖大部分开发需求。

如果长期维护:

  • 大规模公开API;

  • 多租户平台;

  • 高并发服务;

  • API Gateway;

  • 多服务限流;

  • 大量监控和压测数据;

则可以根据实际开发强度评估 Pro。

不过限流系统真正的难点不是代码写得多复杂,而是:

既要阻止异常流量,又不能把真正的正常用户挡在门外。

总结

Codex 写 API 限流后频繁出现429,很多时候不是阈值设置得太低,而是限流算法、身份维度和接口成本没有设计清楚。

通过:

Token Bucket
Sliding Window
用户级限流
并发限制
Retry-After
指数退避

可以让限流从简单的“超过次数就拒绝”,变成更加符合真实用户行为的流量控制系统。

真正合理的 API 限流应该同时回答:

限制的是谁?限制什么资源?允许多大的瞬时突发?什么时候可以重新请求?

只有这几个问题明确以后,429才是在保护系统,而不是制造新的用户体验问题。

CSDN文章描述

本文介绍 Codex 编写 API 限流时常见的429误伤问题,并通过 Token Bucket、Sliding Window、用户级限流、并发控制和指数退避,优化高并发接口的流量控制策略。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐