2分で読了連携

Webhooksとポーリング:クリック追跡の比較

クリック追跡におけるwebhooksとポーリングを比較:遅延とリクエスト数の計算、リトライ、冪等性、署名、判断表。

Sasha Ehrlich
Compliance · EU residency

数秒以内に人やワークフローが反応する必要があるならwebhooksを、数分の遅延で構わず、公開エンドポイントを運用したくないならポーリングを使ってください。Elidoでは、この選択に落とし穴があります。クリック追跡のwebhooksはまだ存在しません。webhooksが対象とするのはリンク、ドメイン、メンバー、ワークスペースのイベントで、クリックごとの click.created イベントは計画中ですが、発行されていません。現在のクリックデータは、analytics APIのポーリングで取得します。

この記事では、遅延とリクエスト量を計算し、各パターンが保証すること(リトライ、重複、順序、署名)を整理し、最後に判断表を示します。コードは掲載前に、ローカルのモックを相手にNode 22で実行しました。リンクイベントの全カタログはリンクイベントのwebhooksを、クリック用のエンドポイントはリンク分析APIのガイドをお読みください。

webhooksとポーリングを一段落ずつ

ポーリングとは、自分のコードがスケジュールに従って新しいデータを要求することです。cronジョブが GET /v1/workspaces/{workspace_id}/analytics/clicks/recent を呼び出し、新しいものを処理して、スリープします。すべてのリクエストはこちらから開始します。公開URLは不要です。

webhooksは向きが逆になります。イベントが発生すると、Elidoが署名付きの POST をJSONエンベロープとともにあなたのHTTPSエンドポイントへ送り、2xxの応答でその配信は完了になります。到達可能なエンドポイントが必要です。TLSも。重複に耐えるハンドラーも。現在のイベントは link.created、link.updated、link.expired、domain.verified と、webhookのドキュメントに載っているその他のものです。

並べて比較した図。ポーリングでは、cronジョブがElido APIにGETを送り、5分ごとに行を取得する。webhooksでは、ElidoのサーバーがあなたのhttpsエンドポイントにPOSTをプッシュし、エンドポイントが2xxを返す。クリックごとのclick.createdイベントはまだ計画中。 購読中のリンクイベント、有効ステータス、作成日を持つエンドポイントを表示した Elido の Webhook ページ

遅延とコストの計算

ポーリングの遅延は間隔で決まります。間隔がI分なら、イベントは平均でI/2分、最悪でI分待たされ、さらにデータが照会可能になるまでの遅れが加わります。1日あたりのリクエスト数は1,440 / Iです。

  • 1分:1日1,440リクエスト、平均遅延30秒、最悪1分。
  • 5分:288リクエスト、平均2.5分、最悪5分。
  • 15分:96リクエスト、平均7.5分、最悪15分。
  • 60分:24リクエスト、平均30分、最悪60分。

コストはレート制限の圧迫ではなく、無駄なリクエストとして現れます。Freeで公開されているワークスペースの上限は毎秒10リクエストで、1日あたり864,000です(APIのレート制限と冪等性を参照)。1分間隔のポーラー1つが使うのは、1,440 / 864,000 = 0.17%です。上限に達するのは、数百のリンクをリンクごとにポーリングする場合や、他の自動化が同じワークスペースを共有している場合であり、フィード1本ではまず起きません。

重要なのは無駄の数字です。ワークスペースで1日に40件のリンクが変更され、それを1分ごとのポーリングで監視するとします。新しい結果が返るのは、多くても1,440回のうち40回なので、少なくとも1,400回(97%)は空です。webhookの受信側には、同じ40件のイベントに対して40リクエストが届きます。最悪でも、各3回の試行で120です。

webhooksは遅延で勝ります。配信ワーカーが保留中の行を継続的に拾うため、通常は数秒です。一方、ポーリングが無料で得ているものでは負けます。取りこぼした実行は次の実行で修復されます。データはまだAPIにあるからです。取りこぼしたwebhookは、修復する仕組みを作っていなければ修復されません。

配信保証:Elidoが約束すること

