Codex写API限流为什么总误伤正常用户?用Token Bucket解决429问题
使用 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、用户级限流、并发控制和指数退避,优化高并发接口的流量控制策略。
更多推荐




所有评论(0)