テックブログ

ポーリングとは?短い間隔でAPIを呼び続ける仕組みと注意点

ポーリングとは?短い間隔でAPIを呼び続ける仕組みと注意点

ポーリングとは、クライアント側から一定間隔でサーバーのAPIへ問い合わせ、最新データや処理状態を取得する通信方式です。既存のHTTP APIとJavaScriptを使えば実装は容易です。だがデータ更新がなくても通信は発生します。間隔を短くしたり利用者が増えると、サーバーやネットワークの負荷が大きくなる。更新頻度と即時性に応じて、適切に通信手段を選択します。ロングポーリング、SSE、WebSocketを使い分けるべきです。

1. ポーリングとは

1-1. 一定間隔でAPIへ問い合わせる仕組み

ポーリングは、ブラウザなどのクライアントが一定間隔でAPIを呼び出し、サーバー上の最新状態を確認する仕組みです。サーバーから自動通知を待つのをやめます。 代わりにクライアントは繰り返し問い合わせを行います。

例えば、ブラウザが5秒ごとに注文状況APIを呼び出し、「処理中」「発送準備中」「発送済み」といった状態を取得する方法がポーリングです。前回取得した値と新しい値を比較し、変更があれば画面を更新します。

実際のポーリング間隔は、必要な即時性、APIの処理負荷、利用者数、データの更新頻度などを考慮して決めます。間隔が短いほど良いわけではありません。 要件に対して過度に短くすると、無駄な通信が増えます。

1-2. 既存のHTTP APIを利用しやすい

ポーリングの大きな長所は、既存のHTTP APIをそのまま利用しやすいことです。この設計では特別な通信プロトコルを追加せず、実装できます。通常のGETリクエストとJSONレスポンスだけで成り立ちます。

ブラウザ側ではfetch()、サーバー側では一般的なREST APIを利用できます。ロードバランサーや認証、アクセスログ、キャッシュ制御なども活用しやすい。既存のWebシステムの仕組みを活用できる点です。

更新頻度が低い管理画面や、小規模なシステムの状態確認であれば、ポーリングは合理的な選択肢です。リアルタイム通信を理由に、必ずSSEやWebSocketへ変更する必要はありません。用途に応じて、ポーリングが合理的な場合もあります。

1-3. 更新がなくても通信が発生する

ポーリングでは、サーバー上のデータが変わっていなくても、設定した間隔ごとに通信が発生します。これはポーリングの分かりやすい特徴であり、同時に注意点でもあります。

クライアントは、次の問い合わせを行うまで更新の有無を知ることができません。そのため、更新がほとんどない状態でもAPIリクエストを送り続けます。反対に、間隔を長くすると通信量は減りますが、画面への反映が遅くなります。

例えば、1時間に数回しか変化しない情報を短い間隔で取得すると、多くのリクエストが「変更なし」という結果になります。更新頻度が低い場合は、間隔を長く設定します。即時配信が必要なときはSSEやWebSocketの検討も含めます。

2. 通常ポーリングの基本

2-1. setIntervalで定期的にAPIを呼び出す

JavaScriptでポーリングを実装する最も単純な方法は、setInterval()を使って一定間隔でAPIを呼び出すことです。コードが短く、初学者でも仕組みを理解しやすい方法です。

setInterval()は、指定した時間が経過するたびに登録した関数を実行します。次の例では、ステータス取得APIを定期的に呼び出し、取得結果を画面へ表示しています。

const POLLING_INTERVAL_MS = 5000;

async function fetchStatus() {
  try {
    const response = await fetch("/api/status");

    if (!response.ok) {
      throw new Error(`HTTPエラー: ${response.status}`);
    }

    const data = await response.json();

    const statusElement = document.querySelector("#status");
    statusElement.textContent = data.status;
  } catch (error) {
    console.error("ステータスの取得に失敗しました", error);
  }
}

// 画面表示直後に1回取得します
fetchStatus();

// 以降は一定間隔で取得します
const intervalId = setInterval(
  fetchStatus,
  POLLING_INTERVAL_MS
);

// ポーリングを停止する場合に呼び出します
function stopPolling() {
  clearInterval(intervalId);
}

