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:额度耗尽,重试是纯浪费
当日额度已经用完。此时重试不仅无效,还会掩盖真正的问题——你的架构在超额消耗额度。
常见成因有三个:
- 用 REST 轮询做实时行情。这是最典型的原因。轮询请求量与标的数量、刷新频率成正比,很快会击穿任何套餐。
- WebSocket 频繁重连。建连握手计入额度,如果重连逻辑有缺陷(心跳超时过短、断线后无退避立即重连),会持续消耗额度。
- 缺少缓存。同一批标的在同一秒内被多个业务模块各拉一次,额度按次数翻倍消耗。
正确的应对不是调重试参数,而是改架构:实时数据走 WebSocket,REST 只用于查询与回填。
MONTHLY_LIMIT_EXCEEDED:本月已经结束
当月额度耗尽,通常意味着容量规划出了问题。这时有三个选择:升级套餐、等到下月重置,或者把非关键请求降到更低频。
建议在额度使用到 70% 与 90% 时分别告警,不要等 100% 才发现。
排查清单
- 先读响应体的
code,确定属于三类中的哪一类。 - 保留
X-Request-Id。这是响应头里的请求追踪 ID,联系支持时提供它能大幅加快定位。 - 统计请求来源:把日额度消耗按业务模块拆分,找出异常增长的那一个。
- 检查是否有轮询在跑:搜索代码里对行情接口的定时调用。
- 检查重连策略:确认 WebSocket 断线后没有无退避的立即重连。
- 加一层本地缓存:同一标的在同一时间窗口内只请求一次。
- 先用配额计算器估算:在改代码前先算清真实需求。
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 鉴权指南。