GA4 counts a Shopify order twice when two sources send the purchase, for example the Google and YouTube app plus a GTM tag or a custom pixel. GA4 only drops repeats that share the same transaction ID from the same user, so two sources using different ID formats are both counted. Find them with an exploration of Transaction ID by event count, then keep one purchase source.
Most GA4 complaints on Shopify are about missing purchases. This one is the opposite: GA4 reports more purchases, or more revenue, than the store actually took. It looks like good news until you notice that conversion rate, revenue per session and every ROAS number built on GA4 are now wrong too.
The cause is nearly always the same: two things are sending the purchase event. This guide shows how to prove it in a few minutes, the five setups that cause it and how to get back to one purchase per order.
Signs GA4 is double counting
- GA4 purchases above Shopify web orders. Compare the same dates and the same timezone. Because consent banners and ad blockers hide some purchases, a healthy GA4 setup sits just below Shopify. Anything above 100% means duplicates.
- Revenue roughly double on some days. Especially right after an app was installed or GTM was changed.
- Two formats of transaction ID in your reports, like
1042alongside a long number such as5739204817201for the same day's orders. - Google Ads conversions jumped at the same time, if you import GA4 purchases there.
If GA4 is below Shopify instead, the cause is different, and why GA4 and Shopify revenue don't match covers it.
Why GA4's own deduplication misses it
GA4 does deduplicate purchases. On web streams, it uses the transaction_id parameter to drop repeat purchase events with the same ID from the same user. That handles a customer refreshing the Thank you page with one tracking setup.
It does not help in the cases that cause most duplicates:
- Different IDs for the same order. One source sends the order number,
1042or#1042. Another sends Shopify's internal order ID, a long number. To GA4 these are two transactions. - Different users for the same order. A server-side source that doesn't pass the browser's client ID creates a new user, so the two purchases don't look like the same person.
- No ID at all. GA4 treats every purchase with an empty
transaction_idas the same one, which can drop real orders instead. That's an undercount, but it often turns up in the same messy setups.
Prove it with one exploration
- Create a free form exploration. In GA4, go to Explore, then Free form.
- Add the dimension Transaction ID and the metrics Event count and Purchase revenue. Add a filter: Event name exactly matches
purchase. - Set the date range to the last 7 days and sort by Event count, highest first.
- Read the result. Rows with more than one event are repeats. More often, every row shows 1, but you see two ID formats side by side and roughly twice as many rows as Shopify had orders. That's two sources.
- Add a second dimension such as Session source / medium or Stream name. Two sources often differ here, for example one with proper attribution and one with
(direct)or(not set).
If you export GA4 to BigQuery, this query lists repeated IDs directly:
SELECT
ecommerce.transaction_id,
COUNT(*) AS purchase_events,
COUNT(DISTINCT user_pseudo_id) AS users
FROM `your-project.analytics_123456789.events_*`
WHERE event_name = 'purchase'
AND _TABLE_SUFFIX BETWEEN '20261001' AND '20261031'
GROUP BY 1
HAVING purchase_events > 1
ORDER BY purchase_events DESCThe export holds raw events, so it can show repeats that GA4's reports have already removed. Compare the total number of distinct IDs with Shopify's order count for the same days to see the real scale.
The 5 usual causes
1. The Google and YouTube app plus GTM or a custom pixel
The most common case. The Google and YouTube app is connected to GA4 and sends purchases, and someone also added a GA4 purchase in GTM or a custom pixel in Settings, Customer events. Both fire on every order, usually with different ID formats.
2. Two custom pixels
A tracking app installed its own pixel, and an older custom pixel added during the checkout extensibility migration was never removed. Open Settings, Customer events and check every pixel that is connected.
3. Browser plus server without a shared ID
A server-side setup sends purchases through the Measurement Protocol or server-side GTM, and the browser still sends them too. Unless both send the same transaction_id and the same client ID, GA4 counts both.
4. Purchase firing on the order status page
Code that sends a purchase when the order status page loads fires again every time the customer comes back to check delivery. Purchases should fire on Shopify's checkout_completed event, which happens once per order.
5. Upsell and post-purchase apps
Some post-purchase upsell apps send their own purchase event. That's fine if it carries only the upsell value with its own ID. If it resends the whole order, it's a duplicate.
Our free tracking check matches your GA4 purchases against Shopify orders, finds every source sending them and tells you which to remove. It checks Google Ads and Meta at the same time. Read-only, within 3 business days.
Get a free tracking checkHow to fix it
The rule is the same as for Meta: one source per purchase, with one stable transaction ID.
| Cause | Fix |
|---|---|
| App plus GTM or custom pixel | Pick one. Either keep the Google and YouTube app's GA4 connection and remove the other purchase tag, or disconnect GA4 in the app and keep your own pixel. |
| Two custom pixels | Disconnect the older pixel in Settings, Customer events. |
| Browser plus server | Send the purchase from one side only, or make both use the same transaction ID and client ID. |
| Order status page | Move the purchase to checkout_completed. |
| Upsell app | Send only the upsell value with a separate ID, or turn off the app's GA4 tracking. |
Whichever source you keep, set transaction_id to one consistent Shopify value on every purchase. The order number is easier to match against Shopify reports by eye, and the order ID is unique across stores. Either works, as long as it never changes.
After the fix
- Place a test order and watch DebugView. You want exactly one purchase event.
- Rerun the exploration a few days later. One ID format, one row per order.
- Add an annotation on the fix date so nobody reads the drop in revenue as a sales problem.
- Check Google Ads. If it imports GA4 purchases, conversions will drop too. If it also has its own purchase action, see Google Ads double counting.
- Watch the ratio weekly. GA4 purchases divided by Shopify web orders. Healthy setups usually land between 0.9 and 1.0. A jump above 1.0 means something new started sending purchases.
The same problem often appears on Meta at the same time, because the same app or GTM change touched both. If Meta shows more purchases than Shopify too, read Meta duplicate purchases on Shopify.
Common questions
Does GA4 deduplicate purchases with the same transaction ID?
Yes, for web streams. GA4 uses transaction_id to remove repeat purchase events from the same user. It does not help when two sources send different IDs for the same order, and it does not work for app streams.
Why does GA4 show more purchases than Shopify?
Almost always because two things send the purchase event, such as the Google and YouTube app plus a GTM tag or custom pixel, and they use different transaction ID formats. Less often, a purchase fires every time the order status page loads.
What happens if transaction_id is empty?
GA4 treats every purchase with an empty transaction_id as the same transaction, so it can drop real orders and undercount. Every purchase should carry the Shopify order ID or order number.
Do duplicate GA4 purchases affect Google Ads?
If you import GA4 purchases into Google Ads as a conversion, yes. The duplicates go with them, inflate conversions and ROAS, and push Smart Bidding toward the wrong targets.
Can I remove duplicate purchases from past GA4 data?
No. GA4 does not let you delete or edit events already collected. Fix the source, annotate the date in your reports, and use the BigQuery export if you need clean historical numbers.