コードの役割: /api/statusを定期的に呼び出し、レスポンス内のstatusを画面へ表示します。clearInterval()へタイマーIDを渡すことで、ポーリングを停止できます。

問題になり得る点: setInterval()は、前回の通信が完了したかどうかに関係なく、設定時間が経過すると次の処理を開始します。通信時間がポーリング間隔を超えた場合、問題が生じることがあります。複数のリクエストを同時に実行する可能性があります。

停止方法: 画面遷移時、コンポーネントの破棄時、処理完了時などにstopPolling()を呼び出します。タイマーを停止しないと、不要になった画面からAPIが呼ばれ続ける場合があります。

2-2. ポーリング間隔を決める考え方

ポーリング間隔は、画面に求められる即時性と、通信・サーバー負荷のバランスから決めます。単純に短い間隔を設定すればよいわけではありません。

間隔を短くすると、更新を早く発見できますが、APIの呼び出し回数が増えます。間隔を長くすると負荷は減ります。サーバー上でデータが更新されてから画面へ反映されるまでの待ち時間が長くなります。

間隔を決めるときは、次の要素を確認します。

  • データが実際に更新される頻度
  • 利用者が許容できる表示遅延
  • 同時に利用するユーザー数
  • 1回のAPI処理に必要な時間とリソース
  • 画面が開かれている平均時間
  • バックグラウンド状態でも更新が必要か

例えば、配送状況のように更新が数分単位で発生する情報と、処理進捗のように短い間隔で変わる情報では、適切なポーリング間隔が異なります。固定値を無条件に使わず、実際の要件や負荷試験をもとに調整します。

2-3. 通信遅延による重複実行の問題

setInterval()を使ったポーリングでは、通信が遅れたときにリクエストが重複する可能性があります。前回の処理が完了していなくても、次の実行時刻が来ると新しい処理が開始されるためです。

例えば、ポーリング間隔を5秒に設定していても、APIの応答に8秒かかった場合、最初のリクエストが終わる前に次のリクエストが送信されます。通信遅延が続けば、同時実行されるリクエストが増えていきます。

さらに、後から送ったリクエストが先に完了すると、古いレスポンスによって新しい画面表示が上書きされることもあります。重複を避けるには、実行中フラグを利用するか、通信完了後にsetTimeout()で次回処理を予約する方法が有効です。

実行中フラグを利用する場合は、次のように制御できます。

const POLLING_INTERVAL_MS = 5000;

let isFetching = false;

async function fetchStatus() {
  if (isFetching) {
    console.warn("前回の通信中のため、今回の実行をスキップします");
    return;
  }

  isFetching = true;

  try {
    const response = await fetch("/api/status");

    if (!response.ok) {
      throw new Error(`HTTPエラー: ${response.status}`);
    }

    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("取得に失敗しました", error);
  } finally {
    isFetching = false;
  }
}

const intervalId = setInterval(
  fetchStatus,
  POLLING_INTERVAL_MS
);

function stopPolling() {
  clearInterval(intervalId);
}

コードの役割: API通信中はisFetchingtrueにし、次のタイマー実行が発生しても新しいリクエストを開始しないようにしています。

問題になり得る点: 重複は防げますが、通信中に予定されていた処理はスキップされます。また、停止時にはタイマーだけでなく、実行中の通信をどう扱うかも検討する必要があります。

停止方法: clearInterval()で今後の実行を停止します。実行中のfetch()も中断したい場合は、後述するAbortControllerを利用します。

3. 重複しにくいポーリング実装

3-1. 通信完了後にsetTimeoutで次回を予約する

リクエストの重複を避けたい場合は、通信完了後にsetTimeout()で次回実行を予約する方法が分かりやすく安全です。前回の処理が終わってから次の待機時間を開始します。

この方式では、処理時間が長くなっても複数のリクエストが同時に実行されません。実際の実行間隔は「 APIの処理時間+待機時間 」になる。サーバーの応答が遅いときも自然にリクエスト頻度が下がります。

次の例では、asyncawaitsetTimeout()を組み合わせてポーリングしています。

const POLLING_DELAY_MS = 5000;

