429
某個限流器認定你問得太頻繁了——這是一句政策上的宣告,不是協定上的,所以兩個都回 429 的服務,數的可能是完全不同的東西。
同樣三個數字,你站在請求的哪一邊,它就是不同的問題。直接讀你自己那一塊。
為什麼會看到它
通常是你自己的程式碼,卡在一個你不是故意寫出來的迴圈裡。常見的樣子有:一個 `useEffect` 的依賴每次 render 都在變,於是一個請求變成每個影格一個請求;一個沒有退避的重試,把一次失敗變成一陣爆量;好幾個元件在掛載時各自去打同一個端點;還有一個「每次 401 就更新 token」的機制,於是一個過期的 session 會朝那個最可能被限流的端點噴出一串請求。也有一種完全不是你的錯:當限制是按 IP 位址算的,一間辦公室、一個校園或一個 VPN 出口,會讓幾百個人看起來像一個非常忙碌的用戶端,而你完全正常的用量,是花在別人的額度上。
該怎麼做
讀回應上的 `retry-after` 並且照著做——RFC 9110 §10.2.3 允許秒數也允許 HTTP-date,所以兩種都要解析,不要假設一定是數字那種。如果它不在,就用帶抖動的指數退避,加上一個硬性的嘗試上限,因為很多用戶端同步重試,只會把當初造成限流的那陣爆量重新組出來。用 key 把進行中的請求去重,讓五個元件要同一個東西時只發一次呼叫,把結果快取到頁面生命週期結束,而且絕對不要在 render 裡重試。想確認是不是迴圈,把網路面板按時間排序,找那種以你 UI 完全解釋不了的間隔重複出現的同一個網址。還有,看到認證端點上的 429,請把它當成「token 更新迴圈」的證據,不是「伺服器很忙」的證據。
為什麼會看到它
先確定你站在限流器的哪一邊,因為這兩種情況毫無共通之處。如果你是用戶端,陷阱是共用出口:一整批伺服器透過同一個 NAT 位址去打別人的 API,對一個按 IP 算的限制來說就是「一個用戶端」,所以每個使用者看起來微不足道的流量,加總起來就頂到天花板,而且水平擴充只會讓它更糟、不會更好。如果你是限流器,那個回應是你的,而它的標記會說話。Cloudflare 的限流規則讓你選動作,所以同一個限流器可以呈現成 429、也可以呈現成攔截頁;GitHub 的文件也寫著超過它的限制時可能回 403 也可能回 429。nginx 的預設更意外:`limit_req` 回的是 `limit_req_status`,你不改的話就是 503,而它會往錯誤紀錄寫 `limiting requests, excess: ... by zone ...`——所以一個帶著那行紀錄的 503,是換了個號碼的限流。
該怎麼做
如果你是用戶端:在對外呼叫前面放「一個」共用的限流器,不要每個行程各一個;key 要照供應商 key 的方式去 key;並且去讀供應商的剩餘量和重置標頭,讓你「學會」額度,而不是靠把它用完來「發現」額度。如果你是限流器:在你調大任何數字之前,先讀設定裡的 zone 和 key,因為一個在代理後面用 `$binary_remote_addr` 當 key 的限流器看到的是代理的位址,除非先把真實用戶端位址還原回來,否則它會把整個網際網路當成一個用戶端來節流。把 `limit_req_status` 設成 429,讓用戶端分得出限流和當機;而且就算 RFC 只是建議,也要送 `Retry-After`——那是「用戶端會等」和「用戶端會狂敲」的差別。接著去錯誤紀錄裡看 zone 名稱,它會告訴你是哪條規則觸發的。
為什麼會看到它
網站在說,它最近從你、從你的網路,或從某個代表你在動作的 app 那裡收到太多請求了,它想要一段暫停。這通常不是你故意做的:某個在背景重新整理的擴充功能、同一個網站開了好幾個分頁沒關、一頁會自己重新載入的頁面,或者一個共用的辦公室、校園、VPN 位址——網站把後面所有人算成同一位訪客。它在設計上就是暫時的:這是網站在保護自己不被流量壓垮,不是你的裝置壞了,也不是你被封鎖。
該怎麼做
等一下,然後忍住不要重新整理。多數限制在幾秒到幾分鐘內就會解除,而一直重新整理會把計數器一直填滿,這就是為什麼猛按重整只會讓它拖更久、不會更快。把同一個網站重複的分頁關掉,把任何會輪詢它的擴充功能或腳本停掉;如果你在 VPN 或共用網路上,先關掉再試一次,因為那個限制有可能是把那個位址上的所有人一起算的。如果頁面上寫了要等多久,那個數字是網站自己的答案,也是你能拿到最短又可靠的那一個。如果它一直不解除,那就值得回報了。
對著出錯的那個網址跑一次。它只印狀態碼、不印錯誤頁,所以你看到的是伺服器真正說了什麼,而不是瀏覽器畫出來的東西。
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 根本不在 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 的樣子出現。
| 容易搞混的 | 怎麼分 |
|---|---|
| 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-After | RFC 6585 把這個標頭在 429 上訂成 MAY,所以它不在是合法的、也很常見;而 RFC 9110 §10.2.3 給了它兩種形式:以秒為單位的延遲,或一個 HTTP-date。一個只解析數字那種的用戶端,會安靜地把日期形式當成零、然後立刻重試——那是把一個 429 變成更長封鎖最快的方法。 |
Cloudflare | 當限制是在邊緣節點套用的,那個回應是 Cloudflare 寫的、不是你的來源機寫的,而那些請求在你的應用程式紀錄裡什麼都不會留——這正是它令人困惑的地方。一頁有品牌樣式、帶著 `cf-ray` 值的頁面就是破綻,而那個 ray id 就是客服查得到的東西;至於訪客看到的是 429 還是攔截頁,是那條限流規則自己的設定決定的。 |
這些全都在你的瀏覽器裡跑,不會上傳任何東西。
查 429 的時候,通常也會順手看一下這幾個。
去讀回應標頭,不要讀那個網頁。curl -sS -o /dev/null -D - https://example.com/path 會只印標頭、不印內容,而 server、via 和 cf-ray 三個加起來,就能指出是哪一層回答的。有 cf-ray 的值代表這個回應是 Cloudflare 處理的,那個值也是他們客服會跟你要的編號。想把邊緣節點整個排除掉,就加上 --resolve example.com:443:203.0.113.10 再打一次:它會照樣送出原本的主機名稱和 SNI,但直接連到你指定的來源位址——答案變了,就代表邊緣節點和來源機講的不是同一件事。
不重要,而且絕對不要拿它來做判斷。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 還是卡住嗎?看完整的狀態碼對照表,或是回到上面,讀為你這一端寫的那一塊。