429 Too Many Requests 怎麼辦:大模型 API 限流排查

部落格 / 排障指南

429 Too Many Requests 怎麼辦:大模型 API 限流排查

呼叫大模型 API 返回 429 Too Many Requests,意思是你在某個時間視窗裡用掉的配額超了,服務端暫時不接這個請求。它通常不是故障,也不是網路問題。處理順序是:先看響應頭和錯誤體,確認撞的是請求數(RPM)、token 數(TPM)還是賬戶額度;如果是前兩種,按 Retry-After 或重置時間等一會兒再重試,同時在客戶端加限速;如果是額度用完,重試多少次都沒用,要去充值或換套餐。

通用的 429 解釋文章大多隻講“請求太頻繁”,但大模型 API 多了一個維度:token。一次請求塞進十萬 token 的上下文,RPM 遠沒到上限,TPM 卻可能一下就滿了。下面按排查順序展開。

429 有哪幾種來源?

大模型 API 的 429 大致分三類,處理方法完全不同:

來源計量方式典型表現等待能否解決
請求數限額(RPM / RPD)每分鐘或每天的請求次數高併發、小請求時出現,比如批次分類能,視窗過去就恢復
token 限額(TPM / ITPM / OTPM)每分鐘輸入、輸出 token 數請求不多但上下文很長,比如 agent 讀整個倉庫能,但要控制單次請求大小
額度或餘額耗盡賬戶餘額、月度花費上限、訂閱額度不管請求多慢都報 429,或錯誤資訊提到 quota、billing不能,要處理賬戶

還有一類容易混進來:有些平臺對整個組織或專案統一計數,同一個 API Key 被幾臺機器、幾個同事同時用,各自看起來請求都不多,加起來已經超了。排查時要把同一組織下所有呼叫方算在一起。

怎麼從響應頭判斷撞的是哪一個?

最直接的辦法是把一次請求的完整響應頭列印出來。下面的命令用 -i 輸出響應頭,請求體只要一個很短的輸入,不會消耗多少額度:

# OpenAI 格式介面,$MODEL 換成你賬戶可用的模型名
curl -sS -i https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":1}" \
  | grep -iE '^(HTTP|x-ratelimit|retry-after)'

# Anthropic 格式介面
curl -sS -i https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d "{\"model\":\"$MODEL\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" \
  | grep -iE '^(HTTP|anthropic-ratelimit|retry-after)'

正常請求也會帶這些頭,所以不必等到報 429 才看。兩家的頭含義如下,具體欄位以官方文件為準,各家會調整:

響應頭含義
x-ratelimit-limit-requests / -tokensOpenAI:當前視窗的請求數、token 數上限
x-ratelimit-remaining-requests / -tokensOpenAI:視窗內還剩多少
x-ratelimit-reset-requests / -tokensOpenAI:多久後恢復,格式是時長,如 1s、6m0s、20ms
anthropic-ratelimit-requests-limit / -remaining / -resetAnthropic:請求數限額,reset 是 RFC 3339 時間戳
anthropic-ratelimit-input-tokens-*、-output-tokens-*、-tokens-*Anthropic:輸入、輸出 token 分開計,以及合計
retry-after建議等待的秒數,429 時最該看的一個

判斷方法很簡單:哪一項的 remaining 接近 0,撞的就是哪一項。remaining-requests 還剩很多而 remaining-tokens 是 0,就是 TPM;Anthropic 的輸入 token 先見底,說明問題出在上下文太長,而不是輸出太多。

錯誤體是第二個依據。OpenAI 格式的錯誤裡,code 為 rate_limit_exceeded 是頻率限額,insufficient_quota 是額度用完,後者雖然也是 429,但等待解決不了。Anthropic 的 429 錯誤體 type 是 rate_limit_error,message 會說明是哪類限額。Gemini 的 429 狀態是 RESOURCE_EXHAUSTED,details 裡可能帶建議的重試間隔。聚合閘道器和中轉服務會把上游的頭原樣透傳、改寫或者乾脆去掉,所以經過閘道器時,先看閘道器自己的文件。

Retry-After 該怎麼用?

