Webhook2026年8月3日8 分で読めます

Shopify に Webhook を削除された。すべては 5 秒で決まる。

Shopify の応答期限は 5 秒です。4 時間で 8 回リトライした後、サブスクリプションを削除します。エラーは出ず、注文が届かなくなるだけです。

同じ 4 つの処理を積み上げ、固定の壁と比べる平常時1.2sERP が遅い日7.1s5sどの配信も壁を越えるため、8 回のリトライは 4 時間以内に尽きるコードは何も変えていない。それでもサブスクリプションは木曜日までに削除される。
このハンドラーに間違いは何もない。ただ、Shopify が待つのをやめる前に終わらなければならない。

症状は異様なほど静かです。注文がアプリに表示されなくなります。ログにエラーはありません。ログまで何も届いていないからです。Shopify 管理画面にも、失敗した配信はありません。配信を表示するためのサブスクリプションが、もう残っていないからです。Shopify がそれを削除したのです。警告メールが誰かの読むメールボックスに届いていない限り、最初の兆候は、直近 2 日分の注文が見当たらないのはなぜかという、マーチャントからの問い合わせです。

Shopify の Webhook のルールはほかの多くより厳しく、その厳しさは 1 つの数字に集約されています。

TL;DR

  • 5 秒。配信 1 件あたりの応答期限は、これだけです
  • Shopify は 4 時間で 8 回リトライし、8 回連続で失敗すると、Admin API で作成したサブスクリプションは削除されます(shopify.app.toml で宣言したものは削除されません)
  • アプリの側から見ると、削除は音もなく起きます。サブスクリプションが残っていないので、トラフィックもエラーもありません
  • 警告と削除のメールは、Partner アカウントに登録した緊急連絡用の開発者メールアドレスに届きます。たいていは、誰も見ていないアドレスです
  • Shopify は再送しないので、復旧にはサブスクリプションの再作成と、Admin API での補完の両方が必要です

5 秒は実際どこに消えるのか

GitHub の応答期限は 10 秒で、Stripe は数字をまったく公表していません。ほかのプロバイダーの値は Webhook プロバイダー別リファレンスにあります。Shopify はレスポンスを 5 秒待ち、それより遅いものはすべて失敗した配信として扱います。

しかも、5 秒が最短というわけでもありません。Slack は 3 秒で、ストアとの連携しか経験のない人がつまずくほど厳格です。Twilio は、これらの数字がいかに恣意的かを示す例外です。タイムアウトのつまみを渡し、自分で設定できるようにしています。ただ、それを回す人はほとんどいません。

5 秒は十分に聞こえますが、典型的なハンドラーの中で実際に起きていることを書き出すと、印象は変わります。

ステップ通常調子の悪い日
コールドスタート(サーバーレス)200ms2.5 秒
HMAC 検証2ms2ms
ドメインからショップを検索30ms400ms
注文と明細行の書き込み80ms1.2 秒
ERP/3PL への同期600msタイムアウト
確認メールの送信250ms3 秒
同じ 4 つの処理を積み上げ、固定の壁と比べる平常時1.2sERP が遅い日7.1s5sどの配信も壁を越えるため、8 回のリトライは 4 時間以内に尽きるコードは何も変えていない。それでもサブスクリプションは木曜日までに削除される。

順調なときは 1.2 秒で、まったく健全に見えます。ところが ERP の調子が悪い朝が来ると、すべての配信が一斉に 5 秒を超え、4 時間のうちに 8 回のリトライを使い切ります。コードは何も変わっていません。コードには何の間違いもありません。それでも、木曜にはサブスクリプションが消えています。

Shopify には、事態を悪化させる固有の要因が 2 つあります。

タイムセールは失敗を連動させます。Webhook は注文の数に比例して届くので、orders/create が最も必要な瞬間は、データベースが最も忙しく、応答時間が最も悪い瞬間でもあります。失敗が起きるタイミングは、失敗を許容できるタイミングとちょうど逆です。

一括操作は、Webhook を一気に増やします。マーチャントがアプリや CSV インポートで 5,000 件の商品を編集すると、短時間に 5,000 件の products/update の Webhook が発生します。エンドポイントの処理速度は、自分のデータベースのコネクションプールで頭打ちになり、5 秒を超えた配信はすべて失敗として数えられます。

