mcpField
A
api-rate-limit-policy

api-rate-limit-policy

github.com/samber开发78.6k 次安装3 天前更新
代码仓库

关于 api-rate-limit-policy

面向公共 API 团队的限流政策设计技能,涵盖计量、分层、公平性、响应和变更通知;不涉及具体限流实现。

主要功能

  1. 01设计前先澄清需求
  2. 02REST 与 GraphQL 计量选择
  3. 03分层配额与突发限制
  4. 04公平性与租户隔离
  5. 05限流响应与请求头
  6. 06覆盖流程与变更通知

适用场景

  1. 01为新 API 制定公开限流规则
  2. 02核对实际执行与文档中的限制
  3. 03为不同客户层级设置限额
  4. 04规划租户间的公平限流

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/samber/developer-platform-skills --skill api-rate-limit-policy

安装后在 Claude Code 中输入/api-rate-limit-policy确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
graphql-cost-metering.md
header-families.md
overrides-and-change-notice.md
vendor-limit-models.md

详细介绍

API 速率限制策略

你是一名 API 速率限制策略设计师。请设计公共 API 向使用者公布的限制:计量模型、套餐层级和突发流量数值、公平性规则、响应头、限流响应、覆盖限制的路径,以及变更如何落地,让集成方能够预测、观察并适应限流,而不是一次次触发限制后才发现每个上限。由哪个网关或中间件来执行限制不在讨论范围内;讨论范围仅限于限制所做出的承诺,以及如何传达这些承诺。

Zuplo 的一句话宗旨阐明了任务:速率限制确立“对 API 资源的公平访问策略,并防止任何单个用户或应用消耗过多资源、影响其他用户的体验”。GeekyAnts 的表述则说明了这为何是策略工作,而非底层实现:“设计速率限制器本质上是关于公平性、可扩展性和用户体验的战略决策。”

记忆(建议): 如果记忆存放在文件中,可考虑使用 developer-platform-context.md;如果使用其他记忆系统,则以该系统为准。该文件仅供参考,并非强制要求。将不同任务的信息分放在不同章节。完成的任务应予以删除。任务需注明日期;一般项目背景无需注明日期。有些访谈回答可能因任务而异。

澄清问题

在开始设计前先询问以下问题;每个答案都会影响一个编号步骤。集中提问——这是战术性设计任务,不是战略访谈。

  1. 范式:仅 REST、仅 GraphQL,还是两者都有?(请求计数和成本点数是互不兼容的计量模型;步骤 1 会针对每种 API 形态分别选择。)
  2. 是从零开始,还是已经实施了限制?如果已经实施,请索取当前执行的数值和当前文档中公布的数值——两者之间的偏差是首要发现。
  3. 现有或计划中的使用者层级有哪些(免费 / 自助付费 / 企业合作伙伴)?企业客户是否已在要求更高的限制?
  4. 滥用压力:API 是否遭到抓取或恶意请求,还是流量大多来自行为正常的集成?(这会提高步骤 1 中滑动窗口方案和次级防护的优先级。)
  5. 是否曾有某个客户的流量导致其他客户的延迟变差?(这会提高步骤 3 中公平性分层的优先级。)
  6. API 是否在多个区域提供服务?如果各区域独立执行限制,调用方的实际全局吞吐量就会乘以区域数量——有人称其“并非理论问题,而是真实的利用途径”——因此,公布的限制必须说明其适用范围。
  7. 边缘层 / 网关目前能发出哪些内容?(能够低成本生成 IETF 结构化字段的边缘层会改变步骤 4 中响应头选项的优先级;无法统计成本点数的网关则会限制步骤 1 的选择。)
  8. 公布的策略必须在何时落地?这是一次性修复(为一场企业客户谈判排除障碍),还是平台要持续执行多年的长期契约?投入上限是多少——用于限制器工作的工程工时、维护公平性分层的 on-call 轮值安排,以及对使用者可见变更的接受度?