原則是:有 Retry-After 就聽它的,沒有再用重置時間,兩樣都沒有才用指數退避。幾個容易出錯的細節:

  1. 單位。Retry-After 按 HTTP 規範可以是秒數,也可以是一個 HTTP 日期。大模型 API 一般給秒數,但解析程式碼最好兩種都支援。
  2. 值為 0 或很小。有時會拿到 retry-after: 0,立刻重試大機率還是 429。給一個下限,比如 1 秒,再加隨機抖動。
  3. 頭缺失。一些閘道器、代理或 SDK 封裝會丟掉這個頭。這時退回到 x-ratelimit-reset-*,再不行就指數退避:1、2、4、8 秒,封頂 60 秒左右,每次加 0 到 1 秒的隨機量,避免多個程序同時醒來再一起撞牆。
  4. 重試次數要封頂。3 到 5 次後仍然 429,就不該繼續重試,應該報出來,交給上層處理或者排隊。
  5. 額度類 429 不重試。錯誤碼是 insufficient_quota 或者資訊裡提到 billing、credit,直接失敗並提示,不然只是在浪費時間。

下面是一段 Python 的處理函式,不依賴特定 SDK:

import random, re, time
from email.utils import parsedate_to_datetime

def parse_duration(s):
    """解析 OpenAI 風格的時長,如 '6m0s'、'1.5s'、'20ms'。"""
    total = 0.0
    for num, unit in re.findall(r"([\d.]+)(ms|s|m|h)", s or ""):
        total += float(num) * {"ms": 0.001, "s": 1, "m": 60, "h": 3600}[unit]
    return total or None

def wait_seconds(headers, attempt):
    h = {k.lower(): v for k, v in headers.items()}
    ra = h.get("retry-after")
    if ra:
        try:
            sec = float(ra)
        except ValueError:  # HTTP 日期格式
            sec = parsedate_to_datetime(ra).timestamp() - time.time()
        return max(sec, 1.0) + random.random()
    for key in ("x-ratelimit-reset-tokens", "x-ratelimit-reset-requests"):
        sec = parse_duration(h.get(key))
        if sec:
            return max(sec, 1.0) + random.random()
    return min(2 ** attempt, 60) + random.random()

def call_with_retry(send, max_attempts=5):
    for attempt in range(max_attempts):
        resp = send()
        if resp.status_code != 429:
            return resp
        body = resp.text.lower()
        if "insufficient_quota" in body or "billing" in body:
            raise RuntimeError("額度或餘額不足,重試無效:" + resp.text[:200])
        time.sleep(wait_seconds(resp.headers, attempt))
    raise RuntimeError("連續 429,已放棄")

send 是一個發請求並返回 requests.Response 的函式。如果用官方 SDK,它們一般自帶重試,先查清楚預設重試幾次,避免自己的重試套在 SDK 的重試外面,次數翻倍。

怎麼在客戶端提前限速,而不是等 429?

重試只是兜底。批次任務更好的做法是按限額主動控速,讓請求幾乎不觸發 429。因為大模型同時限請求數和 token 數,限速器也要兩個桶一起算:

import threading, time

class DualBucket:
    """同時按每分鐘請求數和每分鐘 token 數限速。"""
    def __init__(self, rpm, tpm):
        self.cap = {"req": rpm, "tok": tpm}
        self.level = dict(self.cap)
        self.rate = {"req": rpm / 60, "tok": tpm / 60}
        self.t = time.monotonic()
        self.lock = threading.Lock()

    def acquire(self, est_tokens):
        est_tokens = min(est_tokens, self.cap["tok"])  # 單次超過桶容量時避免死等
        while True:
            with self.lock:
                now = time.monotonic()
                for k in self.level:
                    self.level[k] = min(self.cap[k], self.level[k] + (now - self.t) * self.rate[k])
                self.t = now
                if self.level["req"] >= 1 and self.level["tok"] >= est_tokens:
                    self.level["req"] -= 1
                    self.level["tok"] -= est_tokens
                    return
            time.sleep(0.05)

bucket = DualBucket(rpm=50, tpm=40_000)   # 示例數值,按你賬戶的實際限額填
# 每次請求前:bucket.acquire(輸入 token 估算 + max_tokens)

