JavaScript | 非同期処理:実務での非同期制御 - ポーリング

JavaScript JavaScript
スポンサーリンク

ポーリングを一言でいうと

ポーリング(polling)は、
「サーバー側の状態が変わったかどうかを、クライアント側から定期的に“様子見”しに行く仕組み」です。

サーバーから「変わったよ!」と通知してくれるのではなく、
クライアントが「変わった?」「まだ?」「今は?」と、一定間隔で聞きに行きます。

チャットの新着メッセージ、バッチ処理の進捗、長時間かかる処理の完了待ちなど、
「結果がいつ用意されるか分からない」場面でよく使われます。


なぜポーリングが必要になるのか

「すぐには結果が返ってこない処理」があるから

API を叩いたらすぐに結果が返ってくるケースもありますが、
実務では「時間がかかる処理」がよくあります。

例えば、こんなものです。

大きなファイルの変換
レポートの集計
動画のエンコード

こういう処理は、API を叩いた瞬間に終わるわけではありません。

よくあるパターンはこうです。

1回目のリクエストで「ジョブを登録」する
サーバーは「ジョブ ID」だけ返してくる
そのジョブが終わったかどうかは、あとから別の API で確認する

この「あとから別の API で確認する」を、
一定間隔で繰り返すのがポーリングです。

サーバーから「プッシュ通知」できない環境でも使える

WebSocket や Server-Sent Events のように、
サーバーからクライアントへ「変わったよ」と通知できる仕組みもあります。

ただ、環境や制約によってはそれらが使えないことも多いです。

そのときの現実的な選択肢が、
「シンプルな HTTP を使ったポーリング」です。

HTTP の GET を一定間隔で叩くだけなので、
ブラウザでもサーバーでも実装しやすいのが強みです。


一番シンプルなポーリングの実装イメージ

setInterval を使った基本形

まずは「5秒ごとにサーバーの状態を確認する」ポーリングの最小例です。

async function fetchStatus() {
  const res = await fetch("/api/status");
  const data = await res.json();
  console.log("現在のステータス:", data.status);
}

setInterval(fetchStatus, 5000); // 5秒ごとに実行
JavaScript

これだけで、
5秒ごとに /api/status を叩き続けます。

ただし、この形は実務では少し危険です。
リクエストがまだ終わっていないのに、次のリクエストが飛ぶ可能性があるからです。

そこで、
「前のリクエストが終わってから、次の待ち時間を始める」
という書き方にするのが定番です。

「1回終わってから次を待つ」ポーリング

setTimeoutasync/await を組み合わせると、
きれいに書けます。

async function pollStatus() {
  try {
    const res = await fetch("/api/status");
    const data = await res.json();
    console.log("現在のステータス:", data.status);
  } catch (err) {
    console.error("ステータス取得に失敗:", err);
  } finally {
    setTimeout(pollStatus, 5000); // 5秒後にもう一度
  }
}

pollStatus(); // 最初の1回をスタート
JavaScript

この形だと、

リクエスト
→ 結果取得(成功でも失敗でも)
→ 5秒待つ
→ 次のリクエスト

という順番になります。

ここが重要です。
「ポーリングは“永遠に setInterval で回す”のではなく、
“1回終わってから次の予約をする”形にすると、
リクエストが重なりにくく、制御しやすくなります。」


実務っぽい例:ジョブの進捗をポーリングする

1回目のリクエストで「ジョブ ID」をもらう

例えば、レポート生成 API を考えます。

レポート生成開始の API を叩くと、
サーバーはすぐに結果を返さず、
「ジョブ ID」だけ返してくるとします。

async function startReport() {
  const res = await fetch("/api/report/start", {
    method: "POST",
  });
  const data = await res.json();
  return data.jobId; // 例: "job-123"
}
JavaScript

ジョブ ID を使って「終わったかどうか」をポーリング

次に、そのジョブの状態を確認する API を叩き続けます。

async function fetchJobStatus(jobId) {
  const res = await fetch(`/api/report/status?jobId=${jobId}`);
  const data = await res.json();
  return data; // { status: "pending" | "running" | "done" | "error", ... }
}
JavaScript

これをポーリングで回します。

async function pollJob(jobId) {
  try {
    const data = await fetchJobStatus(jobId);
    console.log("ジョブの状態:", data.status);

    if (data.status === "done") {
      console.log("レポート生成完了:", data.resultUrl);
      return; // ポーリング終了
    }

    if (data.status === "error") {
      console.error("レポート生成に失敗:", data.message);
      return; // ポーリング終了
    }

    // まだ pending / running の場合は、少し待ってから再度ポーリング
    setTimeout(() => {
      pollJob(jobId);
    }, 3000);
  } catch (err) {
    console.error("ステータス取得エラー:", err);
    // ネットワークエラーなどの場合も、少し待って再試行するかどうかを決める
    setTimeout(() => {
      pollJob(jobId);
    }, 5000);
  }
}
JavaScript

最後に、全体の流れをつなげます。

async function runReportFlow() {
  const jobId = await startReport();
  console.log("ジョブ開始:", jobId);
  pollJob(jobId);
}

