Webhook2026年8月3日閱讀時間 8 分鐘

Shopify 刪除了你的 Webhook 訂閱。原因全在那 5 秒。

Shopify 給你的端點五秒,在四小時內重試八次,然後移除訂閱。不會丟出任何錯誤,訂單就這樣不再進來。

同樣的四個步驟疊起來,撞上一道固定的牆順利的日子1.2 秒ERP 變慢7.1 秒5 秒每次傳遞都會同時越線,所以八次重試在四小時內全部用完程式碼什麼都沒改。到週四,訂閱就被刪掉了。
這個處理程式沒有任何地方寫錯。它只是必須在 Shopify 停止等待之前完成。

這個症狀安靜得出奇。訂單不再出現在你的應用程式裡。log 裡沒有錯誤,因為根本沒有東西進到 log。Shopify 管理介面裡也沒有失敗的傳遞,因為已經沒有訂閱可以顯示傳遞了。Shopify 把它移除了,除非警告信寄到了有人會看的信箱,否則你收到的第一個訊號,會是商家來問為什麼最近兩天的訂單都不見了。

Shopify 的 Webhook 規則比大多數服務商嚴格,而這份嚴格集中在一個數字上。

重點摘要

  • 五秒。這就是你每次傳遞的全部回應時限。
  • Shopify 會在 4 小時內重試 8 次,連續失敗 8 次後,透過 Admin API 建立的訂閱就會被刪除(在 shopify.app.toml 裡宣告的不會)。
  • 從你的應用程式來看,刪除是無聲無息的。沒有流量,也沒有錯誤,因為已經沒有訂閱了。
  • 警告信和刪除通知信會寄到你 Partner 帳號的緊急開發者電子郵件(emergency developer email),而那通常是沒人在看的信箱。
  • 復原代表要重新建立訂閱,而且還要用 Admin API 回填,因為 Shopify 不會重播。

那五秒實際上花在哪裡

GitHub 給你十秒,Stripe 完全沒有公布數字,其他服務商的數字都在我們的 Webhook 服務商參考資料裡。Shopify 等回應等五秒,比這更慢的都視為傳遞失敗。

五秒也還不是最低。Slack 給的是三秒,而且嚴格到讓只串接過電商的人措手不及。Twilio 是個例外,也正好說明這些數字有多任意:它把旋鈕交給你,讓你自己設定逾時,但幾乎沒有人去轉它。

五秒聽起來綽綽有餘,直到你把一個典型處理程式裡實際發生的事寫下來:

步驟一般情況狀況差的日子
冷啟動(無伺服器)200 毫秒2.5 秒
HMAC 驗證2 毫秒2 毫秒
依網域查詢商店30 毫秒400 毫秒
寫入訂單和品項明細80 毫秒1.2 秒
同步到 ERP/3PL600 毫秒逾時
寄送確認信250 毫秒3 秒
同樣的四個步驟疊起來,撞上一道固定的牆順利的日子1.2 秒ERP 變慢7.1 秒5 秒每次傳遞都會同時越線,所以八次重試在四小時內全部用完程式碼什麼都沒改。到週四,訂閱就被刪掉了。

順利的情況下是 1.2 秒,看起來完全健康。接著某天早上 ERP 變慢,每一次傳遞同時超過五秒,你在四小時內用光全部八次重試。程式碼一行都沒改。程式碼也沒有任何錯。到了週四,訂閱就不見了。

有兩個讓情況更糟的因素,是 Shopify 特有的。

限時搶購會讓失敗集中發生。Webhook 的數量和訂單數成正比,所以你最需要 orders/create 的時刻,正是資料庫最忙、回應時間最差的時刻。這種失敗發生的時機,恰好和你承受得起的時機完全相反。

大量操作會一口氣產生大量 Webhook。商家透過應用程式或 CSV 匯入編輯 5,000 個商品,就會在短時間內產生 5,000 個 products/update Webhook。這時端點的速度被你自己的資料庫連線池卡住,而每一次超過五秒的傳遞都會算在你頭上。

刪除實際上是什麼樣子

在四小時內失敗八次之後,Shopify 就不再重試那個事件;連續失敗 8 次後,透過 Admin API 建立的訂閱會被移除,並寄一封警告信給應用程式的緊急開發者聯絡人。在 shopify.app.toml 裡宣告的應用程式專屬訂閱,不會以這種方式被移除。

Shopify 確實會寄信給你:偵測到反覆失敗時寄一封警告,訂閱被刪除時再寄第二封。兩封都寄到你 Partner 帳號上的緊急開發者電子郵件。實務上,這個地址只在當初註冊時設定過一次,常常是某位早已離職的人的個人信箱,這也是團隊被這件事嚇到最常見的原因。

現在就去確認那個地址是什麼。只要三十秒,這也是這篇文章裡最有價值的一件事。

刪除之後,webhookSubscriptions 對那個主題(topic)什麼都不回傳,也沒有任何傳遞或失敗,因為已經沒有東西可以失敗了。訂閱不存在期間產生的事件,也沒有在任何地方排隊等著。重新建立訂閱後,Shopify 不會重播。

