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

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