X 接口报 Rate Limit Exceeded?先搞清是哪一种,再决定怎么修

18 min readSocialAPI 工程团队

X 接口报 Rate Limit Exceeded?先搞清是哪一种,再决定怎么修

你发了个请求,拿回来这个:

{"errors":[{"code":88,"message":"Rate limit exceeded"}]}

或者一个 HTTP 429,带着 x-rate-limit-reset,告诉你还得等到某个时间点。

麻烦的地方在于:"rate limit exceeded"这一句话底下,其实盖着三种完全不同的情况,而它们的解法各不相同。选错了,要么白等十四分钟,要么一直撞一堵根本不会让开的墙。

先讲怎么分辨。


三十秒版答案

看失败那次响应的 x-rate-limit-remaining:

你看到的 说明 怎么办
429,remaining = 0 本窗口配额用完了 等到 x-rate-limit-reset,没有别的办法
429,remaining > 0 你发得太快了 降速,约一分钟后恢复
404 且响应体为空 根本不是限流 立刻重试,见下文

大多数人只处理了第一种。时间都耗在第二和第三种上。

如果你不想自己实现这些,我们的 API 已经替你把三种都吸收掉了 —— 不过下面的机制无论如何都值得了解。


情况一:配额用完

这是文档里写的那一种。每个接口在 15 分钟窗口内给你固定的请求次数,用完就得等窗口重置。

有两点不太直观:

窗口是整体重置的,不是逐渐恢复的。 它是一个固定的 900 秒区块,不是滑动窗口。如果你在头一分钟里把额度烧光,接下来十四分钟只能干等。把请求均匀铺开不是"优化" —— 它决定了你的集成是能正常跑,还是每个刻钟里大半时间处于停摆。

不同接口的额度差得非常远。 这是我们见到的最常见的一个坑:两个接口返回的数据几乎一样,但其中一个的额度是另一个的大约十倍。如果你轮询时间线老是不够用,很可能只是调错了接口 —— 换一个不花任何成本,可用空间直接翻好几倍。

想知道自己实际拿到多少,看任何一次成功响应的 x-rate-limit-limit。别假设文档上的数字就是你账号上生效的数字。


情况二:发得太快(文档里没有的那一种)

这一种最坑人。

你的配额可能还剩一大把 —— remaining 稳稳地停在几百 —— 照样吃 429。

原因是还有第二道独立的限制,管的是发送速度,和你还剩多少额度无关。超过它就会被挡住约一分钟,剩多少额度都没用。

好消息是:被拒绝的请求不消耗配额。 等你回来,计数器从中断处继续。你损失的是时间,不是预算。

两种机制对比:

配额限制 速度限制
怎么识别 remaining = 0 429 但 remaining > 0
窗口 15 分钟 约一秒
恢复 等 reset 约 60 秒
消耗配额? 是 否

如果你的重试逻辑只处理第一种,就会不断踩到第二种 —— 更糟的是,你会用"等十五分钟"去应对一个只需要等一分钟的问题。


情况三:根本不是限流

搜索的行为不一样,而且它的报错确实有误导性。

X 拒绝一个搜索请求时,返回的不是 429,而是 404 加一个空的响应体 —— 没有 JSON,没有错误信息,什么都没有。与此同时你的配额几乎纹丝不动。

这一点很容易把人带偏,因为看到像限流的报错,自然反应是退避。而在这里退避恰恰是错的。 这种拒绝看起来是概率性的,不是额度驱动的:同一个查询,几秒后再发,往往就成功了。

有效的做法:及时重试。 试几次就能把成功率拉得很高。我们生产环境靠重试(而不是等待)把成功率做到 95% 以上。

无效的做法 —— 如果你正在排查这个,以下可以直接跳过:

  • 改请求头或认证细节
  • 调整发送的参数
  • 拉长重试间隔

这几种我们都试过,没有一个能让数字变好。唯一管用的就是再试一次。


能跑的重试代码

三种情况都处理了,这就是我们线上逻辑的形状:

import time, random, requests

