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 鑑權指南。