API 又被鎖?別再暴力重試!資深工程師教你用『指數退讓』打造 WordPress 永不卡關的優雅串接

2025/07/15 | API 串接與自動化

API 又被鎖?別再暴力重試!資深工程師教你用『指數退讓』打造 WordPress 永不卡關的優雅串接

嗨,我是浪花科技的 Eric。身為一個整天跟程式碼打交道的工程師,最怕的不是 Bug,而是那種「時好時壞」的 Bug。尤其是在串接第三方 API 的時候,本地測試跑得順順的,一上線,客戶流量一進來,Slack 通知就開始狂響:「Error: 429 Too Many Requests」。啊,又是這個該死的 API Rate Limit

很多剛入門的開發者,甚至是某些資深工程師(我就不說是誰了),遇到這種問題的第一個反應就是:「啊就再試一次就好啦!」然後寫一個簡單的 `for` 迴圈或 `while` 迴圈去暴力重試。老兄,這不是在玩遊戲刷首抽啊!這種做法不但粗暴,而且往往會讓情況變得更糟,直接被對方伺服器封鎖 IP,從「暫時請你喝杯茶」變成「永久黑名單」。今天,我就來跟大家囉嗦一下,如何在 WordPress 中用更優雅、更專業的方式來處理 API Rate Limit 與重試機制,讓你寫的程式碼不只「能動」,更是「穩定可靠」。

一、為什麼 API 需要「速率限制 (Rate Limit)」?它不是故意找麻煩

在我們開罵之前,先搞懂為什麼幾乎所有公開的 API 服務都要做 Rate Limit。這就像一家熱門的餐廳,如果沒有排隊叫號系統,所有客人一窩蜂擠進去,結果就是廚房癱瘓、服務生崩潰,最後誰都吃不到飯。API 伺服器也是一樣的。

Rate Limit 的三大核心目的:

  • 維持服務穩定性:防止惡意或寫得不好的程式在短時間內發送大量請求,把伺服器資源耗盡,影響到其他正常使用者。
  • 確保公平使用:資源是有限的,速率限制確保每個使用者都能在一個公平的基礎上使用服務,避免被少數「大戶」壟斷。
  • 成本與安全考量:每一次 API 請求都代表著伺服器的運算成本(CPU、記憶體、頻寬)。限制請求數量也能有效防止 DDoS 攻擊等濫用行為。

所以,下次看到 429 錯誤時,先別急著怪對方小氣。換個角度想,這其實是一種保護機制。我們作為開發者,要做的是學會「讀懂空氣」,尊重並適應這些規則。

二、API 的潛規則:學會讀懂 Rate Limit 的回應標頭 (Headers)

一個設計良好的 API,在回覆 429 錯誤時,通常不會只給你一個冷冰冰的狀態碼。它會在 HTTP Response Headers 中提供線索,告訴你「遊戲規則」。這就像是夜店門口的保鑣,他不會直接把你打出去,而是會告訴你:「現在客滿,大概半小時後再來看看。」

在 WordPress 中,我們最常用 `wp_remote_get()` 或 `wp_remote_post()` 來發送 API 請求。我們可以從回傳的結果中,用 `wp_remote_retrieve_headers()` 函式來取得這些重要的標頭資訊。常見的 Rate Limit 相關標頭有:

  • X-RateLimit-Limit: 在目前時間窗格內,你總共可以發送多少次請求。
  • X-RateLimit-Remaining: 在目前時間窗格內,你還剩下多少次請求額度。
  • X-RateLimit-Reset: 時間窗格重置的 Unix 時間戳 (Timestamp)。告訴你何時額度會被補滿。
  • Retry-After: 更直接的指示,告訴你「至少」要等幾秒後再重試。

身為一個專業的工程師,我們的程式碼不應該是「盲目地衝」,而是在每次發送請求後,主動去讀取這些 Headers,動態調整我們的請求頻率。如果看到 `X-RateLimit-Remaining` 剩下不多了,就該考慮讓程式「睡一下」,而不是硬要把它用完。

三、重試機制的演進:從暴力到優雅的藝術

好了,理論說完了,來點實際的。當我們真的收到 429 錯誤時,該怎麼辦?這就是「重試機制 (Retry Mechanism)」登場的時候了。但重試也是有分等級的。

等級一:天真無邪的固定延遲重試

最直覺的想法就是:「失敗了?那我等個 5 秒再試一次。」


