既に運用中のバッチを改修するにあたり、検証のためローカルで実行したところ、外部サービスのAPIのレート制限に引っかかり、バッチが落ちる現象に遭遇しました。
バッチ開発当初は大量にAPIを叩く想定は無かったのでレート制限についての考慮がされておらず、また特定の状況下でバッチを起動したときにAPIが予想より多く叩かれていたこともあり、429 Too Many Requests が発生しました。
これをきっかけにAPIのレート制限の認知が低いのかもと思い、レート制限についてと、どういった考慮をしたら良いかについて書いてみようと思います。
レート制限とは何か?
レート制限(Rate Limit)とは、一定時間あたりに実行できるリクエスト数の上限のことです。
これはAPI提供側が概ね以下を守るため設けています。
- サーバー負荷の安定化
- 悪用防止
- 公平な利用
制限を超えると、一般的に以下のレスポンスが返却されます。
429 Too Many Requests
そして多くの場合、
Retry-After: 10
のようなヘッダーが付き、「何秒後に再試行してよいか」が示されたりします。
レートの制限例
サービスによって制限の単位や厳しさは異なります。
どういったレート制限を設けているかはAPI提供側が公開していますので、2つほど例を示したいと思います。
Slack API
Slackは「ティア(Tier)」制を採用しており、メソッドごとに制限が異なります。
- Tier 2: 1分間に20回以上リクエストしたか
- Tier 3: 1分間に50回以上リクエストしたか
etc
例えば conversations.history メソッドであれば Tier 3 となります。
さらに瞬間的な過負荷を許容する バースト(Burst) という概念があります。
一時的に制限を超えても、その後リクエストを控えればエラーにならない仕組みです。
バーストの許容量は公開していないことが殆どですので、そこまで意識する必要はなく、レート制限のほうを意識したほうが良いと思います。
ChatGPT API
OpenAIが提供するChatGPT APIはさらに細かく、以下の軸で制限がかかります。
- RPM: 1分間に何回リクエストしたか
- RPD: 1日に何回リクエストしたか
- TPM: 1分間に合計で何トークン消費したか
- TPD: 1日に合計で何トークン消費したか
- IPM: 1分あたりの画像数
リクエスト回数だけでなく、トークン消費量も考慮する必要があり、「リクエスト回数は少ないにも関わらず制限に引っかかる」といった場合はトークンが原因のこともあります。
レート制限への考慮と対策
ブラウザ画面からのリクエストをトリガーにAPIを叩くのであれば自然と間隔が空くのでレート制限を強く意識しなくても良いですが、バッチ処理の場合は大量データを取得して処理することがメインとなるのでレート制限については意識する必要が出てきます。
レート制限を意識する必要があるかは、実装するバッチで以下を確認すると良いと思います。
- 並列実行をするか(する予定はあるか)
- APIを1分あたりどのくらい叩きそうか
レート制限への対応
レート制限への対応はいくつかあると思いますが、
- 適度にリクエスト間隔を取る
- 429を適切に処理する
が簡単にできる対策として挙げられるかなと思いましたので、具体的にPHP(Laravel)で例を示したいと思います。
対応策①:適度にリクエスト間隔を取る
foreach ($users as $user) {
$data = $this->callExternalApi($user);
// something to do
usleep(800000); // 0.8秒待つ
}
恐らくもっとも簡単な対策で、例えば Tier 3 のSlack APIを叩くことを想定した場合、APIを叩いてからのレスポンス時間やその後処理を踏まえて 0.8 秒の間隔を取った実装例です。
この間隔の時間は実際に検証して定めて貰えればと思います。
対応策②:429を適切に処理する
use Illuminate\Support\Facades\Http;
public function callExternalApi(array $params, int $maxAttempts = 5)
{
$attempt = 0;
while ($attempt < $maxAttempts) {
$response = Http::post('https://example.com/api', $params);
if ($response->status() !== 429) {
return $response;
}
$attempt++;
$retryAfter = $response->header('Retry-After');
if ($retryAfter) {
sleep((int) $retryAfter);
} else {
// 指数バックオフ
sleep(pow(2, $attempt));
}
// レート制限に引っかかっていることに気づけるようにログを仕込む
\Log::warning('Rate limit hit in callExternalApi', [
'retry_after' => $retryAfter,
'attempt' => $attempt,
]);
}
throw new \RuntimeException('API retry limit exceeded.');
}
API提供側で Retry-After などの再試行待機時間を返している場合はその時間を待機し、返していない場合は指数バックオフで試行ごとに待機時間を増やす実装例です。
指数バックオフとは再試行の間隔を2倍、4倍、8倍と、2の累乗で指数関数的に増やす手法で、短時間の連続リトライによるサーバーへの過負荷を防ぐ目的があります。
Laravelの場合、HTTPクライアントに retry メソッドがあり簡単に再試行を実装する仕組みがありますが、Retry-After を動的に読めないのと、固定間隔リトライになるので注意が必要です。
最後に
レート制限は単なる「制約」ではなく、サービス全体を安定させる「ルール」です。
大量のデータを取得して処理するバッチなどで外部サービスのAPIを叩く場合、レート制限についても意識をして頂けたらと思います。
レート制限については以下について意識するだけでも安定稼働に繋がると思います。
- リクエスト間隔に余裕を持つこと
- バーストを抑えること
- 429を前提に実装すること