休市提醒日股将于 2026-11-03 休市(文化日),距今天还有 5 天。全部公告
文章

REST 接口返回 429 怎么办:三类限流的区分与排查清单

429 不是一个错误,而是三个:速率超限、日额度耗尽、月额度耗尽。本文拆解三类限流的错误码含义与正确应对,给出排查清单与带错误码区分的 Python 退避重试实现。

429 Too Many Requests 是行情 API 最常见的错误之一,也是最容易被误处理的一个。多数团队看到 429 的第一反应是「等一会儿重试」,但如果没先分清是哪一类限流,重试只会让情况更糟——在日额度已经耗尽的场景下,每一次重试都在继续消耗你见底的配额。

429 不是一个错误,是三个

行情接口的 429 至少分三类,含义与应对方式完全不同:

错误码含义正确应对
RATE_LIMIT_EXCEEDED每分钟速率超限指数退避后重试
DAILY_LIMIT_EXCEEDED当日额度耗尽停止重试,改用流式或升级套餐
MONTHLY_LIMIT_EXCEEDED当月额度耗尽停止重试,等重置或升级套餐

这个分类由接口文档明确定义,响应体里的 code 字段会告诉你具体是哪一种。所以第一步永远是读 code,而不是只看 HTTP 状态码。

RATE_LIMIT_EXCEEDED:速率超限,可以退避重试

这是唯一「重试有意义」的一类。它说明你的瞬时并发或每分钟请求数超过了套餐允许的上限,但总配额还有余量。

正确做法是指数退避加抖动(jitter):

  • 第一次重试等 1 秒,第二次 2 秒,第三次 4 秒,并乘以一个 0.5–1.0 的随机系数
  • 设置最大重试次数(建议 3–5 次),超过就放弃并上报告警
  • 不要把退避逻辑写在裸循环里,否则多个请求会同时重试,形成新的尖峰

DAILY_LIMIT_EXCEEDED:额度耗尽,重试是纯浪费

当日额度已经用完。此时重试不仅无效,还会掩盖真正的问题——你的架构在超额消耗额度。

常见成因有三个:

  1. 用 REST 轮询做实时行情。这是最典型的原因。轮询请求量与标的数量、刷新频率成正比,很快会击穿任何套餐。
  2. WebSocket 频繁重连。建连握手计入额度,如果重连逻辑有缺陷(心跳超时过短、断线后无退避立即重连),会持续消耗额度。
  3. 缺少缓存。同一批标的在同一秒内被多个业务模块各拉一次,额度按次数翻倍消耗。

正确的应对不是调重试参数,而是改架构:实时数据走 WebSocket,REST 只用于查询与回填。

MONTHLY_LIMIT_EXCEEDED:本月已经结束

当月额度耗尽,通常意味着容量规划出了问题。这时有三个选择:升级套餐、等到下月重置,或者把非关键请求降到更低频。

建议在额度使用到 70% 与 90% 时分别告警,不要等 100% 才发现。

排查清单

  1. 先读响应体的 code,确定属于三类中的哪一类。
  2. 保留 X-Request-Id。这是响应头里的请求追踪 ID,联系支持时提供它能大幅加快定位。
  3. 统计请求来源:把日额度消耗按业务模块拆分,找出异常增长的那一个。
  4. 检查是否有轮询在跑:搜索代码里对行情接口的定时调用。
  5. 检查重连策略:确认 WebSocket 断线后没有无退避的立即重连。
  6. 加一层本地缓存:同一标的在同一时间窗口内只请求一次。
  7. 先用配额计算器估算:在改代码前先算清真实需求。

Python 实现:带错误码区分的退避重试

Python
import random, time
import httpx

API_KEY = "your_api_key"
BASE = "https://api.tiqex.io/api/v1/market"

# 只有速率超限才值得重试;额度耗尽必须立即失败并上报告警
RETRYABLE = {"RATE_LIMIT_EXCEEDED"}

