意外と知らなかった『APIのレート制限』、実際の仕組みを覗いてみた

はじめに

「このAPI、1時間に60回までしか呼べません」——ドキュメントでそんな記述を見たことがある人は多いと思います。いわゆる「レート制限」ですが、実際にどういう仕組みで実装されているのか、改めて調べてみたことはありませんでした。

今回は、実際に公開APIにリクエストを送ってレスポンスヘッダーを確認しながら、レート制限がどう実装されているかを調べてみました。

  • ※本記事の検証には、認証なしでもレート制限ヘッダーを確認できる GitHub API を使用しています。

レート制限とは何か

レート制限は、一定時間内にクライアントが送れるリクエストの数を制限する仕組みです。これがないと、悪意のあるアクセス(DoS攻撃的なもの)や、意図しない過剰アクセス(バグで無限ループしてAPIを叩き続けるなど)によって、サーバーの負荷が急激に上がってしまう可能性があります。

多くのAPIでは、制限に達すると429 Too Many Requestsというステータスコードが返ってきます。

実際にレスポンスヘッダーを確認してみた

GitHub APIに対して、認証なしでリクエストを送ってみます。

curl -I https://api.github.com/users/octocat

レスポンスヘッダーの中に、レート制限に関する項目が含まれています。

DevToolsのHeadersタブでGitHub APIのレート制限ヘッダーを確認している画面
x-ratelimit-limit: 60
x-ratelimit-remaining: 59
x-ratelimit-reset: 1790907910
x-ratelimit-used: 1
x-ratelimit-resource: core

それぞれの意味は以下の通りです。

  • x-ratelimit-limit — 一定時間内に送れるリクエストの上限数
  • x-ratelimit-remaining — 現在残っているリクエスト可能数
  • x-ratelimit-reset — 制限がリセットされる時刻(UNIXタイムスタンプ)
  • x-ratelimit-used — すでに使用したリクエスト数

DevToolsからでも同様に確認できます。該当のリクエストを開き、「Headers」タブの「Response Headers」を見れば同じ情報が表示されます。

試しにx-ratelimit-resetの値をJavaScriptのコンソールで人間が読める形に変換してみます。

new Date(1790907910 * 1000).toLocaleString()

これで「あと何分でリセットされるか」が具体的にわかります。単に数字を見るだけでなく、変換して確認する習慣をつけると、残り時間の感覚がつかみやすくなりました。

制限に達するとどうなるか

実際にx-ratelimit-remainingが0になるまでリクエストを送ってみると、きっちり0になった直後のリクエストから403(Forbidden)に切り替わりました。

remainingが0になった直後にForbidden(403)が返ってくる様子

PowerShellで以下のようなスクリプトを使い、1秒おきにリクエストを送って残数を記録しています。

for ($i=1; $i -le 65; $i++) {
    try {
        $r = Invoke-WebRequest -Uri "https://api.github.com/users/octocat" -Method Get
        Write-Host "$i : $($r.StatusCode) remaining=$($r.Headers['x-ratelimit-remaining'])"
    } catch {
        Write-Host "$i : $($_.Exception.Response.StatusCode) <- 制限に達した可能性"
    }
    Start-Sleep -Seconds 1
}

レスポンスの中身まで見ると、以下のようなJSON形式でエラーメッセージが返ってきます。

HTTP/2 403
x-ratelimit-limit: 60
x-ratelimit-remaining: 0
x-ratelimit-reset: 1790908520

{
  "message": "API rate limit exceeded for xxx.xxx.xxx.xxx. (But here's the good news: Authenticated requests get a higher rate limit...)",
  "documentation_url": "https://docs.github.com/rest/overview/rate-limits-for-the-rest-api"
}

GitHub APIの場合、制限超過時は403が返ってきました(API全体で統一的に429を使っているわけではないようです)。レスポンスのJSONにもメッセージが含まれていて、「認証すればもっと上限が増える」という案内まで入っているのが親切だなと感じました。

APIによっては、いつ再試行すればいいかを示すRetry-Afterヘッダーが返ってくることもあります。これが指定されている場合は、その秒数だけ待ってから再試行するのがマナーです。

他のAPIでも同じように確認してみた