<?php
function naive_retry_api_call($url, $args, $retries = 3) {
    for ($i = 0; $i < $retries; $i++) {
        $response = wp_remote_get($url, $args);
        if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
            return $response; // 成功了,直接回傳
        }

        // 如果是 429 錯誤,就等一下
        if (wp_remote_retrieve_response_code($response) === 429) {
            sleep(5); // 不管三七二十一,就是睡 5 秒
        }
    }
    return new WP_Error('api_failed', 'API request failed after multiple retries.');
}
?>

這段程式碼的問題在哪?如果剛好有 100 個行程同時觸發了這個函式,它們都會在同一時間失敗,然後「一起」等 5 秒,再「一起」發送請求。這會造成所謂的「雷鳴群體效應 (Thundering Herd Problem)」,對 API 伺服器造成一波又一波的同步衝擊,結果就是大家一起繼續被 block。

等級二:工程師的選擇 – 指數退讓 (Exponential Backoff) 與抖動 (Jitter)

這才是我們今天要談的重點。指數退讓 的核心思想是,每次重試的等待時間都要以指數級增長。例如:第一次失敗等 2 秒,第二次失敗等 4 秒,第三次等 8 秒… 這樣可以快速拉開每次重試的時間間隔,給伺服器喘息的空間。

但光這樣還不夠完美。如果大家都是 2、4、8 秒這樣等,還是有可能撞在一起。所以我們要加入「抖動 (Jitter)」,也就是在等待時間上,再增加一個隨機的微小變動。這樣就能確保每個行程的重試時間點都是錯開的。

聽起來很複雜?直接看程式碼吧,這是我個人很喜歡用的一個模式:


<?php
/**
 * 使用指數退讓和抖動策略來進行穩健的 API 請求。
 *
 * @param string $url API 的 URL。
 * @param array $args wp_remote_get 的參數。
 * @param int $max_retries 最大重試次數。
 * @param int $initial_delay 初始延遲時間(毫秒)。
 * @return array|WP_Error 成功時回傳 response,失敗時回傳 WP_Error。
 */
function robust_api_call($url, $args = [], $max_retries = 5, $initial_delay = 1000) {
    for ($attempt = 0; $attempt < $max_retries; $attempt++) {
        $response = wp_remote_get($url, $args);

        if (is_wp_error($response)) {
            // 處理 cURL 錯誤等網路問題
            // ... log error ...
            continue; // 繼續嘗試
        }

        $status_code = wp_remote_retrieve_response_code($response);

        if ($status_code >= 200 && $status_code < 300) {
            return $response; // 請求成功!
        }

        if ($status_code === 429 || $status_code >= 500) {
            // 遇到速率限制或伺服器端錯誤,準備重試
            $headers = wp_remote_retrieve_headers($response);
            
            if (isset($headers['retry-after'])) {
                // 如果 API 有明確指示,就聽它的
                $delay_seconds = (int) $headers['retry-after'];
                sleep($delay_seconds);
                continue;
            }
            
            // 計算指數退讓時間
            $delay = $initial_delay * pow(2, $attempt);
            
            // 加入抖動 (Jitter),這裡用的是 Full Jitter
            $jitter = mt_rand(0, $delay);
            
            // 總等待時間(轉為微秒 usleep)
            $wait_time = ($delay + $jitter) * 1000; 
            usleep($wait_time);

        } else {
            // 其他客戶端錯誤 (如 400, 401, 403),通常重試也沒用
            return new WP_Error('client_error', 'Client error: ' . $status_code, ['status' => $status_code]);
        }
    }

    return new WP_Error('max_retries_exceeded', 'API request failed after ' . $max_retries . ' attempts.', ['status' => $status_code]);
}
?>

看看這段程式碼,是不是優雅多了?我們不只處理了 429,連 5xx 的伺服器暫時性錯誤也一併考慮進去。優先遵守 `Retry-After` 標頭,如果沒有,才啟用我們自己的指數退讓+抖動策略。這才是專業開發者該有的樣子,不是嗎?把這種函式封裝起來,以後所有 API 串接都用它,整個專案的穩定性直接提升一個檔次。

四、攻守互換:如何在自己的 WordPress 網站上實作 Rate Limit

有時候,我們不只是 API 的「消費者」,也可能是「提供者」。例如,你用 `register_rest_route()` 建立了一個自訂的 API 端點,提供給手機 App 或其他服務串接。這時候,你就需要考慮為自己的 API 加上速率限制,保護你的網站。

在 WordPress 中,有一個非常好用的內建工具可以幫我們實現這個功能:Transients API

