APIを使ったシステムを本番運用していると、ある日突然リクエストが弾かれる――そんな経験はありませんか?原因のほとんどはレートリミット(呼び出し回数・トークン数の上限)への到達です。
しかし実は、APIはレスポンスを返すたびに「残り使用枠」を教えてくれています。その情報は HTTPレスポンスヘッダー に含まれており、PHP の cURL でも簡単に取得できます。この記事では、まずヘッダーを生で確認する方法から始めて、最終的に「使用率90%超えを自動検知するコード」まで順を追って解説します。
1. まず「生のレスポンスヘッダー」を確認してみよう
cURL にはレスポンスヘッダーをボディと一緒に取得するオプションがあります。CURLOPT_HEADER => true を追加するだけです。
まずは Anthropic API を例に、ヘッダーを丸ごと出力してみましょう。
<?php
$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'model' => 'claude-opus-4-6',
'max_tokens' => 100,
'messages' => [['role' => 'user', 'content' => 'hi']],
]),
CURLOPT_HEADER => true, // ← これを追加するだけ
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: ' . $ANTHROPIC_API_KEY,
'anthropic-version: 2023-06-01',
],
]);
$rawResponse = curl_exec($ch);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
// ヘッダー部分とボディ部分を分離
$rawHeaders = substr($rawResponse, 0, $headerSize);
$body = substr($rawResponse, $headerSize);
// ★ まずヘッダーを丸ごと出力して中身を確認する
echo $rawHeaders;
実行すると、以下のようなヘッダーが出力されます(一部抜粋):
HTTP/2 200
content-type: application/json
anthropic-ratelimit-requests-limit: 500
anthropic-ratelimit-requests-remaining: 487
anthropic-ratelimit-requests-reset: 2025-04-13T10:00:00Z
anthropic-ratelimit-tokens-limit: 40000
anthropic-ratelimit-tokens-remaining: 36200
anthropic-ratelimit-tokens-reset: 2025-04-13T10:00:00Z
ここにレートリミット情報がすべて入っています。同様に DeepSeek の場合は:
HTTP/1.1 200 OK
x-ratelimit-limit-requests: 1000
x-ratelimit-remaining-requests: 950
x-ratelimit-limit-tokens: 100000
x-ratelimit-remaining-tokens: 91000
2. 各APIのレートリミットヘッダー一覧
2つのAPIで使われているヘッダー名を整理します。
Anthropic(Claude)
| ヘッダー名 | 意味 |
|---|---|
anthropic-ratelimit-requests-limit |
期間内のリクエスト最大数 |
anthropic-ratelimit-requests-remaining |
残りリクエスト数 |
anthropic-ratelimit-requests-reset |
リセット日時(ISO8601) |
anthropic-ratelimit-tokens-limit |
期間内のトークン最大数 |
anthropic-ratelimit-tokens-remaining |
残りトークン数 |
anthropic-ratelimit-tokens-reset |
リセット日時(ISO8601) |
DeepSeek(OpenAI互換形式)
| ヘッダー名 | 意味 |
|---|---|
x-ratelimit-limit-requests |
期間内のリクエスト最大数 |
x-ratelimit-remaining-requests |
残りリクエスト数 |
x-ratelimit-limit-tokens |
期間内のトークン最大数 |
x-ratelimit-remaining-tokens |
残りトークン数 |
使用率の計算式はどちらも同じです:
使用率 = (limit - remaining) ÷ limit × 100
つまり remaining が limit の10%を切ったら「90%超え」と判断します。
3. ヘッダー文字列をパースする
生のヘッダー文字列は改行区切りのテキストなので、まず「キー => 値」の配列に変換するユーティリティ関数を用意します。
/**
* 生のHTTPレスポンスヘッダー文字列を
* ['ヘッダー名(小文字)' => '値'] の配列に変換する
*/
function parseHeaders(string $rawHeaders): array
{
$headers = [];
foreach (explode("\r\n", $rawHeaders) as $line) {
if (strpos($line, ':') !== false) {
[$key, $value] = explode(':', $line, 2);
$headers[strtolower(trim($key))] = trim($value);
}
}
return $headers;
}
使い方はシンプルです:
$h = parseHeaders($rawHeaders);
// Anthropic の例
$tokLimit = (int)($h['anthropic-ratelimit-tokens-limit'] ?? 0);
$tokRemaining = (int)($h['anthropic-ratelimit-tokens-remaining'] ?? 0);
echo "トークン使用率: " . round(($tokLimit - $tokRemaining) / $tokLimit * 100, 1) . "%";
// → トークン使用率: 9.5%
4. 使用率90%超えを検知する関数
「limit」と「remaining」のペアを受け取り、使用率が90%を超えていたら警告メッセージを返す汎用関数です。
/**
* 使用率が90%以上の項目を警告配列として返す
*
* @param string $apiName API識別名(ログ表示用)
* @param array $limits ['項目名' => [使用済み数, 上限数]] の配列
* @return array 警告メッセージの配列(90%未満なら空)
*/
function checkUsageRate(string $apiName, array $limits): array
{
$warnings = [];
foreach ($limits as $label => [$used, $limit]) {
if ($limit > 0) {
$rate = $used / $limit;
if ($rate >= 0.9) {
$warnings[] = sprintf(
'[%s] %s の使用率が %.1f%% に達しています(%d / %d)',
$apiName, $label, $rate * 100, $used, $limit
);
}
}
}
return $warnings;
}
呼び出し例:
$warnings = checkUsageRate('Claude', [
'リクエスト数' => [480, 500], // 96% → 警告あり
'トークン数' => [3000, 40000], // 7.5% → 警告なし
]);
// $warnings = ['[Claude] リクエスト数 の使用率が 96.0% に達しています(480 / 500)']
5. 完成コード(Claude / DeepSeek 両対応)
ここまでの要素を組み合わせた完成版です。既存の API 呼び出し処理に対して、最小限の変更(CURLOPT_HEADER => true の追加とヘッダー処理の追記)で使用率監視を組み込んでいます。
<?php
// ============================================================
// ユーティリティ関数
// ============================================================
/**
* 生のHTTPレスポンスヘッダー文字列を配列に変換
*/
function parseHeaders(string $rawHeaders): array
{
$headers = [];
foreach (explode("\r\n", $rawHeaders) as $line) {
if (strpos($line, ':') !== false) {
[$key, $value] = explode(':', $line, 2);
$headers[strtolower(trim($key))] = trim($value);
}
}
return $headers;
}
/**
* 使用率90%以上の項目を警告配列として返す
*/
function checkUsageRate(string $apiName, array $limits): array
{
$warnings = [];
foreach ($limits as $label => [$used, $limit]) {
if ($limit > 0) {
$rate = $used / $limit;
if ($rate >= 0.9) {
$warnings[] = sprintf(
'[%s] %s の使用率が %.1f%% に達しています(%d / %d)',
$apiName, $label, $rate * 100, $used, $limit
);
}
}
}
return $warnings;
}
// ============================================================
// API 呼び出し本体
// ============================================================
$usageWarnings = []; // 90%超え警告を収集する配列
if (!$deepseek) {
// ----------------------------------------------------------
// Anthropic(Claude)API 呼び出し
// ----------------------------------------------------------
$payload = json_encode([
'model' => $modelname,
'max_tokens' => $maxTokens,
'system' => $system,
'messages' => $messages,
]);
$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 60,
CURLOPT_HEADER => true, // ← ヘッダーも取得
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: ' . $ANTHROPIC_API_KEY,
'anthropic-version: 2023-06-01',
],
]);
$rawResponse = curl_exec($ch);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
// ヘッダーとボディを分離
$rawHeaders = substr($rawResponse, 0, $headerSize);
$response = substr($rawResponse, $headerSize);
// レートリミットヘッダーをパースして使用率チェック
$h = parseHeaders($rawHeaders);
$reqLimit = (int)($h['anthropic-ratelimit-requests-limit'] ?? 0);
$reqRemaining = (int)($h['anthropic-ratelimit-requests-remaining'] ?? 0);
$tokLimit = (int)($h['anthropic-ratelimit-tokens-limit'] ?? 0);
$tokRemaining = (int)($h['anthropic-ratelimit-tokens-remaining'] ?? 0);
$usageWarnings = checkUsageRate('Claude', [
'リクエスト数' => [$reqLimit - $reqRemaining, $reqLimit],
'トークン数' => [$tokLimit - $tokRemaining, $tokLimit],
]);
} else {
// ----------------------------------------------------------
// DeepSeek API 呼び出し
// ----------------------------------------------------------
$deepseekMessages = [];
$deepseekMessages[] = ['role' => 'system', 'content' => $system];
foreach ($messages as $msg) {
$deepseekMessages[] = [
'role' => $msg['role'] === 'assistant' ? 'assistant' : 'user',
'content' => $msg['content'],
];
}
$payload = json_encode([
'model' => $modelname,
'max_tokens' => $maxTokens,
'messages' => $deepseekMessages,
'response_format' => ['type' => 'json_object'],
'temperature' => 0.7,
]);
$ch = curl_init('https://api.deepseek.com/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 60,
CURLOPT_HEADER => true, // ← ヘッダーも取得
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $DEEPSEEK_API_KEY,
],
]);
$rawResponse = curl_exec($ch);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// ヘッダーとボディを分離
$rawHeaders = substr($rawResponse, 0, $headerSize);
$response = substr($rawResponse, $headerSize);
// レートリミットヘッダーをパースして使用率チェック
$h = parseHeaders($rawHeaders);
$reqLimit = (int)($h['x-ratelimit-limit-requests'] ?? 0);
$reqRemaining = (int)($h['x-ratelimit-remaining-requests'] ?? 0);
$tokLimit = (int)($h['x-ratelimit-limit-tokens'] ?? 0);
$tokRemaining = (int)($h['x-ratelimit-remaining-tokens'] ?? 0);
$usageWarnings = checkUsageRate('DeepSeek', [
'リクエスト数' => [$reqLimit - $reqRemaining, $reqLimit],
'トークン数' => [$tokLimit - $tokRemaining, $tokLimit],
]);
// DeepSeek レスポンスを Anthropic 互換形式に変換
if ($httpCode === 200) {
$deepseekResponse = json_decode($response, true);
$convertedResponse = [
'id' => $deepseekResponse['id'],
'type' => 'message',
'role' => 'assistant',
'content' => [[
'type' => 'text',
'text' => $deepseekResponse['choices'][0]['message']['content'],
]],
'model' => $deepseekResponse['model'],
];
$response = json_encode($convertedResponse);
} else {
http_response_code($httpCode);
$response = json_encode([
'error' => 'Unexpected DeepSeek response format',
'response' => $response,
]);
}
}
// ============================================================
// 使用率90%超えの処理
// ============================================================
if (!empty($usageWarnings)) {
// ① サーバーのエラーログに記録
foreach ($usageWarnings as $warn) {
error_log('[API_RATE_WARNING] ' . $warn);
}
// ② レスポンスJSONに警告フィールドを追加(呼び出し元で参照可能)
$decoded = json_decode($response, true);
if (is_array($decoded)) {
$decoded['_rate_limit_warnings'] = $usageWarnings;
$response = json_encode($decoded);
}
}
6. 警告を受け取った後の活用例
警告はレスポンスの _rate_limit_warnings キーに配列として入っています。フロント側や呼び出し元でこれを拾って活用できます。
// API レスポンスを受け取った後の処理例
$result = json_decode($response, true);
if (!empty($result['_rate_limit_warnings'])) {
foreach ($result['_rate_limit_warnings'] as $warn) {
// 管理者にメール通知する
mail('admin@example.com', '[警告] API使用率90%超え', $warn);
// Slack Webhook に投げる
// notifySlack($warn);
}
}
また、サーバーログ(error_log)にも出力されるため、ログ監視ツールと組み合わせてアラートを飛ばすことも簡単です。
まとめ
今回のポイントを整理します。
- cURL の
CURLOPT_HEADER => trueを追加するだけで、レスポンスヘッダーを丸ごと取得できる - Anthropic は
anthropic-ratelimit-*系のヘッダーで使用枠を通知してくれる - DeepSeek は OpenAI 互換の
x-ratelimit-*形式で通知してくれる - 「使用済み = limit − remaining」で計算し、
使用済み ÷ limit ≧ 0.9なら90%超えと判断できる - 警告はログ出力・レスポンスへの付加・通知送信など、用途に合わせて自由に拡張できる
APIの突然の遮断を防ぐために、ぜひ本番環境でも使用率の監視を取り入れてみてください。