GitHub API以外のサービスでも同じようにレスポンスヘッダーを確認してみました。サービスによってヘッダー名や粒度がかなり違うことがわかります。

Qiita API(https://qiita.com/api/v2/items)を認証なしで叩いてみると、以下のようなヘッダーが返ってきました。

Qiita APIのレスポンスヘッダーでRate-Limit/Rate-Remaining/Rate-Resetを確認している画面

GitHubはx-ratelimit-*という名前でしたが、QiitaはRate-*というシンプルな名前になっていました。意味している内容は同じですが、ヘッダー名の付け方は各社バラバラだということが、実際に並べて見るとよくわかります。

サービス 主なヘッダー 気づいたこと
GitHub API x-ratelimit-limit
x-ratelimit-remaining
x-ratelimit-reset
残数とリセット時刻の両方を細かく返してくれます
Qiita API Rate-Limit
Rate-Remaining
Rate-Reset
名前は違いますが、GitHubとほぼ同じ3項目構成です
Stripe API Retry-After(制限超過時のみ) 通常時は残数などを返さず、制限に達した時だけ「何秒待てばいいか」を返す設計です

GitHubとQiitaは「常に現在の残数を可視化する」タイプ、Stripeは「制限に達した時だけ必要な情報を返す」タイプ、という設計思想の違いが見えました。常に残数を返すタイプは、クライアント側が事前に「そろそろ制限に近づいている」と気づけるメリットがありますが、レスポンスヘッダーの情報量は増えます。どちらが優れているというより、APIの利用シーン(頻繁に呼ばれるAPIか、そうでないか)によって向き不向きがありそうです。

ヘッダー名が各社バラバラなのは、業界共通の標準がまだ定まっていないためのようです。先ほど紹介したIETFのRateLimitヘッダー標準化案は、こうしたバラバラな命名を統一しようとする試みの一つです(2026年時点ではまだドラフト段階で、正式な標準仕様としては確定していません)。

レート制限の実装方式、いくつかあるらしい

ヘッダーを見るだけでは実装の内部まではわかりませんが、調べてみると一般的に使われる方式がいくつかあることがわかりました。

方式 考え方 特徴
Fixed Window 一定の時間枠(例:1時間)ごとにカウントをリセット 実装が簡単ですが、時間枠の境界で一時的にリクエストが集中しやすいです
Sliding Window 常に「直近N秒」のリクエスト数を見る 境界の集中が起きにくいですが、実装がやや複雑です
Token Bucket 一定速度でトークンが補充される「バケツ」からトークンを消費 バースト的なリクエストにも柔軟に対応しやすいです
Leaky Bucket リクエストを一定速度で処理し、溢れた分は破棄 出力速度を一定に保ちやすいです

GitHub APIのx-ratelimit-resetが固定の時刻を返してくる挙動を見ると、Fixed Windowに近い方式を採用しているのではないか、と推測できます(公式に実装方式が明言されているわけではないので、あくまで推測です)。

自分でレート制限を実装するなら

もし自分のAPIにレート制限を実装するとしたら、という視点で少し調べてみました。自前で全部実装するよりも、既存のライブラリやミドルウェアを使うのが一般的なようです。

  • Node.js(Express)であればexpress-rate-limitのようなミドルウェアを使う
  • 複数サーバーで動かす場合、カウントの管理にはRedisのようなインメモリDBを使って、サーバー間で制限状態を共有する
  • APIGateway(AWS API GatewayやCloudflareなど)側でレート制限機能が用意されている場合、アプリケーション側で実装しなくても設定だけで済むこともある

「アプリケーションコードで頑張って実装する」よりも、「インフラやミドルウェアの機能に乗る」という選択肢が意外と多いのだなと感じました。

フロントエンド側でどう対応するか

レート制限はサーバー側の実装の話が中心になりがちですが、フロントエンドとしても「制限に近づいていること」をユーザーに伝えたり、無駄なリクエストを減らす工夫ができます。実際に簡単なサンプルを書いてみました。

残数が少なくなったらボタンを無効化する

レスポンスヘッダーからx-ratelimit-remainingを取得し、一定数を下回ったらボタンを無効化してユーザーに伝える、という処理です。

const res = await fetch('https://api.github.com/users/octocat');
const remaining = Number(res.headers.get('x-ratelimit-remaining'));

if (remaining <= 5) {
  submitButton.disabled = true;
  showWarning(`APIの呼び出し回数が残り${remaining}回です。しばらくお待ちください。`);
}

実際に簡単なデモページを作って試してみました。残数(スライダー)を5以下にすると、ボタンが無効化され、警告文が表示されます。

残数が4になった時にボタンが無効化され警告文が表示されているデモ画面

ユーザーが何度もボタンを押して429エラーを連発させてしまう前に、事前に気づかせてあげられるのがポイントです。

リセットまでの時間をカウントダウン表示する

x-ratelimit-resetはUNIXタイムスタンプなので、現在時刻との差分を計算すればカウントダウン表示が作れます。

const resetAt = Number(res.headers.get('x-ratelimit-reset')) * 1000;

function updateCountdown() {
  const diff = Math.max(0, resetAt - Date.now());
  const minutes = Math.floor(diff / 60000);
  const seconds = Math.floor((diff % 60000) / 1000);
  countdownEl.textContent = `制限解除まで ${minutes}分${seconds}秒`;
}

setInterval(updateCountdown, 1000);

こちらも実際にデモページで動かしてみました。ボタンを押すと、リセットまでの残り時間が1秒ごとに減っていきます。

カウントダウンが0分49秒と表示されているデモ画面

「あと何分待てばいいかわからない」という状態より、カウントダウンが見えているだけでユーザーの不安はかなり減る印象です。実際に自分でUIを作ってみると、サーバー側のヘッダー設計がそのままUXに直結していることがよくわかりました。

429エラー時に自動で待ってリトライする

axiosのインターセプターを使うと、429が返ってきたときにRetry-Afterを見て自動的に待ってから再送する処理も書けます。

axios.interceptors.response.use(null, async (error) => {
  if (error.response?.status === 429) {
    const retryAfter = Number(error.response.headers['retry-after'] || 1);
    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
    return axios.request(error.config);
  }
  return Promise.reject(error);
});

ただし、何度も自動リトライを繰り返すと、ユーザーからは「何も起きていないように見える」こともあるので、リトライ中であることを示すローディング表示などと組み合わせるのが良さそうです。

調べる中で気づいた「よくある勘違い」

勘違い1:「レート制限はIPアドレス単位でしか管理できない」

IPアドレス単位の制限はよく見かけますが、APIキーやユーザーID、エンドポイントごとに個別の制限を設けている場合も多いです。GitHub APIも認証状態によって上限が変わる(認証なし60回、認証あり5000回)ように、「誰が」「どこに」アクセスしているかで制限の単位を変えるのが一般的でした。

勘違い2:「429が返ってきたら即リトライすればいい」

Retry-Afterヘッダーを無視して即座にリトライすると、サーバー側の負荷が下がらないままリクエストが繰り返され、状況が悪化することがあります。指定された時間を待つか、指数的にリトライ間隔を伸ばす(Exponential Backoff)などの配慮が必要です。

もっと詳しく知りたい人向け

レート制限の設計について、より体系的に知りたい場合は以下が参考になります。

やってみてわかったこと

レスポンスヘッダーを実際に見てみるまでは、「レート制限って内部でどう管理してるんだろう」とブラックボックスに感じていましたが、ヘッダーの値を追いかけるだけでも、どういう情報をクライアントに伝えているか、どういう設計思想があるかが見えてきました。

自分でAPIを実装する機会があれば、今回調べたヘッダーの命名規則(x-ratelimit-*)に倣って、クライアント側が扱いやすい形でレスポンスを返すようにしたいと思います。

おわりに

普段意識せずに付き合っている「レート制限」も、実際にヘッダーを確認してみると、設計のヒントがたくさん詰まっていました。次に429エラーに遭遇したときは、ただ待つだけでなく、どんなヘッダーが返ってきているかも確認してみようと思います。

この記事を気に入ったら

この記事を書いた人

だいせんせー

だいせんせー

コーヒーが好きですがカフェイン耐性が0。25卒新卒。

この人が書いた記事を見る >>