Reliable Shopify webhooks: retries, idempotency, ordering

Short answer

Shopify gives a webhook endpoint five seconds to answer with a 2xx status; otherwise it retries 8 times over the next 4 hours, and a subscription created through the Admin API is deleted after 8 consecutive failures. Delivery isn’t guaranteed, duplicates happen and order isn’t guaranteed. So verify the HMAC, acknowledge fast, queue the work, deduplicate by X-Shopify-Webhook-Id, order by timestamp and reconcile regularly.

  • Shopify has a one-second connection timeout and a five-second timeout for the whole webhook request.
  • Any response outside the 2xx range, including 3xx redirects, is treated as an error.
  • After an error or no response, Shopify retries 8 times over the next 4 hours.
  • After 8 consecutive failures, a subscription configured through the Admin API is deleted automatically.
  • Shopify doesn’t guarantee ordering within a topic, or across topics for the same resource.
  • X-Shopify-Webhook-Id identifies a delivery and is the header Shopify says to deduplicate on.

Webhooks look simple: Shopify calls your URL when something changes, and you update the other system. The failures come from the parts that aren’t simple. A slow handler times out and gets retried, so the same event arrives twice. Two updates to one product arrive in the wrong order, so the older one wins. A deploy goes wrong for an afternoon, and the subscription disappears. Each is documented behaviour, and each has a known answer.

The delivery rules

RuleWhat Shopify documents
TimeoutsOne second to connect, five seconds for the whole request
SuccessA 2xx response. Anything else, including 3xx, is an error
RetriesAfter an error or no response, 8 retries over the next 4 hours
RemovalAfter 8 consecutive failures, a subscription configured through the Admin API is deleted; warning emails go to the app’s emergency developer email address
GuaranteeDelivery isn’t always guaranteed
OrderingNot guaranteed within a topic, or across topics for the same resource
DuplicatesMinimised, but the same webhook can arrive more than once

Everything below follows from that table. A reliable handler assumes it will sometimes get a delivery twice, sometimes get deliveries out of order, and sometimes not get one at all.

1. Verify before anything else

Every HTTPS delivery carries X-Shopify-Hmac-Sha256, a base64-encoded HMAC-SHA256 of the raw request body, keyed with your app’s client secret. Compute it yourself, compare, and reject the request if it doesn’t match. Shopify’s guide makes one ordering point that trips up many frameworks: put the verification before any body-parsing middleware, because the signature is computed on the raw, unparsed body.

import {createHmac, timingSafeEqual} from 'node:crypto';

export function isFromShopify(rawBody: Buffer, header: string | null, secret: string) {
  if (!header) return false;
  const digest = createHmac('sha256', secret).update(rawBody).digest();
  const received = Buffer.from(header, 'base64');
  return received.length === digest.length && timingSafeEqual(received, digest);
}

2. Acknowledge fast, work later

Five seconds is the whole budget, including the network. A handler that calls an ERP, writes to three tables and sends an email before answering will sometimes run over, and a timeout counts as a failure, which means a retry and a duplicate. Shopify’s own advice is to use a queue: store the payload, answer with a 2xx, and process it in a worker. The worker can then take as long as the other system needs, and retry on its own schedule.

Shopify’s delivery system also reuses connections with HTTP Keep-Alive, so make sure it is enabled on your endpoint. And don’t answer with a redirect: a 3xx is counted as an error.

3. Make processing idempotent

Shopify’s wording is direct: process webhooks with idempotent operations, so receiving the same webhook twice doesn’t produce a different outcome. Two headers help, and they mean different things:

  • X-Shopify-Webhook-Id is a unique key per delivery. Shopify’s verification guide says to check it against a persistent store: if it has been seen, skip the work and return success; if not, process it and save it.
  • X-Shopify-Event-Id is shared across all deliveries produced by the same merchant action. It groups what one action caused; it is not the deduplication key.

Deduplicating by delivery ID stops the exact-repeat case. Idempotent writes cover the rest: set a stock level to a value instead of adding a delta, upsert by the Shopify resource ID instead of inserting, and make side effects such as emails check whether they already happened. Save the delivery ID in the same transaction as the change it caused, so a crash between the two can’t leave one without the other.