设置问题 8,是因为算法选项(步骤 1)和响应头选项(步骤 4)在见效时间、持久性和投入方面差异很大;不回答该问题,就无法确定任一选项的优先级。每个答案都会影响不同的选择层级:

  • 硬性期限会优先采用当前网关已执行的方案,以及传统的三种响应头。
  • 长期契约会优先采用滑动窗口和同时支持两类响应头的方案。
  • 投入上限较低时,应在本轮设计中直接移除步骤 3 的公平性分层,而不是把它们暂时搁置;解除该限制的条件应是“首次出现邻居噪声事件”。

消费者层级

消费者层级是会改变此设计的维度,而不仅仅是细分:层级体系本身就是此技能产出的策略成果。

  • 匿名/免费调用方 - 按 IP 或免费密钥进行标识;限制最严格,负载过高时最先被限流。它们只能通过响应头了解自己的限制。
  • 自助付费集成方 - 按 API 密钥或组织进行标识;公布确切数值,供其据此规划容量。它们需要一份有文档说明的表格。
  • 企业和合作伙伴 - 公布流程,而不公布数值;按已验证的主体配置覆盖规则(第 6 步)。它们需要明确的覆盖处理渠道和提前期。

工作流

  1. 为每种范式选择计量模型——以及算法,因为算法决定数值所代表的承诺。
  2. 设计层级:将配额和突发限额作为两个数值,并有意确定标识方式。
  3. 增加公平性和租户隔离机制。
  4. 选择响应头系列。
  5. 设计限流响应:429 和 Retry-After。
  6. 定义企业/合作伙伴的覆盖处理渠道。
  7. 编写变更通知策略。

下文将按顺序逐步介绍各个步骤。

1. 选择计量模型

存在两种互不兼容的模型,按范式划分——绝不要混用:

  • REST:按请求计数。 GitHub REST 的额度固定为每小时 5,000 个请求;Shopify REST 则采用 40 个请求的桶,每秒漏出 2 个请求。简单、广为人知,但无法区分单个请求的成本。
  • GraphQL:按成本点为查询定价。 仅查询一个标量的请求与查询 100 个连接的请求,服务器成本差异巨大;限额是每个时间窗口内的点数预算,并有公开的成本公式(GitHub:连接的 first/last 除以 100,最低为 1;Shopify:标量为 0,对象为 1,变更为 10,先扣费再退款)。定价和预算策略由你决定;深度上限、持久化查询等结构性防护属于模式设计,由 samber/developer-platform-skills@public-graphql-api-design 负责。参见 references/graphql-cost-metering.md。
  • 如果同时提供这两种接口,应分别公布两种模型,并说明它们是否共享任何限制(GitHub:预算相互独立,但共用一个每秒 100 个并发请求的次级上限)。
  • 即使计量单位不是请求或成本点,也要明确说明:Twilio 按并发计量——即正在处理的请求数,而不是每个时间窗口内的请求数。这是截然不同的单位,调用方必须据此制定不同计划。

底层算法同样是一种策略选择,因为它决定了公开额度所承诺的行为(突发行为、边界行为)。有四种选择,排名如下:

  • 效率:token bucket > fixed window > sliding window > leaky bucket

  • 实施成本:sliding window > token bucket == leaky bucket > fixed window

  • 价值(面向开发者的公共 API):token bucket > sliding window > leaky bucket > fixed window

  • 默认选择:令牌桶——桶深度代表突发额度,补充速率代表持续配额,因此第 2 步中的两个数字可以直接从算法得出。它奖励行为良好的突发流量客户端,是 Stripe 和 GitHub 的选择;Stripe、Google Cloud、AWS API Gateway 等厂商的指导意见也趋于一致,称其为“大多数 Web 服务的标准选择……除非有具体理由不选,否则从这里开始”。令牌桶与漏桶在实施成本上并列,这并非偶然:两者都是为每个键维护一个桶;它们的区别在于设计意图,而非实现成本。

  • 仅在内部或低风险限额场景下退回到固定窗口,前提是文档中说明的缺陷——窗口边界处最多可达到限额的 2 倍——不会造成问题(Sentry 为求简便,明确接受了这种权衡)。它的第二个缺陷是请求激增:遭到限流的客户端会在重置边界同步重试,而 IETF 草案本身也承认这是一个故障模式。

  • 在面对对抗性压力时升级到滑动窗口(问题 4)。这是最容易被忽略的选项:它能避免边界问题,也最公平,Retry-After 精度最高,但计算成本也最高——它在每一轮效率比较中都落败;而爬取或滥用压力恰恰是选择它的原因。

  • 当突发流量是威胁而非优势时,选择漏桶——例如下游系统脆弱,或需要进行扇出式投递。Shopify 作为一个与支付密切相关的平台,仍然选择了它,因为闪购期间的 Webhook 扇出是主要问题:“每个应用都有一个桶……每秒移除一颗弹珠。”

  • 命名陷阱:“令牌桶”和“漏桶”在非正式语境中被混淆的频率,高于这个领域中的任何其他一对术语。在公开策略中,应描述行为(空闲时间是否能累积突发容量,或输出是否保持恒定),绝不要只依赖名称。

  • 这不是整个技能范围内的一项统一决策:Stripe 同时运行四层限流器(第 3 步),每层都采用自己的机制。排名只是默认参考,而非铁律——应根据访谈答案和你对用户技术栈的了解重新排序;如果网关内置了滑动窗口限流器,那么选择该方案几乎不增加成本,这会改变实施成本的排名。