def fetch_with_backoff(url, headers, max_tries=5):
    for attempt in range(max_tries):
        r = requests.get(url, headers=headers, timeout=30)

        # 搜索被拒绝表现为 404 + 空响应体 —— 该重试, 不该等
        if r.status_code == 404 and not r.content:
            time.sleep(0.5)
            continue

        if r.status_code != 429:
            return r

        remaining = int(r.headers.get("x-rate-limit-remaining", -1))
        reset_at  = int(r.headers.get("x-rate-limit-reset", 0))

        if remaining == 0 and reset_at:
            # 配额没了, 重试也没用, 老实等
            wait = max(0, reset_at - int(time.time())) + 1
        else:
            # 发太快了, 短暂退避, 加抖动
            wait = min(60, 2 ** attempt) + random.uniform(0, 1)

        time.sleep(wait)

    raise RuntimeError("重试 %d 次仍被限流" % max_tries)

两个值得保留的细节:

先看 remaining 再决定等多久。 对已耗尽的配额做指数退避是白白烧时间;对速度限制去等 reset 则是扔掉十四分钟可用时间。

必须加抖动。 多个 worker 不加抖动会在同一瞬间醒来,一起再次触发速度限制。

这一段正是我们已经搭好并调过的部分 —— 不想自己弄的话,它在我们的接口里是默认行为。


到底能跑多大吞吐

常见的想法是"多加几个 key 就能成倍提速"。它确实能抬高天花板,但真正的吞吐由另一个东西决定:

所需请求数 = 目标数 × 冗余度 ÷ 新鲜度要求

注意这里面没有"基础设施规模"。如果你要盯 100 个账号、要求 3 秒新鲜度,那就需要一个确定的请求速率 —— 再多的容量也改变不了这个需求本身。

实际的取舍是延迟 vs 规模。把新鲜度从 3 秒收紧到 1 秒,同样的覆盖量需要三倍的请求。先想清楚你真正需要哪一个,再去规划容量。


大家会问的问题

429 会消耗配额吗? 不会。被拒绝的请求不计入预算,计数器从中断处继续。

重置窗口是滑动的还是固定的? 固定的。窗口翻页时额度整体回来,不是一点点渗回来。

为什么还有配额却吃 429? 那是速度限制 —— 一道独立的发送频率上限。降速即可,约一分钟恢复。

怎么知道自己撞的是哪一种? 看失败响应的 x-rate-limit-remaining。0 是配额用完,大于 0 就是发太快。

给 X 多付钱能提高限制吗? 付费档提高的是配额,不是发送速度。2026 年 2 月起 X 把新开发者改为按量付费,读取每条 $0.005,每月上限 200 万条;原来 $200/月的 Basic 和 $5,000/月的 Pro 已对新用户关闭,全量历史搜索需要 Enterprise 档。

为什么搜索有配额也失败? 那不是限流,见情况三。该重试,不该退避。

多申请几个 key 就行了吧? 只在你的目标数和新鲜度允许的范围内。见吞吐那一节 —— 多个 key 抬高的是上限,不会减少你实际需要的请求量。

现在能立刻改的、最有效的一件事是什么? 检查你轮询用的是不是额度更高的那个接口。这一个改动的价值,常常超过所有重试调优加起来。

"rate limit exceeded"到底是什么意思? 说明你发的请求超过了该接口允许的量 —— 要么超了 15 分钟的额度,要么超了它能接受的每秒速率。两者的解法不同。

推特限流会持续多久? 配额型的要等到 15 分钟窗口重置;速度型的约一分钟就好。看 x-rate-limit-remaining 判断是哪一种。

推特的 error code 88 是什么? X 的旧版限流错误码。按 429 一样处理 —— 先看 remaining 再决定等多久。

我没发几个请求为什么就被限流了? 通常是因为限制是按接口算的,不是按账号总量。某个接口闲着不会给另一个繁忙的接口回血。

限流是每天固定时间重置吗? 不是。窗口从你在其中发的第一个请求开始计时,15 分钟后重置。

不发请求能查到剩余额度吗? 不能直接查 —— 这些数字只随真实响应的头部返回。做法是每次响应都读一遍,自己记账。

失败的请求算进限额吗? 因限流被拒的不算。真正到达接口、因其它原因失败的算。