4. Don’t trust arrival order

Because ordering isn’t guaranteed, a products/update from 10:02 can arrive after one from 10:05. Shopify’s guidance is to sequence events with timestamps: X-Shopify-Triggered-At in the headers, or updated_at in the payload. In practice, store the timestamp of the last change you applied for each resource, and ignore any event older than it.

For some jobs the simplest fix is to treat the webhook as a signal only: when a product changes, fetch the product’s current state from the Admin API and apply that. The data is then always the latest, whatever order the notifications came in, at the cost of an extra API call per event.

5. Reconcile, because some deliveries never arrive

Shopify says it plainly: webhook delivery isn’t always guaranteed, and an app can also miss events through its own failures or downtime. Its recommendation is a reconciliation job that periodically fetches data from Shopify so the app stays consistent, and many GraphQL queries accept an updated_at filter so the job only reads what changed since its last run.

The webhook becomes the fast path and the reconciliation job the safety net. How often the job runs depends on how much drift the business can tolerate: stock and prices usually need a tighter window than customer tags. For stock specifically, see planning a Shopify inventory sync.

6. Watch the subscription, not just the code

  • For apps created in the Dev Dashboard or with Shopify CLI, the app’s Overview page in the Dev Dashboard shows deliveries, the failed delivery rate and response time, and its Logs page lists individual deliveries from the past 7 days. The logs can lag by several minutes.
  • Make sure the app’s emergency developer email address reaches someone: it receives the warnings before an Admin API subscription is deleted.
  • After an outage, fix the cause, recreate any deleted subscriptions, and run the reconciliation job for the gap.
  • Alert on your own queue: a backlog that keeps growing is the early sign, long before Shopify starts retrying.

The whole handler, in order

Put together, a handler that follows Shopify’s guidance does the same few things for every delivery:

  1. Read the raw body and verify X-Shopify-Hmac-Sha256. Reject the request if it doesn’t match.
  2. Read X-Shopify-Webhook-Id. If it is already in your store of processed deliveries, return success and stop.
  3. Write the payload, the topic from X-Shopify-Topic, the store from X-Shopify-Shop-Domain and the timestamp from X-Shopify-Triggered-At to a queue.
  4. Return a 2xx, well inside five seconds.
  5. In the worker, compare the event’s timestamp with the last change applied to that resource, and skip it if it is older.
  6. Apply the change with an idempotent write, and save the delivery ID in the same transaction.
  7. Let the reconciliation job catch anything that never arrived.

One more header is worth logging: X-Shopify-API-Version states the API version used to serialize the payload. When a field seems to be missing or renamed after an upgrade, it is the first thing to check.

Where Lintel fits

Webhook handlers, queues and reconciliation jobs are the core of Lintel’s API integration work, and of custom Shopify apps that keep another system in step with a store. For an integration that already drifts, the first step is a review of the handler against the six points above: it usually shows which of them is missing.

Questions

How many times does Shopify retry a failed webhook?
If Shopify gets no response or an error, it retries 8 times over the next 4 hours. After 8 consecutive failures, a subscription configured through the Admin API is deleted automatically, and warning emails go to the app’s emergency developer email address.
How long does my endpoint have to respond?
Shopify has a one-second connection timeout and a five-second timeout for the whole request. Respond with a 2xx status quickly and do slow work in a queue.
Can Shopify send the same webhook twice?
Yes. Shopify minimises duplicates, but the same webhook can arrive more than once, for example after a timeout or a retry. Deduplicate using the X-Shopify-Webhook-Id header and make processing idempotent.
Are Shopify webhooks delivered in order?
No. Shopify doesn’t guarantee ordering within a topic or across topics for the same resource. Use X-Shopify-Triggered-At or the payload’s updated_at to decide which change is newest.

Sources

  1. About webhooksShopify developer documentation, checked 26 September 2026
  2. Webhook delivery structure (headers)Shopify developer documentation, checked 26 September 2026
  3. Verify webhook deliveries (HTTPS delivery, retries, duplicates)Shopify developer documentation, checked 26 September 2026
  4. Troubleshoot webhooksShopify developer documentation, checked 26 September 2026