TL;DR
Shopify gives your webhook endpoint 5 seconds to return a 2xx. Miss that window and Shopify treats the delivery as failed and retries, up to 8 times across roughly 4 hours. Shopify also warns that an app "might receive the same webhook more than once" [1]. For a purchase-tracking app, every re-delivery is a chance to send the same order's conversion a second time. That is same-platform double-counting, and it is a different problem from the cross-platform browser-versus-server overlap that a shared event_id solves. Ad platforms do dedup conversions on their side, but each platform's window is bounded and rule-specific, so it is a backstop, not a fix. The reliable answer is source-side idempotency: record that an order's server-side purchase has already been sent, so retries and concurrent deliveries do not re-send. WeltPixel Conversion Tracking does this across all five server-side senders, including GA4.
Key Takeaways
- Shopify waits 5 seconds for a 2xx, then retries a failed webhook up to 8 times over about 4 hours. A slow handler that responds late gets the same order delivered again.
- Concurrent deliveries can hit more than one app instance at once, so two sends can race even when neither handler is slow.
- Both cases produce same-platform double-counting: the same order's purchase event sent twice to the same destination.
- Platform-side dedup (Google Ads
transaction_id, Metaevent_nameplusevent_id) is window-bounded and rule-specific, so it catches some duplicates and misses others. - This is distinct from cross-platform
event_iddedup, which matches a browser event against a server event for the same order. Different failure mode, different fix. - Source-side idempotency records each order's send-state, so retries and concurrent deliveries are ignored. WCT applies this to GA4, Meta, TikTok, Google Ads, and Reddit.
How does a slow webhook response turn into a double-counted purchase?
Start with the timing rule, because the whole problem hangs on one number. When an order is created, Shopify delivers an orders/create webhook to your endpoint and waits 5 seconds for a 2xx response. Any 2xx means success. No response, a timeout, or an error means failure, and on failure Shopify retries the delivery up to 8 times over the next 4 hours or so [1].
A purchase-tracking handler is not free work. It often has to resolve order data, match the customer, and forward a conversion to one or more ad platforms before it can safely respond. If that work runs long and the handler responds after the 5-second budget has already passed, Shopify has already counted the delivery as failed and queued a retry. The retry arrives, the handler runs again, and now the same order's purchase gets sent a second time. Nothing errored in a way you would notice. The first send succeeded. The retry just did not know that.
Shopify is explicit about this on the delivery side. The webhook documentation states that your app "might receive the same webhook more than once, for example after a network timeout or a retry," and recommends queuing work so you can respond within the window [1]. Duplicate delivery is a documented, expected condition, not an edge case you can wish away. The takeaway is that any handler doing real work per order has to assume re-delivery and plan for it.
How do concurrent deliveries cause races even when nothing is slow?
The slow-handler case is the obvious one. The quieter case is concurrency. A busy store runs more than one app instance behind a load balancer, and a retried or duplicated delivery can land on a different instance than the original. Now you have two handlers processing the same order at nearly the same moment, neither of them slow, each unaware of the other.
A naive guard that lives only inside one process does not help here. An in-memory check on instance A says nothing to instance B. Both pass their local check, both send, and the order's purchase counts twice. This is why a duplicate-prevention scheme has to share state across instances, not just within a single process. The send-state for an order has to be visible to whichever instance picks up the next delivery, and it has to stay visible across the full retry window, which can stretch hours past the first attempt.
| Trigger | What happens | What stops it |
|---|---|---|
| Slow handler exceeds the 5s window | Shopify retries; the late handler runs again | Recorded send-state: the retry sees the order is already sent |
| Concurrent delivery to a second instance | Two handlers process the same order at once | Shared in-progress state across instances |
| Re-delivery hours later in the retry window | A late retry arrives after the first send | Persistent send-state outliving any single process |
Why doesn't platform-side deduplication fully save you?
Ad platforms know duplicates happen, so most of them dedup on their end. Google Ads dedups conversions that share the same transaction_id, counting the first and dropping the rest, and recommends the "one" conversion count per click for ecommerce. The catch is that the IDs have to match exactly across sources, or the dedup does not fire [2]. Meta dedups when it receives two events with the same event_name and event_id within a short window, keeping one and discarding the other, and Meta's own guidance treats this as a safety net rather than a reason to send duplicates freely [3]. TikTok and Reddit each have their own version, keyed on a conversion or event identifier, with their own time windows.
The pattern across all of them is the same. Every platform has some dedup, but each is window-bounded and rule-specific. A duplicate that lands outside the window, or that carries a drifted identifier, counts twice. You are relying on five different rules, with five different windows, to clean up duplicates you generated yourself. That works until it does not, and when it fails you find out from a revenue number that does not reconcile.
The cleaner position is to not generate the duplicate in the first place. If each order's server-side purchase is sent once, platform dedup becomes a backstop you rarely lean on instead of a load-bearing part of your accuracy. That is the difference between hoping the destination cleans up after you and stopping the duplicate at the source.
How is this different from cross-platform event_id dedup?
This is worth pinning down, because the two problems sound alike and have different fixes. Cross-platform deduplication is about one order being tracked by two transports. The browser pixel fires a purchase, the server sends the same purchase, and a shared event_id lets the platform recognize them as the same event and keep one. That is the subject of our companion piece on how event_id prevents double-counting on Shopify, and it is the right tool when the duplicate comes from running browser and server tracking together. If you are still deciding whether to run both, the conversion API versus browser pixel breakdown covers why most stores end up wanting both transports.
Same-platform duplication is a different animal. Here there is only one transport: the server. The duplicate exists because Shopify re-delivered the webhook or two instances raced, and the server sent the same order's purchase twice. A shared event_id does not save you, because both sends could legitimately carry the same event_id, and the platform only dedups within its own window. The fix lives on your side, before the event ever leaves: record the order's send-state so the second attempt never sends. One problem is solved by the destination matching two transports. The other is solved by the source refusing to send twice. They are complementary, not redundant.
What does source-side idempotency look like in practice?
Idempotency here means a simple promise: processing the same order's purchase more than once produces the same result as processing it once. WeltPixel Conversion Tracking keeps that promise by recording each order's server-side send-state. When a delivery arrives, WCT checks whether that order's purchase has already been sent. If it has, the send is skipped. While a send is actually running, the order is marked in-progress, so a concurrent delivery arriving on another instance sees the in-flight state and stands down rather than racing. After a successful send, the order is recorded as sent, and any later retry in the 4-hour window finds that record and does nothing.
Two details matter for accuracy. First, the record is persistent and tied to the order, so it survives across processes and instances and across the full retry window, where a per-process memory check could not. Second, the protection wraps the entire server-side send, so it covers all five senders: GA4 by Measurement Protocol, and Meta, TikTok, Google Ads, and Reddit by their conversions APIs. GA4 also dedups on transaction_id natively, so for GA4 this is belt and suspenders, but the protection is the same record-once behavior for every destination. The browser-side experience is unchanged, and where browser events are involved they respect Shopify's Customer Privacy API signals.
How do I tell if my store is double-counting server-side purchases?
The symptom is a platform purchase count that runs higher than your Shopify order count for the same period, with no returns or test orders to explain the gap. A few percent of phantom conversions is enough to distort ROAS and make a campaign look better than it is.
To check it yourself, start by comparing each ad platform's reported purchase count against Shopify's order count for a clean date range. Then in Shopify Admin, review your tracking under Settings → Customer events to confirm which pixels and apps are firing purchases, so you know how many transports are actually sending. Our pixel audit walkthrough for Shopify shows how to read those numbers and spot the duplicate pattern. Once you have it working, the post-install verification checklist gives you a repeatable way to confirm each platform counts each order once.
FAQ
Does Meta or Google's deduplication handle this for me?
Partly. Google Ads dedups on transaction_id and Meta dedups on event_name plus event_id, but both are window-bounded and rule-specific [2][3]. A duplicate that arrives outside the window or carries a drifted ID counts twice. Platform dedup is a backstop. Stopping the duplicate at the source is the only platform-agnostic fix.
If I run multiple pixels, does each one double-count?
Multiple pixels multiply the surface area. Each destination has its own dedup window, so a retry that gets cleaned up on one platform can slip through on another. Source-side idempotency is what keeps the behavior consistent across every destination at once.
Will a retried webhook still send if the first send already succeeded?
No. Once an order's server-side purchase is recorded as sent, a later retry in Shopify's 4-hour window finds that record and skips the send. That is the entire point of recording send-state per order.
Does this affect GA4 too, or only the conversions APIs?
It affects GA4 too. The protection wraps the whole server-side send, so it covers GA4's Measurement Protocol purchase alongside Meta, TikTok, Google Ads, and Reddit. GA4 also dedups on transaction_id on its side, so for GA4 it is belt and suspenders.
How is this different from browser-plus-server event_id dedup?
event_id dedup matches one order tracked by two transports, the browser pixel and the server. This is one transport, the server, sending the same order twice because of a retry or a race. Different cause, different fix. See the event_id deduplication guide for the cross-platform side.
Stop counting the same order twice
Duplicate purchases inflate every downstream number, from ROAS to GA4 revenue, and platform-side dedup only catches some of them. WeltPixel Conversion Tracking records each order's server-side purchase as sent, so Shopify's webhook retries and concurrent deliveries do not fire the same conversion twice across GA4, Meta, TikTok, Google Ads, and Reddit.
Install WeltPixel Conversion Tracking on the Shopify App Store
Sources
- Shopify. "Deliver webhooks through HTTPS." https://shopify.dev/docs/apps/build/webhooks/subscribe/https
- Google Ads Help. "Use a transaction ID to minimize duplicate conversions." https://support.google.com/google-ads/answer/6386790
- Meta for Developers. "Deduplicate pixel and server events." https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events/