用 Claude Code 实现 API Rate Limiting:429、Redis 与 Cloudflare 实战
用 Claude Code 实现安全的 API 限流:429、Redis、Cloudflare、防滥用和常见坑。
API rate limiting,也就是 API 限流,意思是:同一个用户、脚本或客户端在一段时间内只能发送有限次数的请求,超过后要等待。它不是把服务关掉,而是让过度使用的一方慢下来。可以把它想成热门店铺门口的号码牌:大家都能排队,但不能让一个人无限插队。
Claude Code 很擅长快速生成 endpoint、认证逻辑和测试。但一个能跑的 API 不等于能上线。登录尝试、搜索接口、AI 生成、短信验证码、邮件发送、Webhook 重试都会消耗真实资源。Masa 在测试一个小型联系表单时,曾经以为重复提交只是 UX 问题,结果 QA 阶段就消耗了数百封邮件额度。根本原因不是表单,而是没有提前定义“这个操作多久可以发生一次”。
这篇文章把限流拆成可执行的 Claude Code 工作流:先写清楚设计,再用 Node.js 做一个零依赖 demo,接着用 Redis 做多实例可用的 Express 实现,最后补上客户端重试、Cloudflare 放置位置、防滥用视角和常见失败案例。相关基础可以继续读Claude Code 生产 API 开发、Claude Code 安全最佳实践和Cloudflare Workers 指南。
实现时请以官方资料为准:Cloudflare 的Rate limiting rules、OWASP API Security 2023 的API4: Unrestricted Resource Consumption和API6: Unrestricted Access to Sensitive Business Flows,以及 MDN 对429 Too Many Requests的说明。
先决定要保护什么
新手常从“每分钟 60 次”这种数字开始想,但更好的顺序是先问:风险在哪里?限流保护的不只是服务器,还包括数据库、外部 API 费用、库存、密码重置流程、邮件额度、AI 生成次数、线索质量和业务规则。
flowchart LR
A["Request"] --> B["Identify client"]
B --> C["Check policy"]
C -->|allowed| D["Run handler"]
C -->|too many| E["Return 429 + Retry-After"]
D --> F["Log count and cost"]
下面是实际项目里常见的 4 个用例。
| 用例 | 限流 key | 起始值 | 保护对象 |
|---|---|---|---|
| 登录、OTP、密码重置 | IP + 账号 ID | 10 分钟 5 次 | 暴力破解、短信费用 |
| 搜索和列表 API | 用户 ID + path | 每分钟 60 次 | 数据库负载、爬取 |
| AI 或图片生成 | 用户 ID + 套餐 | 免费用户每日 10 次 | LLM 成本、免费额度 |
| Webhook 接收 | 发送方 + event id | 允许短时间重试 | 重复处理、队列阻塞 |
不要只依赖 IP。公司、学校或移动网络里,很多正常用户可能共享同一个 IP;攻击者也可以轻易更换 IP。认证后的 API 应优先组合用户 ID、API key、组织 ID、套餐、endpoint 和操作类型来生成 key。
给 Claude Code 明确规格
“帮我加限流”太模糊。你需要告诉 Claude Code 算法、key 设计、429 响应格式、header、测试、日志,以及本地和生产环境分别使用什么存储。下面的 prompt 可以直接使用。
Add rate limiting to the existing API.
Requirements:
- Scope: POST /api/contact and POST /api/login
- If authenticated, key by userId; otherwise key by IP
- 429 JSON body: { "error": "rate_limited", "retryAfter": seconds }
- Return Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining
- Tests must cover allowed requests, limit reached, and recovery after time passes
- Use Redis in production and an in-memory store locally
- Make limits configurable through environment variables
After implementation, report the verification commands and any unverified risks.
这样写的好处是验收条件明确。Claude Code 不只是加一个 middleware,还会知道客户端要依赖什么响应格式。再配合API 测试自动化指南,把 429 当成正式 contract 来测,而不是只测成功响应。
可复制运行的最小实现:Node.js 429 Server
先看一个不依赖任何包的版本。保存为 rate-limit-demo.mjs,用 Node.js 20 或更高版本运行。这里使用 token bucket:桶里按固定速度补充 token,每次请求消耗 1 个 token。它允许短时间 burst,但会限制长期平均速度。
import http from "node:http";
class TokenBucket {
constructor({ capacity, refillPerSecond }) {
this.capacity = capacity;
this.refillPerSecond = refillPerSecond;
this.tokens = capacity;
this.updatedAt = Date.now();
}
take(now = Date.now()) {
const elapsed = (now - this.updatedAt) / 1000;
this.tokens = Math.min(
this.capacity,
this.tokens + elapsed * this.refillPerSecond,
);
this.updatedAt = now;
if (this.tokens >= 1) {
this.tokens -= 1;
return { allowed: true, remaining: Math.floor(this.tokens), retryAfter: 0 };
}
const missing = 1 - this.tokens;
const retryAfter = Math.ceil(missing / this.refillPerSecond);
return { allowed: false, remaining: 0, retryAfter };
}
}
const buckets = new Map();
function clientKey(req) {
return req.headers["x-api-key"] ?? req.socket.remoteAddress ?? "anonymous";
}
function checkLimit(req) {
const key = clientKey(req);
if (!buckets.has(key)) {
buckets.set(key, new TokenBucket({ capacity: 5, refillPerSecond: 1 }));
}
return buckets.get(key).take();
}
const server = http.createServer((req, res) => {
if (req.url !== "/api/demo") {
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: "not_found" }));
return;
}
const result = checkLimit(req);
res.setHeader("X-RateLimit-Limit", "5");
res.setHeader("X-RateLimit-Remaining", String(result.remaining));
if (!result.allowed) {
res.writeHead(429, {
"content-type": "application/json",
"Retry-After": String(result.retryAfter),
});
res.end(JSON.stringify({
error: "rate_limited",
retryAfter: result.retryAfter,
}));
return;
}
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, remaining: result.remaining }));
});
server.listen(3000, () => {
console.log("Listening on http://localhost:3000/api/demo");
});
node rate-limit-demo.mjs
另开一个终端连续访问:
for i in 1 2 3 4 5 6 7; do
curl -i http://localhost:3000/api/demo
done
Windows PowerShell 可以这样测:
1..7 | ForEach-Object {
curl.exe -i http://localhost:3000/api/demo
}
第 6 次或第 7 次请求应该看到 429 Too Many Requests。MDN 说明 429 可以带 Retry-After,这能告诉客户端多久后再试,避免无意义的连续重试。
用 Redis 做可扩展实现
内存版本适合学习,但一旦 API server 有多台实例就会失效。A 实例认为剩余 0 次,B 实例可能还认为剩余 5 次。生产环境通常把计数放到 Redis,让所有实例看同一个状态。
下面的 Express 示例使用 Redis sorted set 做 sliding window。Sliding window 表示“从现在往前 60 秒”持续滑动计数,比固定每分钟清零更平滑,不容易在边界产生突刺。
npm init -y
npm i express ioredis
docker run --rm --name redis-rate-limit -p 6379:6379 redis:7-alpine
import express from "express";
import Redis from "ioredis";
const app = express();
const redis = new Redis(process.env.REDIS_URL ?? "redis://127.0.0.1:6379");
const limitScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window_ms = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local member = ARGV[4]
redis.call("ZREMRANGEBYSCORE", key, 0, now - window_ms)
local count = redis.call("ZCARD", key)
if count >= limit then
local oldest = redis.call("ZRANGE", key, 0, 0, "WITHSCORES")[2]
local retry_ms = math.max(1, oldest + window_ms - now)
return {0, 0, retry_ms}
end
redis.call("ZADD", key, now, member)
redis.call("PEXPIRE", key, window_ms)
return {1, limit - count - 1, 0}
`;
async function rateLimit(req, res, next) {
const user = req.get("authorization")?.replace(/^Bearer\s+/i, "");
const identity = user || req.ip || "anonymous";
const key = `rl:${identity}:${req.path}`;
const limit = Number(process.env.RATE_LIMIT_REQUESTS ?? 10);
const windowMs = Number(process.env.RATE_LIMIT_WINDOW_MS ?? 60000);
const now = Date.now();
const member = `${now}:${Math.random()}`;
const [allowed, remaining, retryMs] = await redis.eval(
limitScript,
1,
key,
limit,
windowMs,
now,
member,
);
res.setHeader("X-RateLimit-Limit", String(limit));
res.setHeader("X-RateLimit-Remaining", String(remaining));
if (allowed === 1) return next();
const retryAfter = Math.ceil(Number(retryMs) / 1000);
res.setHeader("Retry-After", String(retryAfter));
res.status(429).json({ error: "rate_limited", retryAfter });
}
app.use(rateLimit);
app.get("/api/search", (req, res) => {
res.json({ data: ["claude-code", "rate-limit"], at: new Date().toISOString() });
});
app.listen(3000, () => {
console.log("API ready on http://localhost:3000/api/search");
});
node redis-rate-limit-server.mjs
for i in $(seq 1 12); do
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/search
done
请在让 Claude Code 写生产版本时明确 Redis 故障策略。联系表单可以短时间 fail open;登录、支付、AI 点数接口可能应该 fail closed。这是业务判断,不应该交给库默认值。
客户端必须尊重 Retry-After
服务器返回 429 只是第一半。SDK、批处理和 Webhook 发送方也要读取 Retry-After,等待后再重试。
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchWithRateLimit(url, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
const retryAfter = Number(res.headers.get("retry-after") ?? "1");
const waitMs = Math.max(1, retryAfter) * 1000;
console.log(`429 received. Waiting ${waitMs}ms before retry.`);
await sleep(waitMs);
}
throw new Error("Rate limit retry budget exhausted");
}
for (let i = 0; i < 8; i += 1) {
const res = await fetchWithRateLimit("http://localhost:3000/api/demo");
console.log(i + 1, res.status, await res.text());
}
对外部 API 客户端,可以让 Claude Code 明确实现:“遇到 429 时使用指数退避;如果有 Retry-After 就优先使用;设置最大重试次数;最后失败要写日志。”无限重试会把对方服务和自己的队列一起拖垮。
Cloudflare 守入口,应用守用户规则
Cloudflare Rate Limiting Rules 适合在请求到达 origin 之前拦住明显的流量尖峰。官方文档说明了 expression、period、request threshold、mitigation timeout 和 action 等参数。它适合登录页、公开搜索 API、管理路径、AI 生成入口和明显 bot pattern。
但 Cloudflare 不能替代应用层规则。免费和付费套餐、组织额度、用户每日 AI 生成次数、退款滥用、邀请奖励滥用,都需要应用数据来判断。实际架构通常分层:
| 层 | 作用 | 例子 |
|---|---|---|
| Cloudflare/WAF | 尽早挡住明显连打和 bot | 按 IP 限制 /api/login |
| 应用层 | 按用户、组织、套餐、操作限制 | 免费用户每天 10 次生成 |
| Queue/worker | 平滑昂贵的异步任务 | 邮件、图片生成、PDF |
| Billing/monitoring | 发现成本异常 | SMS 和 LLM 使用量告警 |
OWASP API4 把无限制的 CPU、内存、文件大小和第三方服务消耗视为安全风险。OWASP API6 关注购买、预约、发帖、推荐奖励等敏感业务流程被自动化滥用。也就是说,限流不是单纯的 DDoS 防护,它也是保护收入、防止免费额度被刷、短信费用暴涨、垃圾注册和倒卖行为的业务控制。
常见失败案例
第一个坑是所有 API 共用同一个限制。读取个人资料和密码重置不应该是同一个阈值。每个 endpoint 要按成本和风险单独判断。
第二个坑是 429 响应不统一。有的路由返回 HTML,有的返回纯文本,有的返回 JSON,客户端会很难处理。请统一 JSON body、Retry-After 和剩余次数 header。
第三个坑是只统计成功请求。登录失败、无效 payload、未知邮箱的密码重置也会消耗资源并暴露攻击信号。很多情况下,失败请求反而需要更严格的限制。
第四个坑是把个人信息直接放进 Redis key 或日志。不要把邮箱和电话号码明文写入 limiter key。必要时先 hash,并设置短 TTL。
第五个坑是测试真的等待 60 秒。CI 不应该因为 60 秒窗口就 sleep 60 秒。把时间作为参数注入,测试里直接推进 now。
最后一个坑是误伤正常基础设施。搜索引擎、uptime check、内部监控、支付 webhook、合作伙伴回调可能需要单独策略。例外要窄、可审计,不能变成全局绕过。
Claude Code 复查清单
实现后,让 Claude Code 按以下点再审一遍:
- 所有 429 是否使用同一种 JSON 结构
- 是否设置
Retry-After和剩余次数 header - IP、用户 ID、API key、组织 ID 的 key 设计是否合理
- Redis 故障时是 fail open 还是 fail closed
- 登录失败、校验失败、外部 API 失败是否按需要计数
- 测试是否覆盖通过、被挡、时间恢复
- admin、监控、webhook、crawler 例外是否过宽
这不只是代码质量。对 AI、短信、邮件、支付相关产品来说,限流错误会直接体现在账单上。
咨询 CTA
ClaudeCodeLab 的Claude Code 培训与咨询可以一起梳理 API 实现、安全审查、限流、计费保护和监控。对已有的 Next.js、Express、Cloudflare Workers 或 AWS API Gateway 项目,真正重要的是决定“哪个操作、哪个身份、多久能做几次”,并把它写进代码、测试和日志。
个人项目可以先跑本文的 Node.js demo,再在多实例部署前切换到 Redis。团队项目则应该补齐 Claude Code prompt、复查清单、环境变量和 Runbook,这样以后改阈值也不会靠猜。
我按本文内容做了一个小验证:内存版在连续请求后返回 429;Redis 版在超过 10 次窗口后返回带 Retry-After 的 429;客户端等待逻辑加入后,不再立即重复请求。结论很明确:限流不是加一个 middleware 就结束,必须连同响应格式、重试、日志和例外策略一起验证。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code Permission Receipt Pattern:记录权限、证据和回滚方式
Claude Code 权限 receipt:记录允许动作、需要批准的边界、验证命令、回滚说明,以及 Gumroad 和咨询 CTA 检查。
Claude Code/Codex 安全 Agent Harness 实战:权限、验证与回滚
用权限策略、执行计划、验证脚本和回滚日志,为 Claude Code 与 Codex 搭建更安全的 AI Agent 工作流。
Claude Code 子代理实战指南:安全委派并行文章与代码工作
用 Claude Code 子代理安全拆分文章和代码工作:委派规则、提示词模板、失败模式与检查清单。