收到 429 之后立刻重试安全吗? 只有空响应体的 404 那种情况适合立刻重试。真正的 429 立刻重试只会延长封锁时间 —— 先等。

推特上"rate limit exceeded"是什么意思? 你在窗口内发出的请求超过了允许量。 ★它是一个节流,不是一个惩罚★ —— 你的账号没有任何问题。

为什么提示"Sorry, you are rate limited"? 浏览撞上了阅读上限。 ★大量下拉或快速打开很多资料页时很常见。★

我为什么会被限流? 短时间内的请求量。 ★上限是按滚动窗口计的,所以它会自己解除★ —— 通常在 15 分钟内。

限流会持续多久? 直到窗口滚过去。 ★没有任何办法能加速它★,而且在此期间重试不会重置任何东西。

rate limiting 是什么? 对单位时间内请求数的一个上限。 ★任何正经接口都有★ —— 这是一个共享服务保持可用的方式。

什么会导致 429? 窗口内请求过多。 ★429 是服务器在说"慢一点",不是"你被封了"。★

能提高我的限额吗? 平台侧不能。 ★你能改变的是怎么花这些请求★ —— 批量和缓存对降低请求量的作用,大于任何配额提升。

怎么避免撞上限流? 能批量就批量、重复的就缓存、遇到 429 就退避 —— 这三条加起来比单纯提高并发重要得多。

token bucket 是什么? 一种逐渐回填而不是一次性重置的配额。 ★它奖励稳定的节奏,惩罚爆发。★

被限流意味着我被封号了吗? 不是。 ★限流拦的是请求;账号限制影响的是可见性★ —— 怎么区分。

推特的 rate limiting 是什么? 对你每个窗口能请求多少的上限。 ★任何共享服务都有★ —— 这是保护可用性的方式。

为什么提示"Sorry, you are rate limited"? 浏览越过了一个阅读上限。 ★大量下拉时很常见★,而且它会自己解除。

rate limit exceeded 怎么修? ★等窗口滚过去。★ 别的都没用 —— 在此期间重试什么也做不到。

能绕过限流吗? ★不能,而且尝试绕过正是会让情况升级的行为★ —— 重试风暴看起来就像自动化。

怎么提高接口限额? 平台侧不能。 ★改变你怎么花这些请求★ —— 批量对降低请求量的作用大于任何配额提升。

429 是什么意思? 窗口内请求太多。 ★它是"慢一点",不是"你被封了"。★

token bucket 是什么? 一种逐渐回填而不是一次性重置的配额。 ★它奖励稳定节奏,惩罚爆发。★

接口的限额是多少? ★公布的限额因端点和档位而异★ —— 实用的答案是"遇到 429 就退避",而不是去背数字。

限流影响读取还是发布? 都影响,而且是分开的。 ★阅读上限和动作上限是两个不同的窗口。★

限流多久解除? 阅读类通常 15 分钟内 —— ★是滚动的,所以它是渐进解除而不是一次性解除★。

限流会影响我整个账号吗? 只影响撞上它的那个会话或密钥。 ★它不是账号层面的惩罚。★

代码里处理 429 的正确方式是什么? ★指数退避,绝不用紧凑的重试循环★ —— 那个循环才是把"暂停"变成"问题"的东西。


如果你不想自己搭这一套

上面这些,就是直接对接 X 需要处理的东西:两种要分辨的限制机制、一种看起来像限流但其实不是的失败、避免在窗口头一分钟烧光额度的节流,以及对三种情况分别做出正确反应的重试逻辑。

这部分正是我们代劳的。我们的 API 给你一个接口和一个固定的按次价格 —— 没有窗口要你节流,没有属于你的速度限制,也没有 429 需要你去解读。响应时间中位数约 1.7 秒。

有两种情况我们不收钱:请求还没发出去就被我们拒绝的(比如参数写错),以及我方原因导致的错误。这两条你可以自己验证 —— 故意发一个错误请求,看余额动不动。

不过上面那些结论,无论你用不用我们都成立。这正是把它们写出来的意义。

延伸阅读:做一个翻页不会截断的粉丝追踪器 · 怎么在 X 上移除粉丝。