2. 设计分层:两个数字,按需确定键控方式

  • 计划→限额表是实际平台中的特例,而非默认做法:
    • Stripe 完全不按计划分层——采用统一的账户默认值、按 API 设置限额,以及由支持团队授予的覆盖值。
    • GitHub 的主要限额取决于认证方式:未认证 60 次/每个令牌 5,000 次/Enterprise 组织 15,000 次。
    • Shopify、Zoom、Auth0 和 Anthropic 会将限额映射到计划。
  • 将限额建模为身份/认证范围 × 端点类别 × 账户覆盖值,然后审慎决定是否将计划作为输入之一。参见 references/vendor-limit-models.md。
  • 将突发和持续限额作为两个明确的数字发布,绝不要合并为一个数值。突发限额等于持续限额,是广为人知的失败模式——它会限制需要高于稳定状态预留空间的合理流量峰值(如 Webhook 扇出、定时报告);Stripe 有意设计了突发流量额度,并对此有明确说明。
  • 随着层级变化,让用于键控该层级的标识符也随之变化:匿名用户使用 IP,已认证用户使用用户/令牌,付费用户使用 API 密钥或组织,内部调用使用服务身份。每次升级都应明确说明这是策略决策,而非偶然结果。
  • 通过按端点类别设置限额,将高成本操作从通用额度中划分出来(Stripe 为不同 API 面单独发布限额:Files 每秒读取 20 次,Search 每秒读取 20 次,Payouts 每秒请求 15 次 + 并发 30 个)。
  • 根据请求特征(并发数、每个端点的压力)而非原始请求数触发的次级滥用防护是合理的,而且往往不可或缺。但要说明它确实存在,以及它如何发出信号:GitHub 有意未充分记录的次级限额,是典型的沟通失败案例——调用方尚未超出已发布配额,却被不透明的机制限流,只有触发后才发现它的存在。
  • 审慎决定测试/沙盒模式是否需要独立层级,并默认不设置:在测试模式中沿用生产环境的数字,可以确保集成方的限流代码可靠;多个成熟平台也确实采用了这种做法。只有在需要防范评估流量滥用(免费层抓取、在共享沙盒上进行负载测试)时,才添加单独的预生产配额,并将其设计为聚合流量和范围上限,而非更严格的每分钟限流器。samber/developer-platform-skills@api-test-mode-design 用于决定是否需要该配额,并包含各供应商的具体做法;如果确实需要,则由你设计该层级。
  • 将发布的数字表述为运营参考值,而非 SLA——所有主要平台都保留为保障稳定性而降低限额的权利(第 7 步负责说明如何落实)。

3. 公平性分层与租户隔离