幾個實際要注意的地方:

  • rpm、tpm 取實際限額的八到九成,給其他程序和估算誤差留一點餘量。
  • token 估算要包含 max_tokens。有的平臺在請求開始時就按 max_tokens 預佔輸出額度,把 max_tokens 設得遠大於實際需要,會白白吃掉 TPM。
  • 多程序或多機器要共用一個桶。單機記憶體裡的桶管不住別的機器,可以放到 Redis 之類的共享儲存裡,或者把所有呼叫收到一個出口服務。
  • 用響應頭校準。每次響應後讀 remaining,如果它比你的桶估計的少很多,說明有別的呼叫方在共用額度。

怎麼從根上減少 429?

控速之後還頻繁撞 TPM,就該從請求本身下手:

  • 縮短上下文。agent 類工具容易把整個檔案甚至整個目錄塞進去。限制單次讀入的內容,或者先摘要再提問。
  • 用提示快取。重複的長系統提示可以快取。按 Anthropic 文件的說明,部分模型的快取讀取 token 不計入輸入 token 限額,具體規則和寫法見 Claude API 提示快取怎麼用才省錢。
  • 把輕任務分到另一組模型。分類、格式化這類任務用輕量模型,它們通常有單獨的限額,主模型的 TPM 留給難任務。思路見 多模型路由怎麼做。
  • 申請提高限額。多數平臺的限額隨用量等級提升,控制檯裡可以看到當前等級和升級條件。走正規渠道申請,不要靠多開賬號、輪換 Key 來疊加限額,這違反大多數服務條款。

429、529 和 503 有什麼區別?

這一節單獨列出來,是因為很多人把服務端過載當成自己超限,去改程式碼、降併發,其實問題不在自己這邊。

狀態碼誰的問題怎麼處理
429你的呼叫超過了分給你的配額按 Retry-After 等待,客戶端限速
529Anthropic 用來表示服務端整體過載(overloaded_error)退避重試,看狀態頁;降低自己的併發作用不大
503服務暫時不可用,可能是過載、維護或閘道器後端異常退避重試,看狀態頁

區分的關鍵是響應頭:429 時你的 remaining 通常已經見底;如果 remaining 還很充足卻報 429,或者大批不相關的使用者同時報限流,就更像是服務端在收緊或出了狀況。完整的錯誤碼含義和哪些該重試,可以對照 大模型 API 錯誤碼速查。

怎麼判斷是不是很多人同時被限流?

自己的限額沒問題卻持續 429,可以這樣查:

  1. 開啟 cdnping 的 掛了嗎 頁面,找到對應服務,比如 Claude 或 ChatGPT。頁面綜合了官方狀態頁、每 5 分鐘一次的 API 探測和使用者一鍵報告,報告型別裡有“限流”一項。如果最近一段時間“限流”報告明顯變多,說明不只是你一個人。你也可以順手提交一條。
  2. 看 API 狀態 頁,例如 /status/anthropic 和 /status/openai。它從 Cloudflare 邊緣每 5 分鐘探測官方 API 入口,並同步官方狀態頁事件,能看出入口本身是否異常。
  3. 讀官方狀態頁時注意事件等級和受影響元件,“部分模型錯誤率升高”這類 minor 事件經常就表現為 429 或 529 增多。怎麼讀狀態頁可以參考 怎麼看 OpenAI 狀態頁和 Claude 狀態頁。

如果你用的是 Codex 這類按訂閱計量的工具,遇到的“用量上限”和 API 的 429 是兩套機制,額度何時恢復要看平臺公告,可以參考 Codex 額度重置時間線。

一份可以照著走的檢查表

遇到 429 時,按順序檢查:

  1. 列印響應頭和錯誤體,確認是 RPM、TPM 還是額度。
  2. 錯誤碼是 insufficient_quota 或提到 billing:停止重試,去控制檯查餘額和花費上限。
  3. 有 retry-after:按它等,值為 0 時至少等 1 秒並加抖動。
  4. 沒有 retry-after:讀 x-ratelimit-reset-* 或 anthropic-ratelimit-*-reset,再不行用指數退避,封頂重試次數。
  5. 確認同一組織、同一 Key 下還有誰在呼叫,把它們算進總量。
  6. 檢查 SDK 自帶重試,避免重試疊加。
  7. 批次任務加雙桶限速,token 估算包含 max_tokens。
  8. 限額充足卻仍然 429:去 /down 看“限流”報告,去 /status 看探測和官方事件,確認是不是服務端的問題。

相關文章

繼續閱讀