ByteScope

429

HTTP 429 Too Many Requests 怎麼修

4xx 用戶端錯誤IETF 標準RFC 6585

某個限流器認定你問得太頻繁了——這是一句政策上的宣告,不是協定上的,所以兩個都回 429 的服務,數的可能是完全不同的東西。

為什麼會看到 429,以及該怎麼處理

同樣三個數字,你站在請求的哪一邊,它就是不同的問題。直接讀你自己那一塊。

用 curl 重現 429

對著出錯的那個網址跑一次。它只印狀態碼、不印錯誤頁,所以你看到的是伺服器真正說了什麼,而不是瀏覽器畫出來的東西。

curl
curl -sS -o /dev/null -w '%{http_code} %header{retry-after}\n' 'https://example.com/api/health?n=[1-50]'

方括號是 curl 的網址展開語法,所以這是對同一個資源送出五十次連續請求,查詢字串每次都變、讓快取插不上手,而每次會印一行。`%header{retry-after}` 需要 curl 7.84 以上,它會印出那個回應標頭,伺服器沒送就印空的。你要看的是轉折點:200 變成 429 的那個請求編號,就是限流器允許的爆量額度;而馬上把同一行再跑一次,就知道時間窗是固定的還是滑動的——第二次立刻 429 代表窗還沒滾動,重新拿到額度代表滾了。`retry-after` 那一欄是空的,本身就是一項發現,因為 RFC 6585 把那個標頭訂成 MAY,它不在就代表每個用戶端都得自己猜要等多久。那一欄也要留意 503:nginx 的 `limit_req` 預設回 503,所以一次在固定的請求編號上轉成 503 的執行,那還是限流。想量的是持續速率而不是爆量額度的話,加上 `--rate 10/s`(同樣要 curl 7.84 以上)替整串請求配速,然後把數字往下調,調到 429 不再出現為止。

429 實際上在講什麼

429 根本不在 RFC 9110 裡:它來自 RFC 6585 §4,定義是使用者在一段給定的時間內送了太多請求,並且說回應的表示「應該」解釋這個狀況、「可以」帶一個 `Retry-After` 標頭說要等多久。這兩條都是刻意寫鬆的,而後面那一段更鬆——RFC 6585 直接寫明,它不定義來源伺服器怎麼辨識使用者,也不定義怎麼數請求。就是這一句話,讓這個代碼在不同服務之間的行為差這麼多:某個 API 是按 token 數、另一個按 IP 位址數、再另一個按端點或按帳號數,而它們誰都沒有違反任何規定。這也代表你眼前那個數字,是在講一份你從回應裡讀不出來的政策,所以想從一個 429 反推出時間窗,那是在猜。`Retry-After` 本身定義在 RFC 9110 §10.2.3,有兩種用戶端都得處理的語法——以秒為單位的延遲,或一個 HTTP-date;而既然 RFC 6585 把它訂成 MAY 不是 MUST,很多限流器兩種都不送。剩餘額度也沒有一個大家都同意的標頭;事實標準是 `X-RateLimit-Limit`、`X-RateLimit-Remaining` 和 `X-RateLimit-Reset`,而舉例來說,GitHub 的文件寫著它的 `x-ratelimit-reset` 是以秒為單位的 UTC epoch,所以要換算過才有意義。還有兩件事能省你時間。429 不在 RFC 9110 §15.1 那份可依啟發式規則快取的代碼裡,所以快取不會自己把它留著。另外,限流器說不的方式不只這一個代碼:nginx 的 `limit_req` 模組回的是 `limit_req_status`,而它預設是 503——所以現實世界裡有一大堆限流,從頭到尾根本不會以 429 的樣子出現。

429 最常被搞混的幾個代碼