公平性是一项策略决策,而非优秀算法的副产品:如果只对一个共享池应用同一个令牌桶,最吵闹的租户就会意外胜出。典型事故是某个租户失控的凌晨 2 点批处理任务拖慢其他所有租户的延迟,而自身仍未触及任何单项上限。

  1. 租户级上限 - 每位客户的总体限额,适用第 2 步中的分级表。
  2. 租户配额内的单用户限额 - 防止多席位账户中的一位用户耗尽整个租户预算。
  3. 按端点类别划分的专用配额 - 为高开销操作分配一小部分额度,并独立于一般预算。
  • 如果严格的租户级上限过于苛刻,可改用加权共享:为每个租户分配权重(企业租户的份额是免费租户的 10 倍),并按比例分配容量,这样即使大型租户占满资源池,小型租户也不会被完全饿死。Cohere 的生产版本将层级权重与 Deficit Round Robin 和优先级队列结合使用。权衡之处在于:你放弃了确定性的保障速率,以消除小型租户被完全阻断的困境。
  • AWS 的 Builders' Library 阐明了目标:通过按租户设置配额,“只拒绝超出计划的部分,其他工作负载仍能以可预测的性能继续运行”;同时也指出了需要明知并接受的悖论:配额在保护其他所有人的可用性的同时,会降低受限租户自身的可用性,即使当时存在空闲容量。
  • 纵深防御是成熟阶段的做法。Stripe 会同时运行四种限流器:
    • 请求速率限流器——“迄今为止最重要的一种”。
    • 针对高开销端点的并发限流器。
    • 预留约 20% 容量供关键方法使用的全局负载削减器。
    • 按优先级类别削减负载的工作进程利用率削减器:先处理测试模式流量,然后是 GET 请求,最后是 POST 请求。
  • 负载削减与速率限制是不同的机制——它依据的是系统整体状态,而非调用方自身的行为,因此即使行为良好的租户也可能在全局负载过高时被削减。应在公开的策略中说明这一点;遗漏此情况的公平性承诺会夸大实际保障。
  • 内部公平性机制(WFQ、DRR、负载削减器)属于架构设计,而非公开策略——应记录其对外可观察的影响(调用方能看到什么,以及何时能看到),而非具体机制。

4. 选择标头系列

机器可读的响应限流状态有两个可选方案,按优先级排列。第三种方案——_仅_发送 IETF 字段——已从下方菜单中删除,而不是排在最后,因此不会悄然重新纳入范围:

方案效率工作量价值持久性
legacy X-RateLimit-* 三件套++++++++
两个系列都发送---+++--
  • 默认选项:legacy 三件套——X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset。几乎所有客户端和 SDK 都已经能解析这些字段;GitHub 仍然只发送这一系列。请明确说明 Reset 的语义(纪元时间戳还是剩余秒数——不同厂商的定义确实不同)。
  • 升级为两个系列都发送——这是被搁置的选项,价值最高,工作量也最大——当边缘节点能够以较低成本生成 IETF 结构化字段(问题 7),或公布策略本身很重要时采用:RateLimit-Policy 允许遵循规范的客户端在首次收到 429 之前自行限流,而 legacy 三件套无法表达这一点。Cloudflare 于 2025 年 9 月采用了两个标头系列——这来自 Tier-1 平台的生产应用,值得引用。
  • 从菜单中删除,而非排在最后:仅发送 IETF 字段。 将其作为唯一的限流状态信号会让你已有的客户端无法正常工作。采用情况有限:GitHub 和 Stripe 完全不发送 IETF 字段。该规范(draft-ietf-httpapi-ratelimit-headers)属于标准轨道,但尚未成为 RFC,且不同草案版本的语法已经发生变化,因此当前的字段名称以后可能会变。
  • 重新纳入的条件: 当草案成为 RFC,或第二个 Tier-1 平台采用该方案时,将此选项重新纳入菜单。在此之前,IETF 字段只随“双系列都发送”选项一起发布——在文档中将其描述为面向未来的方案,并使用当前名称引用(RateLimit、RateLimit-Policy)。
  • 无论发送哪些字段,标头都只是参考提示,绝非保证——草案本身指出,这些值“仅供参考,且 MAY 被忽略”,而“剩余 5 次”并不意味着一定能成功调用五次。请将其作为规划信号发布,而非承诺。
  • 有一种做法不属于此菜单,也不比菜单中的选项低一档:完全不发送限流状态标头,这是 Stripe 的选择——对 429 作出响应,在客户端进行限流。只有同时提供会在内部重试的 SDK 时,这种做法才可行;文档必须将其表述为有意选择,而非疏漏。
  • 此排名是默认建议,不是硬性规定——请结合问题 7 和实际客户端情况重新排序:如果受众中有 90% 使用你自己的 SDK,legacy 三件套在兼容性方面的优势就会减弱,因为解析两端都由你掌控,可以通过一次 SDK 发布更新整个客户端群。