let timeoutId = null;
let isStopped = false;
let abortController = null;

async function pollStatus() {
  if (isStopped) {
    return;
  }

  abortController = new AbortController();

  try {
    const response = await fetch("/api/status", {
      signal: abortController.signal
    });

    if (!response.ok) {
      throw new Error(`HTTPエラー: ${response.status}`);
    }

    const data = await response.json();

    const statusElement = document.querySelector("#status");
    statusElement.textContent = data.status;
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("API通信を中断しました");
      return;
    }

    console.error("ステータスの取得に失敗しました", error);
  } finally {
    abortController = null;
  }

  if (!isStopped) {
    timeoutId = setTimeout(
      pollStatus,
      POLLING_DELAY_MS
    );
  }
}

function startPolling() {
  if (!isStopped && timeoutId !== null) {
    return;
  }

  isStopped = false;
  pollStatus();
}

function stopPolling() {
  isStopped = true;

  if (timeoutId !== null) {
    clearTimeout(timeoutId);
    timeoutId = null;
  }

  if (abortController !== null) {
    abortController.abort();
    abortController = null;
  }
}

startPolling();

コードの役割: 1回のAPI通信が完了してから、setTimeout()で次の通信を予約します。AbortControllerを利用し、停止時には実行中のfetch()も中断します。

問題になり得る点: startPolling()を複数回呼び出すと、実装によっては複数のポーリング処理が開始される可能性があります。開始状態と停止状態を明確に管理してください。

停止方法: stopPolling()を呼び出すと、予約中のタイマーを解除し、実行中の通信も中断します。コンポーネントの破棄処理や画面離脱処理から呼び出せるようにしておきます。

3-2. エラー時はバックオフして再試行する

APIエラーが発生した場合は、一定時間待ってから再試行し、エラーが続くほど待機時間を長くするバックオフを検討します。失敗直後には再試行を無制限に行わないでください。再試行を続けると、障害中のサーバーへさらに負荷を与える可能性があります。

代表的な方法が指数バックオフです。連続失敗回数に応じて待機時間を延ばし、成功したら通常の間隔へ戻します。待機時間には上限を設けます。必要に応じてランダムな揺らぎを加えます。複数クライアントの再試行が同時に集中しにくくなります。

次のコードは、バックオフを含むポーリングの実装例です。各時間は説明用の値であり、実際にはサービス要件に合わせて調整してください。

const NORMAL_DELAY_MS = 5000;
const MAX_RETRY_DELAY_MS = 60000;

let failureCount = 0;
let timeoutId = null;
let isStopped = false;

function calculateRetryDelay() {
  const exponentialDelay =
    NORMAL_DELAY_MS * (2 ** failureCount);

  const cappedDelay = Math.min(
    exponentialDelay,
    MAX_RETRY_DELAY_MS
  );

  // 再試行の集中を避けるため、待機時間にランダムな差を加えます
  const jitter = Math.random() * 1000;

  return cappedDelay + jitter;
}

async function pollStatus() {
  if (isStopped) {
    return;
  }

  let nextDelay = NORMAL_DELAY_MS;

  try {
    const response = await fetch("/api/status");

    if (!response.ok) {
      throw new Error(`HTTPエラー: ${response.status}`);
    }

    const data = await response.json();
    console.log("取得成功:", data);

    failureCount = 0;
  } catch (error) {
    failureCount += 1;
    nextDelay = calculateRetryDelay();

    console.error(
      "取得に失敗しました。待機後に再試行します",
      error
    );
  }

  if (!isStopped) {
    timeoutId = setTimeout(
      pollStatus,
      nextDelay
    );
  }
}

function startPolling() {
  isStopped = false;
  pollStatus();
}

function stopPolling() {
  isStopped = true;

  if (timeoutId !== null) {
    clearTimeout(timeoutId);
    timeoutId = null;
  }
}

startPolling();

コードの役割は次のとおりです。成功時には通常間隔で次回実行を予約します。失敗時には連続失敗回数に応じて待機時間を延ばします。

問題になり得る点は、認証エラーとリクエスト内容の誤りです。待っても解決しないエラーを繰り返し再試行するのは適切ではありません。HTTPステータスやエラー内容に応じて、再試行と停止を分けます。これにより適切な対応を取ります。