最後這一點,正是代價高昂的原因。Stripe 端點被停用時,事件在 API 裡仍然存在 30 天;但 Shopify 的訂閱被刪除,代表那些 Webhook 根本從來沒送出。復原是一項對照 Admin API 的對帳工作,不是重播。

自架平台也沒有比較寬容:WooCommerce 也會在連續失敗超過五次後停用 Webhook。

壓到五秒以內

唯一可靠的結構是:驗證、儲存、確認收到,真正的工作拿到別的地方做。逾時檢查工具會告訴你,你的處理程式現在超出五秒多少。如果慢的那一步是模型呼叫,你的 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
}

有四個細節值得講清楚。

讀原始請求主體(raw body),不要讀解析過的 JSON。Shopify 的 HMAC 是對原始位元組計算的,所以任何會解析再重新序列化的 middleware,都會改變鍵的順序或空白,讓每一次簽章驗證都失敗。在 Next.js App Router 上,任何解析之前先呼叫 await req.text()。各服務商的細節見驗證 Stripe、GitHub 和 Shopify 的 Webhook 簽章。

先驗證再排入佇列,而不是反過來。驗證大約只要 2 毫秒,它是阻止攻擊者塞滿你佇列的那道關卡。便宜的關卡放最前面。

用 X-Shopify-Webhook-Id 去重。同一個事件的所有重試,它都不會變,所以它是正確的冪等鍵。Shopify 會重新傳送,所以你的消費端必須冪等。做法見同一個 Webhook 收到兩次。

注意冷啟動。在無伺服器上,程式碼還沒開始執行,冷啟動就可能吃掉一半的時限。讓 Webhook 路由的依賴圖保持精簡:一個只需要把位元組排入佇列的處理程式,卻 import 整個 ORM 和 admin 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 } }
  }
}

把結果交給你冪等的消費端處理。已經處理過的不會有任何效果;漏掉的就會被套用。這也是你發現消費端到底是不是真的冪等的時刻。

第三,清查你失去了哪些主題。列出目前的訂閱,和應用程式預期的清單比對差異。刪除是以訂閱為單位,所以 orders/create 不見了、products/update 卻還在,是完全正常的,這也讓症狀更令人困惑。

第四,修正緊急開發者電子郵件,並在此之上加上你自己的監控。一個「營業時間內 6 小時沒收到任何 orders/create」的警示,第一天就能抓到這個問題。

AnyHook 的角色

五秒的時限是傳輸層的問題,而傳輸正是中繼服務所取代的部分。

把 Shopify 訂閱指向 in.anyhook.net/you/shopify。不論你的 ERP 是健康還是著火,AnyHook 都會大約一秒就回 200。Shopify 的計時現在針對的是這個回應,而不是你的伺服器,所以伺服器變慢,不會再啟動那八次重試的倒數。唯一的例外是 AnyHook 完全無法儲存的事件:這時它會回 503 而不是 200,改由 Shopify 自己的重試接手。

  • 你的伺服器變慢,和 Shopify 的計時脫鉤。我們會依 5 分鐘、30 分鐘、3 小時、18 小時、之後每天一次的間隔,重試傳遞到你的伺服器,依方案共 3 到 10 次嘗試。在 Free 方案,最後一次嘗試在 35 分鐘時,所以更長的服務中斷,要靠儲存下來的事件紀錄和重播來補。
  • 每個事件都在嘗試傳遞之前先儲存,儲存時已加密,所以缺口可以從我們的事件紀錄重播,而不必從 Admin API 重建。
  • 連續失敗 1、5、20 次時會發出警示,寄到你能掌控的信箱,連續失敗 20 次時自動暫停。
  • 一鍵重播可以涵蓋伺服器當機的那段時間範圍,而且重播失敗的事件不計入每月配額,不過重播有每日上限,也會用到你的運算額度。

跨多家商店的公開應用程式,只需要一個 AnyHook App(一個接收網址,對應一個 Webhook 來源),不必每家商店各一個:把每家商店的訂閱都指向同一個接收網址(inbound URL)。Shopify 用你應用程式的用戶端密鑰(client secret)替應用程式的 Webhook 簽章,每家商店都一樣,所以 AnyHook 驗證的就是這個密鑰;X-Shopify-Shop-Domain 標頭仍然會告訴你的處理程式,每個事件是哪家商店送來的。

Shopify 的五秒規則,講的是它直接對話的那個端點。在那裡放一個大約一秒就回應的端點,這條規則就不再限制你的商業邏輯。

結論

Shopify 的五秒不是可以通融的目標,一再錯過的懲罰是刪除,而不是一個你能在儀表板上看到的暫停端點。把回應路徑精簡到驗證、儲存、確認收到,用 X-Shopify-Webhook-Id 去重,然後去確認你 Partner 帳號的緊急開發者電子郵件指向哪個地址。你唯一會收到的警告,就會寄到那裡。

所有文章2026年8月3日 · 8 分鐘

別再漏掉 Webhook。

換一個網址,就有重試、事件紀錄和一鍵重播。或者先親眼看看:註冊後跑一次 30 秒的當機演練,看著一個事件在伺服器當機時保存下來。不需要伺服器。