API Rate Limiting: What Actually Breaks When You Get It Wrong

API Rate Limiting: What Actually Breaks When You Get It Wrong

API 限流:做错时究竟会发生什么?

Most teams add rate limiting to their API as an afterthought — usually right after something has already gone wrong. A scraper hammers an endpoint, a client integration goes into a retry loop, or a single misbehaving user takes down a shared resource for everyone else. By then, you’re not designing a rate limiter, you’re firefighting. This post walks through the rate limiting mistakes that show up most often in production systems, why they happen, and what a more resilient approach looks like.

大多数团队在设计 API 时,往往将限流视为事后补救措施——通常是在出了问题之后才想起它。比如爬虫疯狂请求某个接口、客户端集成陷入重试死循环,或者某个行为异常的用户拖垮了共享资源。到了那个时候,你已经不是在设计限流器,而是在救火了。本文将探讨生产系统中常见的限流错误、产生原因,以及更具弹性的解决方案。

The Problem With “Just Add A Limit”

“加个限制就行”的问题所在

The instinct is usually: cap requests at X per minute per API key, done. In practice, this single-number approach breaks down fast for a few reasons:

  • Not all requests cost the same — a cached read and a heavy join are treated identically under a flat request-count limit.
  • Bursts are normal, not exceptional — a dashboard loading 15 widgets fires 15 requests instantly, then goes quiet for minutes.
  • Fixed windows create edge-of-window spikes — 100 requests at 0:59 and 100 more at 1:01 is 200 requests in two seconds, technically within “the rules.”

人们的直觉通常是:限制每个 API Key 每分钟请求 X 次,搞定。但在实践中,这种单一数值的方法很快就会失效,原因如下:

  • 并非所有请求的成本都相同:在统一的请求计数限制下,缓存读取和复杂的数据库联表查询被同等对待。
  • 突发流量是常态而非例外:仪表盘加载 15 个小组件会瞬间发出 15 个请求,随后几分钟内保持静默。
  • 固定窗口会造成边界尖峰:在 0:59 发送 100 个请求,在 1:01 再发送 100 个,两秒内总计 200 个请求,从技术上讲这完全符合“规则”。

What Actually Works Better

更好的做法是什么?

  • Sliding window or token bucket algorithms: instead of a hard reset every N seconds, a token bucket refills at a steady rate, and each request costs a token. This naturally allows small bursts while enforcing a steady average — matching real usage far better than a fixed window.

  • Cost-based limiting, not just count-based: weight your endpoints so an expensive search costs more “budget” than a simple lookup by ID. This prevents a handful of expensive calls from doing more damage than a thousand cheap ones.

  • Separate limits for authenticated vs. unauthenticated traffic: anonymous/IP-based traffic should have tighter limits than identified clients, so a shared office IP doesn’t get incorrectly throttled.

  • Respond with the right signals: a bare 429 forces every integration to guess when it’s safe to retry. Include a Retry-After header and expose limit, remaining, and reset time (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) so well-behaved clients back off correctly instead of retrying in a tight loop.

  • 滑动窗口或令牌桶算法:令牌桶不是每隔 N 秒硬重置,而是以恒定速率填充令牌,每个请求消耗一个令牌。这既允许小规模突发流量,又能强制执行稳定的平均速率,比固定窗口更符合实际使用情况。

  • 基于成本而非仅基于计数的限流:为接口设置权重,让昂贵的搜索操作比简单的 ID 查询消耗更多“预算”。这可以防止少数高成本调用造成的破坏超过成千上万次低成本调用。

  • 区分已认证与未认证流量的限流:匿名/基于 IP 的流量应比已识别的客户端有更严格的限制,这样共享办公空间的 IP 就不会被错误地限流。

  • 返回正确的信号:仅仅返回 429 状态码会迫使每个集成方猜测何时可以安全重试。应包含 Retry-After 响应头,并暴露限流总量、剩余量和重置时间(X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset),以便表现良好的客户端能正确退避,而不是陷入死循环重试。

The Mistake That Causes the Most Damage

危害最大的错误

The biggest failure mode isn’t a badly tuned number — it’s rate limiting that isn’t distributed correctly across multiple servers. If each instance keeps its own in-memory count, a client can multiply their effective limit by the number of instances behind your load balancer. A limit of “100 requests per minute” quietly becomes “100 times N servers” until traffic spikes and the database falls over anyway. The fix is centralizing the counter — usually with Redis — so every instance checks and decrements against the same source of truth. It adds a small amount of latency per request, but it’s the difference between a rate limiter that actually limits anything and one that only works on a good day.

最大的故障模式不是数值设置不当,而是限流未在多台服务器间正确分布。如果每个实例都维护自己的内存计数器,客户端就可以将有效限流额度乘以负载均衡器后的实例数量。原本“每分钟 100 次请求”的限制会悄悄变成“100 乘以 N 台服务器”,直到流量激增,数据库依然会崩溃。解决方法是集中化计数器(通常使用 Redis),让每个实例都针对同一个“事实来源”进行检查和扣减。这会增加少许请求延迟,但这是“真正有效的限流器”与“只在风平浪静时才起作用的限流器”之间的区别。

A Practical Starting Point

一个实用的起步建议

If you’re retrofitting rate limiting onto an existing API rather than designing it from scratch, a reasonable rollout looks like this:

  1. Start with logging and monitoring only — understand your actual traffic shapes before setting a real limit.
  2. Set limits per authenticated client, not per IP, wherever identity is available.
  3. Use a token bucket or sliding window, not a fixed reset window.
  4. Centralize your limit counters if running more than one instance.
  5. Return clear headers and a Retry-After value on every 429.
  6. Alert on clients consistently near their limit — often a sign of a bug, not malicious intent.

如果你是在现有 API 上补加限流,而不是从零开始设计,合理的实施步骤如下:

  1. 先从日志和监控开始——在设置真正的限制之前,先了解真实的流量形态。
  2. 只要能识别身份,就按已认证的客户端设置限制,而不是按 IP。
  3. 使用令牌桶或滑动窗口,不要使用固定重置窗口。
  4. 如果运行多个实例,请务必集中化限流计数器。
  5. 在每次 429 响应中返回清晰的响应头和 Retry-After 值。
  6. 对持续接近限流阈值的客户端发出警报——这通常是 Bug 的迹象,而非恶意攻击。

Rate limiting is invisible when it’s working and very visible when it isn’t. Getting the fundamentals right early avoids a class of production incidents that are annoying to diagnose precisely because everything “looks fine” on a request-count dashboard while the system is being overwhelmed underneath it.

限流在正常工作时是隐形的,但在失效时却非常显眼。尽早打好基础,可以避免一类生产事故。这类事故之所以难以排查,正是因为在请求计数仪表盘上一切看起来都“很正常”,而系统底层却早已不堪重负。