停止方法: stopPolling()でタイマーを解除します。本番実装では、実行中の通信を止めるためにAbortControllerも組み合わせると安全です。

例えば、一時的なサーバーエラーやネットワークエラーは再試行の対象になります。一方、認証が切れている場合は再ログインを促します。存在しないリソースへの問い合わせは、ポーリングを終了する判断が必要です。

3-3. 画面離脱時にタイマーを停止する

ポーリングは、対象画面が不要になった時点で停止する必要があります。画面遷移やコンポーネント破棄後もタイマーが残ると、API通信が続く可能性があります。利用者には見えない場所で発生します。

通常の複数ページ構成だけでなく、React、Vue、Angularなどのシングルページアプリケーションでも注意が必要です。URLや表示コンポーネントが変わっても、JavaScriptの実行環境はそのまま残ることがあります。これにより挙動が変わる可能性があります。

ブラウザページを離れるときに停止する単純な例は、次のとおりです。

function stopPolling() {
  // clearTimeout、clearInterval、AbortControllerなどを使って
  // アプリケーションのポーリング処理を停止します
  console.log("ポーリングを停止しました");
}

window.addEventListener("pagehide", () => {
  stopPolling();
});

コードの役割は、ページが非表示になるタイミングでポーリング停止処理を呼び出すことです。履歴キャッシュへ移動する可能性がある場合も同様です。

問題になり得る点: pagehideだけで、シングルページアプリケーション内のすべての画面遷移を検出できるわけではありません。使用しているフレームワークの破棄処理も利用してください。

停止方法: タイマーをclearTimeout()またはclearInterval()で解除し、実行中の通信はAbortControllerで中断します。イベントリスナーを追加している場合は、不要になった時点で削除します。

バックグラウンドタブでポーリングを止めたい場合には、別の方法もあります。 Page Visibility APIを使う方法です。

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    stopPolling();
  } else {
    startPolling();
  }
});

ただし、非表示中も更新を継続すべきアプリケーションもあります。画面を再表示したときには、最新状態をすぐ取得します。用途に応じて動作設計を行います。

4. ロングポーリングとは

4-1. サーバー側で更新までレスポンスを保留する

ロングポーリングは、クライアントからリクエストを受けたサーバーが、データの更新が発生するまでレスポンスを返さずに待機する通信方式です。更新が発生するとレスポンスを返し、クライアントは受信後に次のリクエストを開始します。

通常のポーリングでは、更新の有無にかかわらずAPIがすぐにレスポンスを返します。ロングポーリングでは、更新がない間にサーバー側がリクエストを保留します。 その結果、短い間隔での「変更なし」通知を受け取る回数が減ります。

例えば、チャットの新着メッセージを待つ場合、サーバーは新しいメッセージが届くまでレスポンスを保留します。ただし、一定時間更新がなければタイムアウトレスポンスを返します。クライアントが新しいリクエストを開始する設計が一般的です。

4-2. 通常ポーリングとの違い

通常ポーリングとロングポーリングの違いは、サーバーがレスポンスを返すタイミングです。通常ポーリングは各問い合わせで現在の状態をすぐ知らせます。ロングポーリングは更新を待つか、タイムアウト後に返します。

通常ポーリングでは、クライアント側のタイマーが次の問い合わせ時刻を決めます。ロングポーリングでは、サーバーからレスポンスを受け取った直後に、クライアントが次の待機用リクエストを開始します。

ロングポーリングは通常ポーリングより更新を早く受け取れることがありますが、SSEのように1つのレスポンスでイベントを送り続ける仕組みではありません。1回の更新またはタイムアウトごとにレスポンスが終了し、新しいHTTPリクエストが必要です。

4-3. ロングポーリングが向く場面と注意点

ロングポーリングは、通常のHTTPリクエストを利用しながら、通常ポーリングより早く更新を検知したい場面で候補になります。既存環境の制約によってSSEやWebSocketを導入しにくい場合にも利用できます。

一方、サーバーは多数の保留中リクエストを管理する必要があります。同期処理で接続ごとに実行スレッドを占有する構成では、同時接続が増えたときにリソースが不足しやすくなります。

