跳轉到主要內容

API 調用頻率限制與 429 Too Many Requests:支付接口如何限流,你該怎麼處理

API 調用頻率限制是什麼、支付接口為什麼返回 429 Too Many Requests、如何遵循 Retry-After 重試,以及如何設計調用方式避免觸發限流。

發佈於 約 4 分鐘閲讀

手繪插畫:一個漏斗上方堆積着請求箭頭,旁邊是 429 指示牌和標着 Retry-After 的沙漏,還有一個指着它們的人
本文目錄
  1. API 頻率限制是如何運作的?
  2. 429 響應長什麼樣?
  3. 如何處理 429 Too Many Requests
  4. 如何避免觸發頻率限制
  5. 頻率限制會影響真實買家嗎?
  6. 測試你的 429 處理邏輯
  7. 總結

API 調用頻率限制(rate limit)規定了客戶端在一段時間內最多能發多少個請求。超過之後,接口不再執行,而是返回 429 Too Many Requests,通常還會帶一個 Retry-After 響應頭,告訴你要等多久。支付接口做限流,是為了不讓某一個接入的失控循環拖慢所有人的收銀台;而只有當代碼的調用頻率遠超業務實際需要時,限流才會真正起作用。

這篇文章説明支付接口的頻率限制如何運作、KukoPay 的具體限額、如何正確處理 429,以及如何設計接入,讓它在正常運行中永遠不會碰到限流。

API 頻率限制是如何運作的?

大多數接口按客戶端(這裏是商戶賬戶)在一個時間窗口(通常是一分鐘)內計數。窗口內配額用完之前正常服務,用完之後,每個請求都會得到 429,直到窗口滾動到下一個。

KukoPay 按商戶賬戶使用固定的一分鐘窗口,不同類型的請求有各自的配額:

類型限額覆蓋範圍
寫接口每分鐘 120 次創建訂單、退款及其他變更
讀接口每分鐘 600 次查詢和列表
導出每分鐘 10 次CSV 導出(format=csv)
易支付網關每分鐘寫 300 次、讀 600 次易支付兼容接口,與 /v1 分開計數
  • 配額屬於賬戶:賬戶下的每個應用、每把密鑰,無論沙箱還是正式環境,都共用同一組額度。多簽發幾把密鑰並不會增加容量。
  • 導出有單獨的小額度,因為一次導出最多會掃描 10,000 行;運行導出的對賬任務不會擠佔收銀台下單的寫額度。
  • 易支付的流量單獨計數,所以由買家驅動的易支付收款不會用光你的 API 額度。

429 響應長什麼樣?

HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-Request-Id: req_31b6c0d7f2a94e15b8c3d0a7e6f45219
{
  "code": 429,
  "error": "rate_limited",
  "type": "rate_limit_error",
  "message": "…",
  "request_id": "req_31b6c0d7f2a94e15b8c3d0a7e6f45219"
}

Retry-After 是距離當前窗口結束的秒數,也就是重試最早可能成功的時刻。這個錯誤被標記為可以安全重試:請求沒有被執行,稍後重試不會產生重複。完整的錯誤碼見 錯誤碼。

如何處理 429 Too Many Requests

  1. 等待 Retry-After 指定的時間後再重試。 提前重試只會再得到一個 429。
  2. 加入隨機抖動,也就是額外加一段隨機延遲,避免同時被限流的多個 worker 在同一秒一起重試。
  3. 限制重試次數,超過之後報錯,而不是無限重試。
  4. 重試時使用同一個 Idempotency-Key。 429 在請求被處理之前就已經判定,永遠不會存到這個鍵下,所以重試會真正執行;如果之前某次嘗試其實成功了,這個鍵也能防止重複。冪等鍵的原理見 冪等鍵是什麼。
async function callWithRetry(doRequest, maxAttempts = 5) {
  for (let attempt = 1; ; attempt++) {
    const res = await doRequest() // 每次都使用同樣的請求體和 Idempotency-Key
    if (res.status !== 429 || attempt >= maxAttempts) return res
 
    const retryAfter = Number(res.headers.get("Retry-After")) || 2 ** attempt
    const jitter = Math.random() * 1000
    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 + jitter))
  }
}

對於 429 以外的錯誤,比如超時和 5xx,請改用指數退避,做法見冪等鍵指南裏的重試循環。

如何避免觸發頻率限制

大多數 429 來自接入方式,而不是真實的業務量。每分鐘 120 筆訂單,一天就超過 170,000 筆。

  • 用 Webhook 代替輪詢。 每隔幾秒查一次訂單狀態、直到它變為已支付,是耗光讀額度的典型方式。payment.succeeded Webhook 會在付款成功的那一刻通知你;如果感覺回調沒到,原因排查見 收不到支付 Webhook 回調怎麼辦。
  • 必須輪詢時要剋制。 對於處理中的退款,每 10 秒查一次、最多持續 5 分鐘,之後改為低頻查詢。
  • 翻頁時用大頁。 列表接口支持 limit 和游標分頁,一次取 100 條,而不是很多個小頁。
  • 一次導出,而不是逐條查詢。 對賬時,一次 format=csv 導出一天的資金流水,就能取代成千上萬次查詢,見 支付對賬怎麼做。
  • 把自己的批處理任務分散開。 大規模的數據回填分攤到幾分鐘內執行,不要一次全部發出。
  • 不要在緊湊循環裏重試。 每次出錯就立即重試的循環,會把一次失敗放大成幾百個請求。

頻率限制會影響真實買家嗎?

限額只作用於用你的密鑰發起的 API 調用,也就是你服務器的流量。在託管收銀台或通過收款鏈接付款的買家,不會消耗你的 API 額度;確認付款和重試 Webhook 的任務也在平台一側運行。不過,你服務器上一個失控的任務仍然可能延遲你自己的新訂單,這正是要儘早修好輪詢循環的最好理由。

測試你的 429 處理邏輯

因為沙箱和正式環境的調用計入同一個賬戶級額度,不要用壓測接口的方式去觸發 429:一波測試請求會在同一分鐘裏限流你的正式訂單。應該去測試處理邏輯本身:在你的 HTTP 客戶端前放一個樁(stub),讓它返回帶 Retry-After 頭的 429,檢查你的代碼是否會等待、加入抖動、保持同一個 Idempotency-Key,並在達到上限後停止。值得用真實接口覆蓋的沙箱場景,見 支付測試卡號大全。

總結

  • API 頻率限制規定了每段時間的請求上限,超過後返回帶 Retry-After 的 429 Too Many Requests;
  • KukoPay 每個商戶賬戶每分鐘允許寫 120 次、讀 600 次、導出 10 次,所有應用、密鑰和兩個環境共用;
  • 遇到 429,等待 Retry-After 秒並加上抖動,限制重試次數,並保持同一個 Idempotency-Key;
  • 用 Webhook 代替輪詢、用大頁翻頁、用 CSV 導出、分散批處理任務,就能遠離限流;
  • 託管收銀台上的買家不消耗你的 API 額度;用樁來測試 429 處理,而不是壓測。

在沙箱裏把它跑通

註冊商戶後即可申請測試密鑰,不需要等待資質審核。