以下の挙動は配信コードとヘルプドキュメントに基づくもので、2026年10月1日に確認しました。

リトライ。 配信は3回試行されます。すぐに1回、失敗の5分後に1回、さらにその15分後に1回で、全体で約20分です。2xx以外のステータス、ネットワークエラー、10秒より遅い応答は失敗です。3回目の後、配信はfailedとして記録されます。ダッシュボードのRetryまたは POST .../deliveries/{delivery_id}/retry で、新たな3回分の試行枠として再設定されます。比較として、Stripeはlive modeのイベントを最大3日間リトライします。20分は短いです。受信側の障害がカバーされると思い込まないでください。

冪等性。 配信はat least onceです。X-Webhook-Delivery ヘッダーは1つのエンドポイントへの1回の配信を識別し、自動または手動のリトライをまたいでも変わらないため、重複排除のキーになります。

順序。 順序の保証はありません。早いイベントのリトライが、後のイベントより遅れて届くことがあります。何かを上書きする前に、data.updated_at を保存済みの値と比較してください。

署名。 署名付きの配信にはすべて、X-Webhook-Signature(別名 X-Elido-Signature)が付き、値は v1= に {timestamp}.{raw_body} のHMAC-SHA256の16進数が続いたものです。タイムスタンプは X-Webhook-Timestamp にあります。タイムスタンプは試行のたびに新しくなるので、正当なリトライが古いと判定されることはありません。シークレットのローテーション中は、X-Elido-Signature-Previous が旧シークレットによる署名を7日間運びます。webhook署名の検証では、Python、Go、不一致のデバッグを扱っています。

トランスポートと一時停止。 新しいイベント用エンドポイントはhttpsを使う必要があり、平文のhttpはlocalhostと127.0.0.1でのみ受け付けられます。is_active をfalseにするとエンドポイントが一時停止します。9月22日の修正以降は、キューに入っている配信も止まります。

Nodeのwebhook受信側

この受信側は、生のバイト列に対して署名を検証し、5分の許容時間を強制し、ローテーション中は旧シークレットのヘッダーも受け付け、配信IDで重複を排除します。5分の許容時間はElidoではなくあなたの選択で、Stripeのデフォルトに合わせています。ローカルの署名ツールで実行したところ、有効なリクエストは200を返し、同じ配信IDのリプレイは「duplicate」を返し、誤ったシークレットと400秒前のタイムスタンプはどちらも401を返しました。

import http from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.WEBHOOK_SECRET; // the whsec_... value
const seen = new Set(); // use a unique index in a real database