タイムアウト、切断、再接続、重複取得、認証期限なども設計が必要です。ロングポーリングを選ぶ場合は、サーバーが長時間リクエストを効率的に保持できるか、ロードバランサーやプロキシのタイムアウト設定と合っているかを確認します。

5. ポーリングで起きやすい問題

5-1. 短すぎる間隔で負荷が増える

ポーリング間隔を短くすると更新を早く検知できますが、APIへのリクエスト数も増えます。利用者数が増えるほど、サーバーとネットワークへの影響は大きくなります。

1回のAPI処理が軽く見えても、認証確認、データベース照会、JSON生成、ログ記録などが毎回発生します。レスポンス内容に変化がない場合でも、サーバー側の処理が完全になくなるわけではありません。

間隔を決める際は、利用者が必要とする更新速度から逆算します。処理の進捗をある程度早く見せたい場合と、管理画面の一覧を時々更新したい場合では、同じ設定を使う必要はありません。

API側では、差分取得、更新時刻、ETag、条件付きリクエストなどを利用し、不要なデータ転送を減らす方法もあります。ただし、レスポンス本文を削減できても、リクエスト自体の処理は残る点に注意してください。

5-2. 複数タブや複数端末で通信数が増える

同じ利用者が複数のタブを開くと、タブごとにポーリングが開始される場合があります。複数端末から同じ画面を利用すれば、端末ごとに通信が追加されます。

1つのタブでは問題がなくても、実際の利用環境ではダッシュボードを複数開いたままにする利用者がいるかもしれません。ユーザー数だけでなく、1人あたりのタブ数も通信量に影響します。

対策として、非表示タブではポーリングを止める、BroadcastChannelでタブ間に取得結果を共有する、一定時間操作がない画面では間隔を長くするといった方法があります。

ただし、タブ間共有や代表タブの選出を実装すると、状態管理は複雑になります。システム規模が小さく更新頻度も低い場合は、複雑な最適化を追加するより、適切な間隔設定だけで十分なこともあります。

5-3. バックグラウンドでも通信が続く

画面が見えていなくても、JavaScriptのタイマーや通信が完全に停止するとは限りません。ブラウザがバックグラウンドタブのタイマーを遅らせることはありますが、その動作だけに依存すべきではありません。

非表示タブで不要なポーリングが続くと、サーバー通信、端末のバッテリー、モバイル回線のデータ使用量などに影響します。利用者が長時間タブを開いたままにするシステムでは、特に注意が必要です。

バックグラウンド更新が不要なら、visibilitychangeを使って停止し、画面が再表示されたときに最新データを1回取得してから再開します。バックグラウンドでも通知が必要なら、SSE、WebSocket、Web Pushなど別の仕組みを含めて要件を整理します。

6. SSE・WebSocketとの使い分け

6-1. 低頻度更新ならポーリングを検討する

データの更新頻度が低く、多少の表示遅延を許容できる場合は、ポーリングが有力な選択肢です。既存のHTTP APIを利用でき、実装と運用を比較的単純に保てます。

例えば、管理画面の集計状態、定期バッチの結果、在庫一覧など、常に即時更新する必要がない情報では、一定間隔で取得する方法が要件に合うことがあります。

ただし、利用者数が多い場合や画面を長時間開く場合は、低頻度であっても総リクエスト数が増えます。実装前に、想定ユーザー数、画面滞在時間、API処理内容を確認します。

6-2. 一方向の継続配信ならSSEを検討する

サーバーからブラウザへ継続的に更新を届けたい場合は、SSEを検討します。SSEは1本のHTTP接続を維持し、サーバーでイベントが発生したときにテキストデータを送信する仕組みです。

ポーリングとは異なり、ブラウザが一定間隔で更新を問い合わせる必要がありません。新着通知、処理進捗、ライブログ、ダッシュボード更新など、サーバーからクライアントへの一方向配信に向いています。

一方、SSEでは長時間接続、再接続、イベント履歴、認証、プロキシのバッファリングなどを考慮する必要があります。更新頻度が低く即時性が不要なら、ポーリングのほうが運用しやすい場合もあります。

