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, and after 8 consecutive failures a subscription created through the Admin API is deleted (ones declared in
shopify.app.tomlare not) - A webhook added in the Shopify admin is not deleted, and nobody is emailed. On a test store we watched one fail 36 times in a row: the next day it was still listed, and the four orders behind those failures had never arrived
- 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
GitHub gives you ten seconds, Stripe publishes no figure at all, and the rest are in our webhook provider reference. Shopify waits five seconds for a response and treats anything slower as a failed delivery.
Five is not the floor either. Slack allows three, and it is strict about it in a way that catches out anyone who has only ever integrated a store. Twilio is the exception that shows how arbitrary these numbers are: it hands you the dial and lets you set the timeout yourself, and almost nobody ever turns it.
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 are specific to Shopify.
Flash sales correlate failures. Webhooks arrive in proportion to orders, so 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, and after 8 consecutive failures a subscription created through the Admin API is removed, with a warning email to the app's emergency developer contact. App-specific subscriptions declared in shopify.app.toml are not removed this way.
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, webhookSubscriptions returns nothing for that topic, and there are no deliveries and no failures because there is nothing left to fail. Events generated while the subscription was gone are not queued anywhere either. 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.
Self-hosted platforms are no gentler about it: WooCommerce disables a webhook after more than five consecutive failures too.
What we saw on a test store
The deletion rule above is for subscriptions created through the Admin API. A webhook added by hand in the admin, under Settings → Notifications → Webhooks, behaves differently, and Shopify's docs do not mention it. On 2 November 2026 we made one fail on purpose on a development store. It took 36 failed deliveries in a row, and the next day it was still listed, with no warning on the page and no email to the store owner. The four orders behind those failures never arrived. If your ERP or warehouse sync runs off an admin webhook, this is what an outage looks like: nothing disappears, nothing warns you, and the orders from that window are never sent.
The same test logged the timing Shopify does not publish: attempts at about 2 seconds, 3 seconds, 1 minute, 3 minutes, 8 minutes, 30 minutes, 1 hour, 2 hours and 4 hours, then nothing, even after the receiver recovered. Any order placed more than four hours before your endpoint recovers is gone from the webhook path, and the Admin API backfill below is the only way to get it back.
Getting under five seconds
The only reliable shape is: verify, persist, acknowledge, and do the real work somewhere else. The timeout checker shows how far over the five seconds your handler is today, and if the slow step is a model call, Your Webhook Handler Calls an LLM has the same shape for each platform.
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, so any middleware that parses and re-serializes will change key order or whitespace and every signature check will fail. On the Next.js App Router, call 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 about 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 has to 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
First, 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 }
}
}
Second, 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.
Third, 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.
Fourth, 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 about a second, whether your ERP is healthy or on fire. Shopify's clock now runs against that answer instead of your origin, so your origin's slowness no longer starts the eight-retry countdown. The one exception is an event AnyHook cannot store at all: then it answers 503 rather than 200, and Shopify's own retry takes over.
- Your origin's slowness is decoupled from Shopify's clock. We retry against your server after gaps of 5 minutes, 30 minutes, 3 hours, 18 hours, then daily, 3 to 10 attempts by plan. On Free the last attempt is at 35 minutes, so a longer outage is covered by the stored log and replay
- 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 fire at 1, 5, and 20 consecutive failures, sent to an address you control, with auto-pause at 20
- One-click replay covers a time range for the window your server was down, and replays of failed events don't count against your monthly quota, though they have a daily limit and use your compute allowance
A public app across many shops needs one AnyHook app, not one per shop: point every shop's subscription at the same inbound URL. Shopify signs app webhooks with your app's client secret, the same for every shop, so that is the secret AnyHook checks, and the X-Shopify-Shop-Domain header still tells your handler which shop sent each event.
Shopify's five-second rule is a statement about the endpoint it talks to. Put an endpoint there that answers in about a second, 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. A webhook added in the admin gets no warning at all, so after any outage longer than four hours, run the backfill whether or not anything told you to.