请参阅 references/header-families.md,了解确切语法、跨厂商对比表和引用来源方面的注意事项。

5. 设计限流响应

  • REST 接口中的每次限流都返回 HTTP 429(RFC 6585),并带有 Retry-After 标头——没有例外。RFC 6585 还禁止缓存 429;若缓存层忽略这一点,表面上的限流时长就会超过实际时长。
  • Retry-After 有两种符合 RFC 9110 的格式——延迟秒数和 HTTP 日期。请使用延迟秒数:普遍存在的客户端错误是用 parseInt 解析 HTTP 日期,结果得到 NaN,继而引发重试风暴。无论你还发送什么内容,IETF 草案中的优先级规则都适用:Retry-After MUST 优先于任何计算得出的重置时间。
  • 429 响应正文是 API 错误信封的普通实例,包含 rate_limit_exceeded 之类的机器可读代码;其设计由 samber/developer-platform-skills@api-error-design 负责,绝不要另创限流专用格式。本技能决定出现哪些信号(重试时间,以及触发了_哪项_限额——Stripe 的 Stripe-Rate-Limited-Reason 是区分多项限额的范例);错误信封的形状则由该技能负责。
  • 必须从设计中规避的陷阱:HTTP 200 中的限流。 Shopify GraphQL 会在 200 响应中返回 THROTTLED 错误代码;GitHub GraphQL 的主要限额则会返回状态码 200、错误正文和 x-ratelimit-remaining: 0。仅检查状态码的客户端会把两者都视为成功。如果 GraphQL 的部分结果语义要求在正文中发出信号,务必像突出状态码一样醒目地记录,并确保信号可供机器读取,绝不能悄然混用约定——Shopify 对成本限额返回 200-THROTTLED,对资源限额则返回真正的 429;客户端必须分别处理这两种情况。
  • 在限额说明旁发布重试指南:
    • 遵循 Retry-After。
    • 使用带抖动的指数退避——AWS 的 Marc Brooker:“解决办法不是移除退避,而是加入抖动。”
    • 说明持续高频请求的后果——GitHub 表示,受限时仍继续请求“可能会导致你的集成被封禁”。
  • 客户端机制——退避算法、重试预算、幂等键——属于 samber/developer-platform-skills@api-idempotency-retry;你负责发布预期,该技能负责设计实现。

6. 定义企业/合作伙伴的覆盖路径

  • 跨厂商的常见模式是:自助服务套餐公布具体数值;最高级套餐公布一个流程。覆盖限额需通过支持、销售或客户经理申请——几乎从不通过仪表板开关——且要绑定到经过身份验证的主体(API 密钥、组织、应用安装),而不是个人。
  • 选择你要采用的机制并记录下来;只说“联系销售”而没有流程,就是缺口。已有四种明确的形式:
    • 基于工单并设有提前期——Zyte:至少提前 24 小时申请,注明密钥、RPM 和日期;Stripe:大幅提高限额需提前至少 6 周申请。
    • 基于消费自动升级——Anthropic 的 Tier 1→4。
    • 由套餐控制——Zoom、GitHub Enterprise。
    • 限时自助突增——Auth0 每月 48 小时的倍数提升,这是罕见的自助例外。
  • 明确说明更高价套餐是否意味着更高限额——OpenAI 的 Enterprise 套餐不会自动提高限额,必须明确启用;这与大多数买家的预期相反。
  • 覆盖机制可以保留某一维度的不可协商性(GitHub 的服务器管理员豁免仍无法绕过 GraphQL 点数配额)——确定哪些限额不能被覆盖,并明确说明。
  • 书面保留为保障平台稳定性而降低任何覆盖限额的权利。

关于全部七种厂商机制,请参阅 references/overrides-and-change-notice.md。

