Advanced (更新: 2026/6/2)

用 Claude Code 实现 API Rate Limiting:429、Redis 与 Cloudflare 实战

用 Claude Code 实现安全的 API 限流:429、Redis、Cloudflare、防滥用和常见坑。

用 Claude Code 实现 API Rate Limiting: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 ConsumptionAPI6: 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 + 账号 ID10 分钟 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 就结束,必须连同响应格式、重试、日志和例外策略一起验证。

#Claude Code #rate limiting #API #security #Node.js
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。