The symptom is unusually quiet. Orders stop appearing in your app. No errors in your logs, because nothing is reaching your logs. No failed deliveries in the Shopify admin, because there is no subscription left to show deliveries for. Shopify removed it, and unless the warning email reached a mailbox somebody reads, the first signal is a merchant asking why their last two days of orders are missing.
Shopify's webhook rules are stricter than most, and the strictness is concentrated in one number.
TL;DR
- Five seconds. That is your entire response budget, per delivery
- Shopify retries 8 times over 4 hours; if failures persist over roughly the next 48 hours the subscription is deleted
- Deletion is silent from your app's perspective. No traffic and no errors, because there is no subscription left
- Warning and deletion emails go to your Partner account's emergency developer email, which is usually the address nobody monitors
- Recovery means re-creating the subscription and backfilling with the Admin API, because Shopify does not replay
Where the five seconds actually goes
Stripe gives you tens of seconds. GitHub is comparably relaxed. Shopify waits five seconds for a response and treats anything slower as a failed delivery.
Five seconds sounds like plenty until you write down what actually happens inside a typical handler:
| Step | Typical | Bad day |
|---|---|---|
| Cold start (serverless) | 200ms | 2.5s |
| HMAC verification | 2ms | 2ms |
| Look up shop by domain | 30ms | 400ms |
| Write order + line items | 80ms | 1.2s |
| Sync to ERP / 3PL | 600ms | timeout |
| Send confirmation email | 250ms | 3s |
The happy path is 1.2 seconds and looks completely healthy. Then your ERP has a slow morning, every delivery crosses five seconds simultaneously, and you burn all eight retries inside four hours. Nothing in your code changed. Nothing in your code is wrong. The subscription is gone by Thursday.
Two aggravating factors specific to Shopify:
Flash sales correlate failures. Webhooks arrive in proportion to orders. The moment you most need orders/create is the moment your database is busiest and your response times are worst. The failure mode is precisely anti-correlated with when you can afford it.
Bulk operations fan out hard. A merchant editing 5,000 products through an app or a CSV import generates 5,000 products/update webhooks in a short window. Your endpoint is now rate-limited by your own database connection pool, and every delivery over five seconds counts against you.
What deletion actually looks like
After eight failed attempts within the four-hour window, Shopify stops retrying that event. If deliveries keep failing over roughly the next 48 hours, the subscription itself is removed.
Shopify does email you: a warning when repeated failures are detected, and a second when the subscription is deleted. Both go to the emergency developer email on your Partner account. In practice that address was set once during onboarding, often to a personal address of someone who has since left, and it is the single most common reason a team is surprised by this.
Go check what that address is right now. It takes thirty seconds and it is the highest-value thing in this post.
After deletion:
webhookSubscriptionsreturns nothing for that topic- No deliveries and no failures, because there is nothing left to fail
- Events generated while the subscription was gone are not queued anywhere. Shopify does not replay after re-creation
That last point is what makes this expensive. Unlike a Stripe endpoint being disabled, where the events still exist in the API for 30 days, a deleted Shopify subscription means those webhooks were simply never sent. Recovery is a reconciliation job against the Admin API, not a replay.
Getting under five seconds
The only reliable shape is: verify, persist, acknowledge, and do the real work somewhere else.
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
}
Four details worth being precise about:
Read the raw body, not parsed JSON. Shopify's HMAC is computed over the exact bytes. Any middleware that parses and re-serializes will change key order or whitespace and every signature check will fail. On Next.js App Router, await req.text() before any parsing. See Verifying Stripe, GitHub, and Shopify Webhook Signatures for the per-provider details.
Verify before queueing, not after. Verification is ~2ms and it's the gate that stops an attacker filling your queue. Cheap gate first.
Dedupe on X-Shopify-Webhook-Id. It is stable across all retries of the same event, which makes it the correct idempotency key. Shopify will re-deliver, so your consumer must be idempotent. The patterns are in You Received the Same Webhook Twice.
Watch cold starts. On serverless, a cold start can eat half your budget before your code runs. Keep the webhook route's dependency graph small; importing your entire ORM and admin SDK into a handler that only needs to enqueue bytes is a real and avoidable cost.
Recovering after a deletion
1. Re-create the subscription.
mutation {
webhookSubscriptionCreate(
topic: ORDERS_CREATE
webhookSubscription: {
callbackUrl: "https://your-app.example.com/webhooks/shopify"
format: JSON
}
) {
webhookSubscription { id }
userErrors { field message }
}
}
2. Reconcile the gap from the Admin API. Since nothing is replayed, query the objects directly for the outage window and process them through the same code path your webhook consumer uses:
{
orders(first: 250, query: "updated_at:>2026-07-28T00:00:00Z") {
edges { node { id name updatedAt displayFinancialStatus } }
}
}
Run that through your idempotent consumer. Anything already handled is a no-op; anything missed gets applied. This is also the moment you discover whether your consumer really is idempotent.
3. Audit which topics you lost. List current subscriptions and diff against what your app expects. Deletion is per-subscription, so it is entirely normal to lose orders/create while products/update survived, which makes the symptom even more confusing.
4. Fix the emergency developer email, and add your own monitoring on top. A "no orders/create received in 6 hours during business hours" alert would have caught this on day one.
Where AnyHook fits
The five-second budget is a transport problem, and transport is exactly what a relay replaces.
Point the Shopify subscription at in.anyhook.net/you/shopify. AnyHook returns 200 in under 50ms, every time, whether your ERP is healthy or on fire. From Shopify's perspective the endpoint is permanently fast, so the eight-retry countdown never starts and the subscription is never deleted.
- Your origin's slowness is decoupled from Shopify's clock. We retry against your server on a schedule measured in hours, not four
- Every event is persisted before delivery is attempted, encrypted at rest, so a gap is replayable from our log rather than reconstructable from the Admin API
- Failure-streak alerts at 1, 5, and 20 consecutive failures, sent to an address you control, with auto-pause at 20
- One-click replay over a time range for the window your server was down. Replays don't consume quota
Shopify's five-second rule is a statement about the endpoint it talks to. Make that endpoint something that is always fast, and the rule stops constraining your business logic.
Takeaway
Shopify's five seconds is not a soft target, and the penalty for missing it repeatedly is deletion rather than a paused endpoint you can spot in a dashboard. Get the response path down to verify-persist-acknowledge, dedupe on X-Shopify-Webhook-Id, and go check what address your Partner account's emergency developer email points at. That is where the only warning you get is going to land.