Transients API 是一種暫時性的快取機制,我們可以利用它來記錄某個 IP 在特定時間內的請求次數。它的好處是底層會自動選擇最有效率的儲存方式(例如 Redis 或 Memcached,如果有的話),而且資料會自動過期,非常適合用來做 Rate Limit 的計數器。

這是一個簡單的實作範例,你可以把它加到你的 `functions.php` 或是一個自訂外掛中:


<?php
add_filter('rest_pre_dispatch', 'my_custom_api_rate_limiter', 10, 3);

function my_custom_api_rate_limiter($result, $server, $request) {
    // 只針對我們自訂的某個 API 路由
    if (strpos($request->get_route(), '/my-custom-namespace/v1/') === false) {
        return $result;
    }

    $limit = 100; // 每小時最多 100 次請求
    $ip = $_SERVER['REMOTE_ADDR'];
    $transient_key = 'rate_limit_' . $ip;

    $request_count = get_transient($transient_key);

    if ($request_count === false) {
        // 第一次請求,設定計數器為 1,過期時間為 1 小時
        set_transient($transient_key, 1, HOUR_IN_SECONDS);
    } elseif ($request_count >= $limit) {
        // 超過限制,回傳 429 錯誤
        return new WP_Error(
            'rest_too_many_requests',
            'Too many requests. Please try again later.',
            ['status' => 429, 'headers' => ['Retry-After' => HOUR_IN_SECONDS - (time() - (get_option('_transient_timeout_' . $transient_key) - HOUR_IN_SECONDS))]]
        );
    } else {
        // 還在額度內,計數器 +1
        set_transient($transient_key, $request_count + 1, HOUR_IN_SECONDS);
    }

    return $result;
}
?>

這個範例透過 `rest_pre_dispatch` 這個 filter hook,在 WordPress REST API 處理請求前插入我們的檢查邏輯。它會根據使用者的 IP 建立一個 transient,如果請求次數超限,就回傳一個標準的 `WP_Error` 物件,並附上 429 狀態碼和 `Retry-After` 標頭。你看,攻守兼備,才是大師風範。

處理 API Rate Limit 與重試機制,看的是細節,測的是開發者的耐心與遠見。一個好的策略,可以讓你的應用程式在面對不穩定的網路環境和嚴格的 API 政策時,依然表現得像個紳士,從容不迫。下次再遇到 API 串接問題,別再只是 `sleep(5)` 了,試試看指數退讓的威力吧!

延伸閱讀

如果你在 WordPress API 串接、效能優化或打造複雜的自動化流程時遇到了瓶頸,浪花科技的團隊擁有豐富的實戰經驗。我們不只是寫「能動」的程式碼,我們打造的是穩定、可擴展、能為您帶來商業價值的解決方案。歡迎與我們聯繫,讓專業的工程師團隊為您的專案保駕護航。

常見問題 (FAQ)

Q1: 到底什麼是 API 速率限制 (API Rate Limit)?

A: API 速率限制是 API 服務提供商用來控制單位時間內客戶端可以發送多少次請求的一種機制。這就像是高速公路的流量管制,目的是為了防止伺服器因過多請求而過載,確保所有使用者都能獲得穩定、公平的服務品質。當你超過限制時,通常會收到一個 HTTP 429 “Too Many Requests” 的錯誤回應。

Q2: 為什麼「指數退讓 (Exponential Backoff)」比簡單的固定延遲重試更好?

A: 簡單的固定延遲重試(例如每次都等5秒)可能會導致「雷鳴群體效應」,即多個客戶端在同一時間失敗、等待、然後又在同一時間重試,對伺服器造成週期性的衝擊。而「指數退讓」策略會讓每次重試的等待時間以指數級增長(如 2s, 4s, 8s…),能快速拉開重試間隔,有效分散請求壓力。如果再加上「抖動 (Jitter)」,也就是在等待時間上增加一個隨機值,更能避免客戶端之間的重試時間同步,是目前業界公認處理 API 重試最穩健的策略。

Q3: 我可以在自己的 WordPress 網站上實作 API 速率限制嗎?該用什麼工具?

A: 絕對可以,而且非常建議這麼做!特別是當你透過 WordPress REST API 提供自訂端點給外部服務使用時。最推薦的內建工具是 WordPress 的「Transients API」。你可以利用它來為每個 IP 地址或 API Key 建立一個有生命週期的計數器。每次收到請求時,就檢查這個計數器是否超限,若超限就回傳 429 錯誤,否則就將計數器加一。這種方法輕量、高效,且不需要額外安裝資料庫或外掛就能實現。

 
立即諮詢,索取免費1年網站保固