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-Afterheader 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:
- Start with logging and monitoring only — understand your actual traffic shapes before setting a real limit.
- Set limits per authenticated client, not per IP, wherever identity is available.
- Use a token bucket or sliding window, not a fixed reset window.
- Centralize your limit counters if running more than one instance.
- Return clear headers and a
Retry-Aftervalue on every 429. - Alert on clients consistently near their limit — often a sign of a bug, not malicious intent.
如果你是在现有 API 上补加限流,而不是从零开始设计,合理的实施步骤如下:
- 先从日志和监控开始——在设置真正的限制之前,先了解真实的流量形态。
- 只要能识别身份,就按已认证的客户端设置限制,而不是按 IP。
- 使用令牌桶或滑动窗口,不要使用固定重置窗口。
- 如果运行多个实例,请务必集中化限流计数器。
- 在每次 429 响应中返回清晰的响应头和
Retry-After值。 - 对持续接近限流阈值的客户端发出警报——这通常是 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.
限流在正常工作时是隐形的,但在失效时却非常显眼。尽早打好基础,可以避免一类生产事故。这类事故之所以难以排查,正是因为在请求计数仪表盘上一切看起来都“很正常”,而系统底层却早已不堪重负。