def get_quote(codes: str, max_retries: int = 4):
    for attempt in range(max_retries):
        resp = httpx.get(
            f"{BASE}/quote",
            params={"codes": codes},          # 单次最多 50 个 codes
            headers={"X-API-Key": API_KEY},
            timeout=10,
        )
        if resp.status_code == 200:
            return resp.json()

        body = resp.json() if resp.content else {}
        code = body.get("code")
        request_id = resp.headers.get("X-Request-Id")

        if resp.status_code == 429 and code in RETRYABLE:
            backoff = (2 ** attempt) * (0.5 + random.random())
            print(f"[{code}] 速率超限,{backoff:.1f}s 后重试 (attempt {attempt + 1})")
            time.sleep(backoff)
            continue

        # 额度耗尽或其它错误:不重试,直接抛出并带上追踪 ID
        raise RuntimeError(f"请求失败 code={code} requestId={request_id}")

    raise RuntimeError("重试次数已用尽")

print(get_quote("BTCUSDT,ETHUSDT,AAPL"))

关键点是 RETRYABLE 白名单:只有 RATE_LIMIT_EXCEEDED 允许重试,另外两类直接失败。这样能避免在额度耗尽时把剩余配额继续烧掉。

从症状反推根因:三个真实场景

把三类 429 与实际现象对应起来,排查会快很多。

场景一:每天早上 9 点开始报错,下午缓解。 典型的日额度问题。开盘前后业务量上升,轮询或查询请求集中,额度在几个小时内被耗尽。根因通常是缺少缓存,或者有定时任务在开盘时批量拉取全量标的。

场景二:平时正常,一到大行情就大面积 429。 这多半是速率超限。行情剧烈波动时,客户端会因为收到更多 tick 而触发更多下游 REST 查询,形成正反馈。正确做法是给下游查询加队列与限流,而不是单纯加大退避时间。

场景三:每月 20 号左右开始报错。 典型的月额度规划问题——按平均值设计的额度,被月初的一次数据回填或压测消耗掉了大半。这种情况应该把批量回填任务与生产流量分开计量。

提前发现:额度监控的两个阈值

不要等 429 出现才开始处理。额度监控只需要两个阈值:

  • 70% 告警:还有调整空间,可以提前把非关键请求降频。
  • 90% 告警:进入应急状态,立即暂停批量回填任务,并检查是否有轮询异常。

实现上不需要复杂系统:在客户端记录每次成功请求,按天聚合,用一个定时任务比对套餐额度即可。关键是按业务模块分开统计,否则总量数字无法定位到具体来源。

常见问题

429 会消耗我的额度吗?

被拒绝的请求通常不计入业务额度,但速率超限的请求仍会触发速率限制计数。真正棘手的是额度耗尽类错误——此时你的正常请求已经全部失败,重试没有意义。

为什么升级套餐后还是 429?

如果 code 是 RATE_LIMIT_EXCEEDED,说明瓶颈在每分钟速率而不是总量,需要检查并发数。如果是 DAILY_LIMIT_EXCEEDED,说明额度消耗速度太快,升级只是延缓问题——根因通常是 REST 轮询,应改用 WebSocket。

429 和 403 应该怎么区分排查?

429 是「用量超限」,403 是「权限不足」。403 常见于套餐未开通某市场、密钥范围受限或 IP 策略拦截,不会因为等待而恢复。如果等一段时间后自动恢复,基本可以确定是 429 而不是 403。

怎么知道额度用在哪里了?

每日额度与每分钟速率(RPM)的规则见错误与限流文档。排查时建议在客户端按模块打点,把每次请求归类记录,这样能快速定位异常来源。

WebSocket 重连会消耗额度吗?

会。建连握手计入每日额度与 RPM,握手之后的 tick 推送不计费。所以重连策略必须有退避,不要用频繁重连来「保活」。

下一步

先确认你遇到的是哪一类 429。如果答案是「日额度耗尽」,真正要做的不是调重试参数,而是把实时数据迁到 WebSocket。错误码与限流规则的完整定义见 错误与限流文档,改架构前可先用 配额计算器 估算需求;如果发现现有 套餐 额度长期不够用,再考虑升级。鉴权头的正确用法见 REST 鉴权指南。