削除は実際にどう見えるか

4 時間の間に 8 回失敗すると、Shopify はそのイベントのリトライをやめます。また、8 回連続で失敗した時点で、Admin API で作成したサブスクリプションは削除され、アプリの緊急連絡用の開発者連絡先に警告メールが送られます。shopify.app.toml で宣言したアプリ固有のサブスクリプションは、この方法では削除されません。

Shopify はメールで知らせてはくれます。失敗の繰り返しを検出したときの警告と、サブスクリプションを削除したときの通知です。どちらも、Partner アカウントの緊急連絡用の開発者メールアドレスに届きます。実際には、そのアドレスはオンボーディングのときに一度設定されたきりで、すでに退職した人の個人アドレスになっていることもよくあります。チームがこの件で不意を突かれる理由として、これが最も多いものです。

今すぐ、そのアドレスが何になっているかを確認してください。30 秒で済み、この記事の中で最も価値のある行動です。

削除後は、webhookSubscriptions はそのトピックについて何も返しません。失敗するものが何も残っていないので、配信も失敗もありません。サブスクリプションがなかった間に発生したイベントも、どこにもキューイングされていません。再作成しても、Shopify は再送しません。

この最後の点が、代償を大きくしています。Stripe のエンドポイントが無効化された場合は、イベントが 30 日間 API に残っています。しかし Shopify のサブスクリプションが削除された場合、その間の Webhook はそもそも送信されていません。復旧は再送ではなく、Admin API との照合作業になります。

セルフホストのプラットフォームも甘くはありません。WooCommerce も、5 回を超えて連続で失敗すると Webhook を無効にします。

5 秒に収める

確実に動く形は、検証し、永続化し、応答し、本当の処理は別の場所で行う、というものだけです。タイムアウトチェッカーを使えば、今のハンドラーが 5 秒をどれだけ超えているかがわかります。遅いステップがモデルの呼び出しなら、Webhook ハンドラーが LLM を呼ぶときに、同じ形をプラットフォームごとにまとめています。

export async function POST(req: Request) {
  const raw = await req.text();               // raw body, required for HMAC
  if (!verifyShopifyHmac(raw, req.headers.get("x-shopify-hmac-sha256"))) {
    return new Response("unauthorized", { status: 401 });
  }

  await queue.publish({                       // durable write, ~10ms
    topic: req.headers.get("x-shopify-topic"),
    shop: req.headers.get("x-shopify-shop-domain"),
    webhookId: req.headers.get("x-shopify-webhook-id"),
    body: raw,
  });

  return new Response(null, { status: 200 }); // well inside 5s
}

正確に押さえておきたい細部が 4 つあります。

パース済みの JSON ではなく、生のリクエストボディを読みます。Shopify の HMAC は送信されたバイト列そのものに対して計算されるので、パースして再シリアライズするミドルウェアがあると、キーの順序や空白が変わり、署名検証はすべて失敗します。Next.js の App Router では、パースする前に await req.text() を呼びます。プロバイダーごとの詳細は Stripe、GitHub、Shopify の Webhook 署名を検証するを参照してください。

検証はキューに入れる前に行い、後回しにしません。検証にかかるのは約 2ms で、攻撃者にキューを埋め尽くされるのを防ぐ関門です。安い関門を先に置きます。

X-Shopify-Webhook-Id で重複排除します。この値は同じイベントのリトライの間ずっと変わらないので、冪等キーとして正しい選択です。Shopify はリトライで同じイベントを再び届けるので、コンシューマーは冪等でなければなりません。パターンは 同じ Webhook を 2 回受け取ったときにあります。

コールドスタートに注意します。サーバーレスでは、コードが動き出す前に、コールドスタートが持ち時間の半分を食うことがあります。Webhook ルートの依存グラフは小さく保ちます。バイト列をキューに入れるだけのハンドラーに ORM や管理用 SDK を丸ごとインポートするのは、実際にかかる、しかも避けられるコストです。

削除された後の復旧

まず、サブスクリプションを再作成します。

mutation {
  webhookSubscriptionCreate(
    topic: ORDERS_CREATE
    webhookSubscription: {
      callbackUrl: "https://your-app.example.com/webhooks/shopify"
      format: JSON
    }
  ) {
    webhookSubscription { id }
    userErrors { field message }
  }
}