runReportFlow();
JavaScript

これで、

レポート生成開始
→ ジョブ ID を取得
→ 3秒ごとに状態確認
→ done になったら終了

というポーリングの一連の流れが完成します。

ここが重要です。
「ポーリングは“永遠に叩き続ける”のではなく、
“終わり条件(done / error)をきちんと決めて、そこで止める”設計が必須」です。


ポーリングで必ず考えるべきポイント

間隔(インターバル)をどうするか

ポーリングの間隔は、
短すぎるとサーバーに負荷がかかり、
長すぎるとユーザーが「いつまで経っても更新されない」と感じます。

例えば、

チャットの新着メッセージ → 1〜3秒
重いバッチ処理の進捗 → 5〜10秒
そこまで急がない定期更新 → 30秒〜数分

のように、
「どれくらいリアルタイム性が必要か」と「サーバー負荷」のバランスで決めます。

また、
「最初は短く、その後は少しずつ間隔を伸ばす(バックオフ)」
という設計もよくあります。

ここが重要です。
「ポーリングの間隔は“なんとなく”ではなく、
“どれくらいの遅延ならユーザーが許容できるか”と“サーバーの体力”をセットで考える。」

いつ止めるか(終了条件)

ポーリングは、
止める条件を決めないと永遠に走り続けます。

典型的な終了条件は、

サーバーの状態が done / error になったとき
一定回数試しても終わらなかったとき(タイムアウト)
ユーザーが画面を離れたとき

などです。

例えば、最大 20 回までに制限するならこう書けます。

async function pollJob(jobId, attempt = 0) {
  if (attempt >= 20) {
    console.error("タイムアウト: これ以上待ちません");
    return;
  }

  try {
    const data = await fetchJobStatus(jobId);
    console.log("ジョブの状態:", data.status);

    if (data.status === "done" || data.status === "error") {
      console.log("ジョブ終了:", data.status);
      return;
    }

    setTimeout(() => {
      pollJob(jobId, attempt + 1);
    }, 3000);
  } catch (err) {
    console.error("ステータス取得エラー:", err);
    setTimeout(() => {
      pollJob(jobId, attempt + 1);
    }, 5000);
  }
}
JavaScript

こうすると、
最大 20 回(約 1〜2 分など)で諦めることができます。

ユーザーに「待っていること」を伝える

ポーリングは、
「裏で何かを待っている」状態が続きます。

ユーザーから見ると、
何も表示が変わらないと「止まっているのか、処理中なのか」が分かりません。

そこで、

ローディング表示
「レポートを生成中です…」というメッセージ
進捗バー(もしサーバーが進捗を返してくれるなら)

などを組み合わせて、
「今は待ち時間なんだ」と分かるようにするのが大事です。

ポーリングは技術的には「定期的に API を叩く」だけですが、
ユーザー体験としては「待ち時間のデザイン」とセットで考えるべきものです。


ポーリングと他の手法の関係

WebSocket や SSE との違い

WebSocket や Server-Sent Events(SSE)は、
サーバーからクライアントへ「変わったよ」とプッシュ通知できる仕組みです。

それに対してポーリングは、
クライアントから「変わった?」と聞きに行く仕組みです。

理想的には、
リアルタイム性が強く必要なチャットやゲームなどは WebSocket、
そこまでリアルタイムでなくてよいものはポーリング、
という使い分けになります。

ただ、
インフラや制約の都合で WebSocket が使えないことも多く、
その場合はポーリングが現実的な選択肢になります。

ロングポーリング(少しだけ触れておく)

ポーリングの亜種として「ロングポーリング」というものもあります。

普通のポーリングは、
「すぐレスポンスを返す API を、一定間隔で叩く」イメージですが、

ロングポーリングは、
「サーバー側が“何か変化があるまで”レスポンスを返さず、
変化があったタイミングで返す」というスタイルです。

クライアントは、
レスポンスが返ってきたらすぐにまた同じリクエストを投げることで、
ほぼリアルタイムに近い通知を実現できます。

これは少し高度な話なので、
「ポーリングには“普通のポーリング”と“ロングポーリング”がある」
くらいの認識で今は十分です。


初心者として「ポーリング」で本当に押さえてほしいこと

ポーリングは、
「サーバーの状態を一定間隔で確認しに行く」仕組み。

setInterval で雑に回すのではなく、
async/await + setTimeout で「1回終わってから次を予約する」形にすると、
リクエストが重なりにくくて安全。

必ず「いつ止めるか」を決める。
状態が done / error になったら止める、
一定回数で諦める、
画面を離れたら止める、など。

間隔は「リアルタイム性」と「サーバー負荷」のバランスで決める。
チャットなら短め、重いバッチなら長め。

そして何より、
「ポーリング中はユーザーから見ると“待ち時間”」なので、
ローディング表示やメッセージで「今は処理中です」と伝える。

もし今、
「サーバー側で時間のかかる処理があって、終わったら結果を取りたい」
という場面があるなら、
そのフローを「開始 API + 状態確認 API +ポーリング」で一緒に設計してみると、
ポーリングの感覚が一気に自分のものになります。

タイトルとURLをコピーしました