6-3. 双方向かつ高頻度ならWebSocketを検討する

クライアントとサーバーが双方向に頻繁なデータを送り合う場合は、WebSocketを検討します。接続後は、どちらからでもメッセージを送信できます。

チャット、共同編集、オンラインゲーム、リアルタイム操作などでは、クライアントからサーバーへの送信も継続的に発生します。このような用途では、一方向配信が中心のSSEや、問い合わせ型のポーリングよりWebSocketが要件に合う可能性があります。

ただし、WebSocketを導入すれば常に高性能になるわけではありません。再接続、認証、接続状態、メッセージ順序、スケーリング、監視などの実装・運用コストも考慮します。

比較項目通常ポーリングロングポーリングSSEWebSocket
基本的な仕組み一定間隔でAPIを呼ぶ更新までレスポンスを保留するHTTP接続でイベントを継続配信する専用接続で双方向通信する
通信方向クライアントから問い合わせクライアントから問い合わせ主にサーバーからクライアント双方向
レスポンス毎回すぐに完了更新またはタイムアウトで完了接続を維持して複数イベントを送る接続を維持してメッセージを送受信する
更新がない場合定期的に通信が発生リクエストを保留イベント本文は送らないメッセージ本文は送らない
実装の始めやすさ比較的始めやすいサーバー側の待機処理が必要EventSourceと配信処理が必要接続・再接続・状態管理が必要
向いている用途低頻度の状態確認HTTP環境で早い通知が必要な場合通知、進捗、ログチャット、ゲーム、共同編集
主な注意点通信量、重複実行保留接続、タイムアウトプロキシ、接続数、イベント復元再接続、状態管理、スケーリング

どの方式を選ぶ場合でも、リアルタイム性だけで判断しないことが重要です。通信方向、更新頻度、利用者数、既存インフラ、障害時の動作、運用チームの経験まで含めて比較します。

7. まとめ

7-1. ポーリングの長所と短所を整理する

ポーリングは、クライアントが一定間隔でAPIへ問い合わせ、最新状態を取得する通信方式です。既存のHTTP APIやfetch()を利用しやすく、小規模なシステムや低頻度更新では合理的な選択肢です。

一方、更新がない場合でも通信が発生し、間隔や利用者数によって負荷が増加します。setInterval()をそのまま使うと、通信遅延によって複数リクエストが重複する可能性もあります。

安全に実装するには、通信完了後にsetTimeout()で次回を予約し、エラー時にはバックオフし、画面離脱時にはタイマーと通信を停止します。必要な即時性が高い場合は、ロングポーリング、SSE、WebSocketも検討します。

7-2. 導入・実装時のチェックリスト

ポーリングを導入するときは、実装コードだけでなく、利用条件と停止条件を事前に整理することが重要です。特に、更新頻度と利用者数はサーバー負荷に直接影響します。

setInterval()を使う場合は重複実行の可能性を確認し、必要に応じて実行中フラグやsetTimeout()方式へ変更します。エラー時は無制限に即時再試行せず、エラーの種類に応じてバックオフまたは停止します。

実装前と実装後には、次の項目を確認してください。

  • 更新が実際に発生する頻度を把握しているか
  • 利用者が許容できる表示遅延を決めているか
  • 必要以上に短い間隔を設定していないか
  • 前回の通信が終わる前に次の通信が始まらないか
  • エラー時の再試行間隔と上限を決めているか
  • 認証エラーなど再試行すべきでない状態を判定しているか
  • 画面離脱時にタイマーを停止しているか
  • 実行中のfetch()を中断できるか
  • バックグラウンドタブで通信を続ける必要があるか
  • 複数タブや複数端末からの通信を想定しているか
  • サーバー側でAPIの負荷を計測できるか
  • SSEやWebSocketのほうが要件に合わないか確認したか

ポーリングは古いから避けるべき方式ではありません。必要な更新頻度が低いときは、既存HTTP APIを活用できます。方法はシンプルで保守性も高いです。長所と制約を理解し、要件に合う通信方式を選びましょう。

8. 参考リンク

この記事では、次の公式資料および一次情報を参考にしています。