次に、Admin API を使って欠けた期間を照合します。何も再送されないので、障害の期間について対象のオブジェクトを直接クエリし、Webhook のコンシューマーと同じコードパスで処理します。

{
  orders(first: 250, query: "updated_at:>2026-07-28T00:00:00Z") {
    edges { node { id name updatedAt displayFinancialStatus } }
  }
}

その結果を、冪等なコンシューマーに通します。すでに処理済みのものには何も起きず、取りこぼしたものだけが反映されます。コンシューマーが本当に冪等かどうかがわかるのも、このときです。

続いて、どのトピックを失ったかを監査します。現在のサブスクリプションを一覧にし、アプリが想定しているものとの差分を取ります。削除はサブスクリプション単位なので、products/update は残っているのに orders/create だけが失われる、ということはごく普通に起こります。そのため、症状はさらにわかりにくくなります。

最後に、緊急連絡用の開発者メールアドレスを直し、その上で独自の監視を加えます。「営業時間中に 6 時間 orders/create を受信していない」というアラートがあれば、初日にこの問題を検出できていたはずです。

AnyHook の位置づけ

5 秒の応答期限は伝送の問題で、伝送こそ中継サービスが置き換えるものです。

Shopify のサブスクリプションを in.anyhook.net/you/shopify に向けます。AnyHook は、ERP が健全でも炎上中でも、約 1 秒で 200 を返します。Shopify の時計はオリジンではなくその応答に対して進むので、オリジンが遅くても、8 回のリトライのカウントダウンは始まりません。唯一の例外は、AnyHook がイベントをまったく保存できなかった場合です。そのときは 200 ではなく 503 を返し、Shopify 自身のリトライに引き継がれます。

  • オリジンの遅さは、Shopify の時計から切り離されます。AnyHook はサーバーへの配信を、5 分、30 分、3 時間、18 時間の間隔で、以降は 1 日ごとにリトライします。試行回数はプランによって 3〜10 回です。Free プランでは最後の試行が 35 分の時点なので、それより長い障害は、保存されたログと再送でカバーします
  • すべてのイベントは配信を試みる前に永続化され、保存時に暗号化されます。そのため欠けた期間は、Admin API から再構成するのではなく、AnyHook のログから再送できます
  • 連続失敗のアラートは、1 回、5 回、20 回の時点で、自分で管理しているアドレスに届きます。20 回で自動的に一時停止します
  • ワンクリックの再送で、サーバーがダウンしていた期間をまとめて再送できます。失敗したイベントの再送は月間クォータにカウントされません。ただし 1 日の再送上限があり、コンピュート枠を消費します

多くのショップで使われる公開アプリでも、必要な AnyHook の App(Webhook の送信元ごとに作る受信設定)は 1 つだけで、ショップごとに作る必要はありません。すべてのショップのサブスクリプションを、同じ受信 URL に向けます。Shopify はアプリの Webhook をそのアプリのクライアントシークレットで署名し、それはどのショップでも同じです。そのため、AnyHook はそのシークレットで検証します。どのショップが送ったイベントかは、引き続き X-Shopify-Shop-Domain ヘッダーでハンドラーにわかります。

Shopify の 5 秒ルールは、Shopify が通信するエンドポイントについての決まりです。そこに約 1 秒で応答するエンドポイントを置けば、このルールがビジネスロジックを縛ることはなくなります。

まとめ

Shopify の 5 秒は努力目標ではありません。何度も守れなかったときの罰は、ダッシュボードで気づける一時停止ではなく、削除です。応答までの処理を検証、永続化、応答だけに絞り、X-Shopify-Webhook-Id で重複排除し、Partner アカウントの緊急連絡用の開発者メールアドレスがどこを指しているかを確認してください。届く唯一の警告は、そのアドレスに届きます。

すべての記事2026年8月3日 · 8 分

Webhook を、もう取りこぼさない。

URL を 1 つ変えるだけで、リトライ、イベントログ、1 クリックでの再送が使えます。先に動きを見ることもできます。新規登録して 30 秒の障害訓練を実行すると、サーバーがダウンしていてもイベントが失われない様子を確認できます。サーバーを用意する必要はありません。