7. 编写变更通知政策

  • 行业将限制数值变更视为非破坏性变更——IBM 明确指出:速率限制“可以在 API 生命周期内针对 API 端点进行调整,且无需更新版本”。没有版本升级,就不会触发弃用机制,这正是书面通知政策至关重要的原因:没有其他机制会强制进行沟通。应将两种机制分开——版本弃用(6–24 个月的窗口、Deprecation/Sunset 标头)属于 samber/developer-platform-skills@api-versioning-policy。
  • 通知范围应与影响范围相匹配。Stripe 对大幅提高限制要求至少提前 6 周通知,这作为降低限制的对称下限也很合理。
  • 在实时流量中引入或收紧限制时,采用 GitHub 自己的发布顺序,并在每次收紧前通知受影响的调用方:
    1. 观察——记录真实流量,不执行限制。
    2. 建立基线——在较高的阈值下执行限制。
    3. 优化——根据观测数据收紧限制。
  • 为了平台稳定而紧急降低限制是合理的;应在书面政策中明确保留这种权利(Shopify 和 Stripe 都这样做),让它成为政策,而非意外。
  • 绝不要静默更改重置语义(epoch 秒 → 剩余秒数):这会在客户端解析中造成隐蔽故障,而不是产生明显错误——仅此一项限制变更就值得单独发布重大公告。
  • 带内通知优于变更日志:通过标头公开限制,能让行为良好的客户端自动适应。Zuplo 为其他做法定下了基调:“提供充足的通知并解释变更原因,以保持透明度和信任。”

失败模式

反模式检查清单——每一项都是直接的审计发现:

  • 在 HTTP 200 中发出限流信号,但文档对此只字未提。
  • 突发限制与持续限制设为相同数值,或只公布一个混合数值。
  • 所有租户共用一个桶——这种设计必然会产生吵闹邻居问题。
  • 将每个区域的限制说成全局限制(实际吞吐量 = 限制 × 区域数)。
  • 只有触发后才会发现存在次级限制或滥用限制。
  • 文档中记载的限制与实际执行的限制不一致,无论高低。
  • 静默更改重置语义。
  • 429 响应没有 Retry-After,或明明用延迟秒数即可,却将 Retry-After 以 HTTP 日期形式发送。
  • 政策称采用令牌桶,却描述了漏桶行为,或反之。
  • 口头批准覆盖规则,没有关联对象,也没有任何文档记录。
  • 合同将已公布的限制引用为 SLA 承诺,但政策并未说明它属于 SLA 承诺。

测量

设定两个门槛;不断迭代设计,直到两者都达标。

  • 机器可读性:100% 的限流响应都包含文档记载的客户端可据此分支处理的机器可读信号——429 响应带有 Retry-After,或在范式限制迫使使用 200 时,响应正文中包含有文档记录的代码。只要存在一条未记录的 200 限流路径,就无法通过此门槛,因为客户端的正确性取决于文档最不完善的信号。
  • 文档与执行保持一致:公布的限制就是实际执行的限制。 双向比对文档与执行配置:每个文档中记载的数值都必须与实际执行的数值一致,且每项实际执行的限制——包括次级防护,至少要记录其存在及触发条件——都必须出现在文档中。零偏差。

根据首月数据建立基准趋势(业界没有统一阈值):

  • 各层级的 429 比率。
  • 429 后在 Retry-After 窗口内进行重试的比例——客户端未遵守该窗口,说明文档或 SDK 存在缺口。
  • 与限制相关的支持工单。
  • 每季度的覆盖规则申请数——如果申请量长期居高不下,说明公布的层级设置得过低。

调用示例

  • “为我们的公共 REST API 设计限流层级——我们有免费、专业版和企业版套餐,企业版客户总是要求提高限额。”
  • “我们即将在 REST API 旁推出公共 GraphQL API——这两个 API 的限流应该如何协调?”
  • “我们最大的客户夜间运行的批处理任务会拖慢其他所有人的延迟表现——在不硬性拦截他们的情况下,为我们的限额设计公平机制。”
  • “根据网关实际执行的规则审核我们的限流文档,并确定应该返回哪些标头。”

参考资料

另请参阅同一集合中的:

  • samber/developer-platform-skills@developer-portal-design - 已发布的限额、用量仪表板和剩余配额显示在开发者门户中的位置。

评论

支持 Markdown