export function verify(secret, headers, rawBody, toleranceSec = 300) {
  const ts = headers["x-webhook-timestamp"] ?? "";
  if (!/^\d+$/.test(ts)) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false;
  const mac = createHmac("sha256", secret).update(`${ts}.`).update(rawBody);
  const expected = Buffer.from("v1=" + mac.digest("hex"));
  return ["x-elido-signature", "x-elido-signature-previous"].some((name) => {
    const got = Buffer.from(headers[name] ?? "");
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}

http
  .createServer((req, res) => {
    const chunks = [];
    req.on("data", (c) => chunks.push(c));
    req.on("end", () => {
      const raw = Buffer.concat(chunks);
      if (req.method !== "POST" || !verify(SECRET, req.headers, raw)) {
        return res.writeHead(401).end();
      }
      const id = req.headers["x-webhook-delivery"];
      if (seen.has(id)) return res.writeHead(200).end("duplicate");
      seen.add(id);
      const event = JSON.parse(raw.toString("utf8"));
      queueMicrotask(() => console.log("handled", event.type)); // do the work after replying
      res.writeHead(200).end("ok");
    });
  })
  .listen(8787);

timingSafeEqual の前の長さチェックは重要です。Nodeはサイズの異なるバッファに対して例外を投げます。先に応答を返し、処理は後で行ってください。10秒の打ち切りは、遅いが成功しているハンドラーが重複になる境目です。

Nodeのクリックポーラー

click.created が提供されるまでは、これがクリックの経路です。clicks/recent は新しい順に返し、limit は最大500(デフォルト100)まで受け付け、ページがいっぱいで返ってきたときは常に next_cursor を返します。各実行は先頭から読み、すでに処理した最新のクリックに達したところで止まります。2行ずつページングするモックで実行したところ、1回目は5クリックを処理し、2回目は新しい2クリックだけを処理し、3回目は何も処理しませんでした。

const BASE = "https://api.elido.app";
const WS = process.env.ELIDO_WORKSPACE_ID;
const KEY = process.env.ELIDO_API_KEY;

// Newest click already handled; clicks/recent is newest first.
let lastSeen = null;
const keyOf = (c) => `${c.ts}|${c.link_id}`;

export async function pollOnce(handle) {
  let cursor = null;
  let newest = null;
  for (;;) {
    const url = new URL(`${BASE}/v1/workspaces/${WS}/analytics/clicks/recent`);
    url.searchParams.set("limit", "500");
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${KEY}` },
      signal: AbortSignal.timeout(10_000),
    });
    if (!res.ok) throw new Error(`clicks/recent returned ${res.status}`);
    const { items, next_cursor } = await res.json();
    for (const click of items) {
      if (keyOf(click) === lastSeen) {
        if (newest) lastSeen = newest;
        return;
      }
      newest ??= keyOf(click);
      await handle(click);
    }
    if (!next_cursor) break;
    cursor = next_cursor;
  }
  if (newest) lastSeen = newest;
}

// setInterval(() => pollOnce(handleClick).catch(console.error), 60_000);

2つの制約を率直に述べます。カーソルはタイムスタンプとリンクIDで順序付けられるため、同じリンクへの2つのクリックが同じ秒にあると、キーを共有します。まれなクリックの取りこぼしや重複も許容できない場合は、handle を行全体に対して冪等にしてください。また lastSeen はメモリ上にあるので、再起動をまたいで永続化してください。そうしないと、再起動で最近の履歴が再生されます。

どちらもまだ作る準備ができていませんか? それで構いません。まずダッシュボードを開いてダッシュボードの分析を使い、スケジュールで必要な数字が分かってからAPIを追加してください。

判断表

状況現時点で使うもの理由
リンクやドメインの変更に数秒で反応するlink.*、domain.* のwebhookイベントはプッシュされる。クリックイベントは不要
自社プロダクト内のライブクリックカウンターclicks/recent の1分ポーリングクリックのwebhookはまだない。1日1,440リクエストは上限に十分収まる
クリックごとのCRMエンリッチメントイベントフォワーダーか短い間隔のポーリング平均30秒のポーリング遅延は通常許容できる
週次または月次のレポートsummary と timeseries をポーリング過去のデータはプッシュの恩恵を受けない
公開エンドポイントが使えないポーリングwebhooksにはインターネットから到達可能なhttps URLが必要
リンクイベントを失えないwebhookと夜間の照合リトライは約20分をカバーし、ポーリングが残りをカバーする

サンプリングされた click.created ストリームが提供されたら、ライブカウンターとCRMの行はwebhookの列に移り、ポーリングは照合のパスになります。それまでは、Mixpanelのリンククリックイベントのようなイベントフォワーダーが、クリックデータのプッシュ型の選択肢です。

ハイブリッドパターン

失うわけにいかないものには、両方を動かしてください。webhookが高速な経路です。スケジュールされたポーリングが合計値を比較して隙間を見つけます。障害中に3回の試行を使い切った配信や、順序が入れ替わって届いたイベントなどです。

summary と timeseries のレポートは、日付範囲の合計と日別のバケットを1回の呼び出しで返すため、夜間の比較が安く済みます。ボットフィルターは、クリックが記録される前にクローラーやモニターを除外するので、合計値は生のサーバーログとは一致しません。何が除外されるかは分析ガイドで説明しています。残りはセキュリティチェックリストでカバーしています。独自のwebhooksを設計する場合は、Standard Webhooksの仕様が役立つ参考資料です。

ElidoダッシュボードのAPIドキュメントページ。APIリファレンス、SDK、OpenAPI仕様、APIキー管理へのリンクを表示

DEMOワークスペースのAPIドキュメント画面。サンプルデータであり、お客様のアカウントではありません。

どちらのパターンにも使える、公開前チェックリスト

受信側もポーラーも、公開する前に6つのチェックを確認してください。どちらのパターンでも、本番での障害の大半はどれか1つを飛ばしたことから起きます。

  • Webhook:10秒以内に2xxを返し、時間のかかる処理は返信した後に行う。
  • Webhook:JSONをパースする前に、生のボディに対して署名を検証する。
  • Webhook:X-Webhook-Delivery をユニークインデックス付きで保存し、リトライで二重カウントされないようにする。
  • ポーラー:カーソルまたは最後に見たキーを、プロセスのメモリ外に永続化する。
  • ポーラー:429や5xxでは、通常の間隔でリトライせずにバックオフする。
  • 両方:通常の静かな時間帯より長くイベントや新しいクリックが届かない場合はアラートを出す。壊れた連携は、沈黙という形で現れるからです。Hookdeckのwebhook vs APIポーリングのガイドのような独立した解説も、同じ結論に至っています。速さにはプッシュ、突き合わせにはポーリングです。

ブログの関連記事

よくある質問

webhooksとポーリングの違いは何ですか?

ポーリングでは、自分のコードがスケジュールに従ってAPIに新しいデータを問い合わせます。webhooksでは、何かが起きたときにサーバーがあなたのエンドポイントへHTTP POSTを送ります。ポーリングはリクエスト数のコストがかかり、最大で1間隔分の遅延が加わります。webhooksは常時稼働の公開エンドポイントと、リトライや重複を処理する作業がコストになります。

Elidoにクリック追跡のwebhooksはありますか?

まだありません。webhooksが対象とするのは、link.createdやlink.expiredなど、リンク、ドメイン、メンバー、ワークスペースのイベントです。click.createdイベントは計画中ですが、現時点では何も発行されません。クリック単位のデータが必要な場合は、analytics APIのclicks/recentレポートをカーソル付きでポーリングするか、イベントフォワーダーを使ってください。

analytics APIはどのくらいの頻度でポーリングすべきですか?

そのデータが支える判断に間隔を合わせます。ほぼリアルタイムのカウンターなら1分、ダッシュボードなら5〜15分、レポートなら1時間または1日が適しています。1分間隔のポーリングは1日1,440リクエスト、5分間隔なら288リクエストになるので、それより短くする価値はまずありません。

webhooksは必ず1回だけ届きますか?

いいえ。webhookの配信は少なくとも1回(at least once)なので、タイムアウトや手動の再送の後には、受信側に重複が届くことがあります。Elidoはリトライをまたいで同じX-Webhook-Deliveryの値を保つので、そのIDを保存して、処理済みのものはスキップしてください。

webhookのエンドポイントが停止していたらどうなりますか?

Elidoは3回試行します。すぐに1回、失敗の5分後に1回、さらにその15分後に1回です。3回とも失敗した場合、誰かがRetryを押すかretryエンドポイントを呼び出すまで、その配信はfailedとして記録されます。約20分を超える障害には、照合(reconciliation)ジョブが必要です。

webhooksとポーリングを組み合わせることはできますか?

はい。重要なものには組み合わせるべきです。webhookを高速な経路として使い、スケジュールされたポーリングを、受信側が取りこぼしたイベントを見つける安全網として使います。ポーリングは隙間を埋めるだけなので、1時間ごとや夜間に実行すれば十分です。

Elidoを試す

URLを貼り付けて短縮リンクを取得

登録不要。リンクは30日間有効。永久に保存するには登録してください。

登録不要。無料登録すると月 50 件のリンクを作成できます。

Elidoを試す

EUホスティングのURL短縮サービス。カスタムドメイン、詳細な分析、オープンAPI付き。無料プラン - クレジットカード不要。

タグ
webhooks vs polling
click tracking webhooks
click tracking api
url shortener webhooks
webhook retries idempotency
link analytics api

続きを読む