容易搞混的怎麼分
503這一組裡代價最高的誤會,是 nginx 自己的限流器回的是 503 而不是 429:`limit_req_status` 預設就是 503,所以一個跟負載有關、而且錯誤紀錄裡有 `limiting requests, excess: ... by zone ...` 的 503,是限流不是當機。兩個代碼都可能帶 `Retry-After`,所以那個標頭分不出來——分得出來的是那行紀錄,以及這次失敗有沒有跟著請求量走。
403限流器可以用 429 以外的方式拒絕你。GitHub 的文件寫著超過它的速率限制時可能回 403 也可能回 429,而 Cloudflare 的限流規則可以把設定的動作換成直接封鎖。所以一個只在高負載時出現、等一下就好的 403,是限流不是權限問題,你去稽核角色設定是什麼都找不到的。
401這兩個會互相製造對方。一個每次 401 就更新 token 的用戶端,會把一個過期的 session 變成朝認證端點爆量——而那正是最可能被限流的端點——接著產生的 429 看起來就像一次不相干的當機。如果 429 集中在你的 token 端點上,bug 是用戶端的更新迴圈,不是伺服器的限制。
Retry-AfterRFC 6585 把這個標頭在 429 上訂成 MAY,所以它不在是合法的、也很常見;而 RFC 9110 §10.2.3 給了它兩種形式:以秒為單位的延遲,或一個 HTTP-date。一個只解析數字那種的用戶端,會安靜地把日期形式當成零、然後立刻重試——那是把一個 429 變成更長封鎖最快的方法。
Cloudflare當限制是在邊緣節點套用的,那個回應是 Cloudflare 寫的、不是你的來源機寫的,而那些請求在你的應用程式紀錄裡什麼都不會留——這正是它令人困惑的地方。一頁有品牌樣式、帶著 `cf-ray` 值的頁面就是破綻,而那個 ray id 就是客服查得到的東西;至於訪客看到的是 429 還是攔截頁,是那條限流規則自己的設定決定的。
原因片語
Too Many Requests
類別
4xx 用戶端錯誤
定義出處
RFC 6585
標準
IETF 標準

站上可以搭配的工具

這些全都在你的瀏覽器裡跑,不會上傳任何東西。

相關狀態碼

查 429 的時候,通常也會順手看一下這幾個。

常見問題

怎麼知道這個狀態碼是哪一台伺服器回的?

去讀回應標頭,不要讀那個網頁。curl -sS -o /dev/null -D - https://example.com/path 會只印標頭、不印內容,而 serverviacf-ray 三個加起來,就能指出是哪一層回答的。有 cf-ray 的值代表這個回應是 Cloudflare 處理的,那個值也是他們客服會跟你要的編號。想把邊緣節點整個排除掉,就加上 --resolve example.com:443:203.0.113.10 再打一次:它會照樣送出原本的主機名稱和 SNI,但直接連到你指定的來源位址——答案變了,就代表邊緣節點和來源機講的不是同一件事。

數字後面那句原因片語(像 Not Found)重要嗎?

不重要,而且絕對不要拿它來做判斷。RFC 9112 要求用戶端忽略原因片語,伺服器可以隨意改它,而 HTTP/2 和 HTTP/3 根本沒有這個欄位——所以在 HTTP/1.1 上是 404 Not Found,到了 HTTP/2 就只剩一個 404。這個網站會寫出登記在案的片語,是因為那是大家會拿去搜尋、也是會出現在 HTTP/1.1 紀錄裡的字串,不是因為有任何軟體依賴它。

這個代碼失敗的請求該不該重試?

5xx 和 429 可以重試,4xx 不要——下一次送過去也不會有任何不同。如果回應帶了 Retry-After 就照它做,RFC 9110 定義它就是為了這件事,值可以是秒數也可以是一個時間點。沒有的話,就用帶抖動的指數退避加上一個硬上限,而且只對冪等的方法這樣做:一個被重試的 POST 可能會刷兩次卡。對一台已經在失敗的伺服器丟出重試風暴,是短暫事故變成長時間事故最常見的那條路。

我可以讓自己的伺服器改回別的狀態碼嗎?

可以,唯一要守的規則是那個代碼必須是真的。所有會自動讀你回應的東西——搜尋引擎、監控、快取、用戶端的重試邏輯——都只看那個數字,從來不看那一頁寫了什麼。用 200 回一頁錯誤訊息,會把故障從你自己的警報裡藏起來;用 200 回一頁不存在的內容,會讓錯誤被當成內容收進索引;而對一個只是格式不對的請求回 500,會把值班的人送去堆疊裡錯的那一半。

429 還是卡住嗎?看完整的狀態碼對照表,或是回到上面,讀為你這一端寫的那一塊。