ロングポーリング

サーバーがデータの準備ができるまでレスポンスを保留し、擬似的なリアルタイム通信を実現する手法

Webリアルタイム

ロングポーリングとは

ロングポーリング (Long Polling) は、クライアントがサーバーにリクエストを送り、サーバーがデータの準備ができるまでレスポンスを保留する手法である。通常のポーリング (短いポーリング) が定期的にリクエストを繰り返すのに対し、ロングポーリングはサーバー側でイベントが発生するまで接続を維持する。

通常のポーリングとの比較

通常のポーリングはデータの有無にかかわらず即座にレスポンスを返すため、データがない間も無駄なリクエストが繰り返される。ロングポーリングはサーバーがデータの到着まで接続を保持し、データが来た時点でレスポンスを返す。これにより無駄なリクエストが減り、リアルタイム性も向上する。

短いポーリング:
  クライアント → リクエスト → サーバー (データなし → 即座に空レスポンス)
  クライアント → リクエスト → サーバー (データなし → 即座に空レスポンス)
  クライアント → リクエスト → サーバー (データあり → レスポンス)

ロングポーリング:
  クライアント → リクエスト → サーバー (データが来るまで保留...)
                                        (データあり → レスポンス)
  クライアント → 即座に再リクエスト → サーバー (保留...)

リアルタイム通信手法の比較

選定の軸は、通信の方向・レイテンシ・実装の複雑さ・サーバーが抱える保留接続の量である。ロングポーリングは「短いポーリングよりレイテンシが小さく、WebSocket より実装が軽い」中間解として位置付けられる。

手法方向レイテンシ複雑さサーバー負荷
短いポーリングクライアント → サーバー高い (間隔依存)低い高い (空リクエスト多数)
ロングポーリング擬似双方向中程度中程度中程度
SSEサーバー → クライアント低い低い低い
WebSocket双方向最低高い低い

クライアント側の実装

クライアント側の要点は 3 つある。レスポンスを受け取ったら間を置かず次のリクエストを出すこと、タイムアウトと通信エラーを区別すること、エラーのときだけ待ってから再接続することである。区別せず一律で待つと通知が遅れ、一律で即再接続するとサーバー障害時にリクエストが集中する。

async function longPoll(url: string) {
  while (true) {
    try {
      const response = await fetch(url, {
        signal: AbortSignal.timeout(30000), // 30秒タイムアウト
      });
      const data = await response.json();
      handleData(data);
    } catch (error) {
      if (error instanceof DOMException && error.name === 'TimeoutError') {
        continue; // タイムアウト → 再接続
      }
      await new Promise(r => setTimeout(r, 3000)); // エラー → 3秒待って再接続
    }
  }
}

サーバー側の実装

サーバー側は、保留を打ち切る時刻をクライアントのタイムアウトより短くするのが定石である。逆にするとクライアントが先に接続を切り、サーバーは戻す先を失ったまま待ち続ける。さらに、打ち切りとデータ到着が競合したときにレスポンスを二重送信しないガードが必要になる。

app.get('/api/notifications', async (req, res) => {
  let sent = false;
  const send = (payload: unknown) => {
    if (sent) return; // 二重送信を防ぐ
    sent = true;
    res.json(payload);
  };

  // クライアント側の 30 秒より先に打ち切る
  const timeout = setTimeout(() => send({ data: null }), 25000);

  try {
    const data = await waitForNewData(req.query.lastId); // データが来るまで待機
    send({ data });
  } finally {
    clearTimeout(timeout);
  }
});

クライアントが前回受け取った位置 (上の例では lastId) を毎回送っているのも必須の作りである。レスポンスを返してから次のリクエストが届くまでにはわずかな隙間があり、その間に起きたイベントは位置情報が無いと消える。

Lambda での制約

Lambda 関数自体は最大 900 秒 (15 分) 動かせる。ロングポーリングで先に壁になるのは前段の API Gateway 側で、REST API の統合タイムアウトは既定 29 秒、設定できる範囲は 50 ミリ秒から 29,000 ミリ秒である。2026 年 8 月時点ではサービスクォータの引き上げ申請によって 29 秒を超える値も設定できるが、対象はリージョン別 REST API とプライベート REST API に限られ、アカウントのスロットリング上限の引き下げを求められる場合がある。HTTP API の統合タイムアウトは 30 秒が上限で、こちらは引き上げできない。「Lambda が 29 秒しか待てない」と覚えると障害の切り分けを誤るので、制約がどちら側にあるかを分けて把握しておく。

保留している時間がそのまま関数の実行時間になる点も見落としやすい。Lambda の課金はコードが動き始めてから終わる (または停止する) までの時間を 1 ミリ秒単位に切り上げて計算するため、待っているだけの時間にも料金が発生する。常時接続に近い使い方をしたいなら、次の選択肢を検討する。

  • API Gateway WebSocket API: 双方向通信ができる。ただし 1 接続の維持は最大 2 時間、無通信が続くと 10 分で切断されるため、クライアント側の再接続実装が前提になる。
  • AppSync Subscriptions: GraphQL の購読としてリアルタイム通知を実現する。接続の管理をサービス側に寄せられる。
  • SQS: クライアントから直接ではなくバックエンドがキューを読む構成にする。SQS 自体が受信待機時間として最大 20 秒のロングポーリングを備えており (待機時間 0 秒に設定したときだけ短いポーリングになる)、AWS も多くの場合 20 秒を推奨している。

使うべきケース

  • WebSocket や SSE が使えない環境 (プロキシ制限、古いブラウザ)
  • シンプルな通知機能で、WebSocket ほどの双方向通信が不要
  • 既存の HTTP インフラをそのまま使いたい場合

ロングポーリングの理解を深めるには関連書籍が参考になる。

この記事は役に立ちましたか?

関連用語

関連する記事