Skip to content
Back to blog
GA4Jul 11, 2026 · Ludde Nyström · 14 min read

GA4 Purchase Event Firing Twice on Shopify? Root Causes and the Real Fix.

Duplicate purchase events in GA4 on Shopify usually mean two tracking methods fired for the same order — here's how to confirm it and fix it.

GA4 Purchase Event Firing Twice on Shopify? Root Causes and the Real Fix

Someone opens GA4's Realtime report the morning after a launch and counts two purchase events for the same order, seconds apart. Or it's subtler: the weekly Monetization overview says 214 purchases, Shopify's Orders page says 107, and the ratio is suspiciously close to 2:1. Either way, the instinct is to assume GA4 is somehow inflating numbers on its own — but GA4 doesn't invent purchases. Something is telling it about the same order twice.

This is the mirror image of the more common Shopify-GA4 mismatch, where GA4 runs lower than Shopify because of consent, ad blockers, or a tag that isn't firing at all — that version is covered in our companion piece on GA4 revenue not matching Shopify. Duplicate purchase events are a different, much more mechanical problem. On Shopify specifically, they cluster around a small set of well-documented patterns, and almost all of them boil down to the same root cause: two tracking mechanisms independently reporting the same checkout.

Quick answer

GA4's purchase event fires twice on Shopify almost always because two separate mechanisms are both sending it for the same order — most often Shopify's native Google & YouTube app running alongside a hand-built GTM or gtag purchase tag, or a leftover Additional Scripts / checkout.liquid snippet still live after a Web Pixels-based implementation was added during Shopify's checkout-extensibility migration. GA4 does have a built-in deduplication rule for purchase events, but it only collapses two events that carry the exact same transaction_id — and the two mechanisms above typically generate different transaction_id values for the same order (a checkout token versus a Liquid order number, for example), so the dedup never triggers and both purchases post to your reports. The fix is finding and removing the redundant install, not adding more tracking code on top of it.

How Do You Confirm You Actually Have Duplicate Purchases?

Before chasing a cause, confirm the pattern is real and mechanical rather than a genuinely good week. Two checks, in order of speed.

Realtime purchase count vs. Shopify orders. Open GA4 → Reports → Realtime and note the purchase event count for a short window — an hour is usually enough on a store with reasonable volume. Compare it against Shopify's own Orders count for the exact same window, filtered to the Online Store sales channel (POS, wholesale, and other channels never touch a browser, so including them manufactures a fake gap in the other direction — the same channel-filtering trap covered in our revenue-mismatch piece). A near-exact 2:1 ratio — twice as many GA4 purchases as real orders — is the signature of a duplicate. A modest, inconsistent gap is more likely something else entirely.

DebugView on one real order. Place a test order (or watch a live one) with GA4 Admin → DebugView open. If a single checkout produces two purchase rows in the event timeline, look at the transaction_id parameter on each. Identical transaction_id on both confirms a straightforward double-fire from one mechanism — most likely the trigger problem covered below. Two different-looking values for what you know is the same order confirms the double-install pattern — the values differ specifically because they're coming from two implementations that were never told about each other. Our full DebugView and Realtime walkthrough covers both tools in more depth if you haven't used them for this before.

If you have GA4's BigQuery export connected, this is even easier to confirm at scale: query event_name = 'purchase', group by the ecommerce.transaction_id field — a documented top-level string field in the export schema — and look for any transaction ID with a row count greater than one over the date range you're checking. That gives you an exact duplicate count instead of a sampled impression from Realtime or a single order in DebugView.

Cause #1: Is the Native App Firing Alongside a Manual GTM or gtag Tag?

This is the single most common cause, and Google's own product behavior confirms how common it is. Shopify's native path — Settings → Customer events → Google & YouTube app — fires GA4's purchase event using Shopify's own server-confirmed order data through a sandboxed script. Completely separately, a manually built GTM container or a raw gtag snippet, added by a developer or a previous agency, fires its own GA4 Event tag for purchase, wired to a checkout-success trigger. Both are individually correct implementations. The only problem is that both exist.

This isn't a theoretical edge case — Google publishes a dedicated migration flow for exactly this scenario, because it's common enough to warrant one. When you install or update the Google & YouTube app, it automatically scans the past few weeks of activity to detect tags that may need migrating, then surfaces an in-app notification with a "remove duplicate tags" guide. That guide tells you to search your code for specific identifiers — gtag, UA-, AW-, GTM-, google-analytics.com, googletagmanager.com, googleadservices.com — across four locations: custom pixels (Settings → Customer events), Additional Scripts (Settings → Checkout), checkout.liquid, and the <head> section of theme.liquid. If you've ever installed the Google & YouTube app after a GTM container was already tracking purchases, that overlap is worth checking directly — the migration notification is easy to dismiss without reading closely, and the tooling only offers cleanup, it doesn't force it.

The same conflict doesn't stay contained to GA4, either. Because the Google & YouTube app also feeds Google Ads and Merchant Center, a duplicate purchase tag inflates reported conversions there too — which is a large part of why Shopify merchants running Smart Bidding sometimes see campaigns that look like they're crushing target CPA right up until someone reconciles conversion count against actual order count.

Cause #2: Did a Checkout-Extensibility Leftover Get Left Running?

Shopify's legacy checkout customization path — editing checkout.liquid directly, or pasting a script into Settings → Checkout → Additional Scripts — is being retired on a hard schedule. Plus stores hit their deadline on August 28, 2025; non-Plus stores (Basic, Shopify, Advanced) have until August 26, 2026, after which Additional Scripts, checkout.liquid customizations, and any script tags on the Thank You or Order Status pages stop firing entirely. (If you're reading this well after that date, the cutover has likely already happened on your plan — confirm the current date against Shopify's own help center before acting on any of this.)

The duplicate risk lives in the migration window, not the deadline itself. A developer adds a proper custom pixel via Settings → Customer events → Add custom pixel, using Shopify's Web Pixels API, tests it, confirms purchase fires correctly — and doesn't go back to strip the old Additional Scripts snippet, because nothing visibly breaks. It just keeps firing alongside the new implementation until Shopify's forced cutover eventually kills it, and every order in between gets counted twice. Worth knowing while you're in this territory: Shopify's checkout_completed event fires on the first post-purchase upsell offer page instead of the final Thank You page for stores using post-purchase upsells — not the Thank You page itself. If part of your tracking still assumes a fixed URL for "the confirmation page," an upsell flow can produce timing mismatches worth ruling out separately from a true duplicate.

Cause #3: Is a Thank-You Page Refresh or Bookmark Re-Firing the Event?

A customer refreshes the confirmation page, hits back-then-forward, opens a bookmarked order-status link days later, or clicks back into it from a confirmation email. Any of these re-executes whatever fires the purchase event on that page. This is usually less dangerous than it sounds — the detail most lower-effort write-ups skip is that it depends entirely on where the transaction_id comes from.

If your implementation reads a stable identifier from the page's own order data every time it re-executes — Shopify's order ID, order number, or checkout token, all of which stay identical no matter how many times the page reloads — GA4's dedup rule catches the repeat cleanly and counts the order once, because the transaction_id is literally the same string both times. It becomes a real duplicate specifically when the ID generation isn't stable: a GTM variable built from a timestamp, a session identifier, or an auto-generated event ID instead of the actual order reference. Test this directly rather than assuming: reload the thank-you page a few times with DebugView open and watch whether transaction_id stays identical across reloads or changes each time. If it stays identical, refresh isn't your cause — look at causes #1, #2, or #5 instead.

Cause #4: Do You Have a theme.liquid Leftover Plus an Active App Embed?

On Online Store 2.0 themes, tracking is often installed as a theme app embed (Online Store → Themes → Customize → App embeds). Older themes, or stores that went through an agency rebuild, sometimes also have a GTM or gtag snippet hardcoded directly into theme.liquid's <head> section from before the app-embed approach existed. After a theme upgrade or a developer handoff, it's common for both to end up live simultaneously — nobody explicitly disabled either one, they just accumulated across two different implementation eras.

This is exactly one of the four locations Google's own duplicate-tag cleanup guidance names directly, alongside custom pixels and checkout scripts — theme.liquid is a genuine, documented suspect, not just a checkout-adjacent afterthought. Search it for the same identifiers as cause #1 (gtag, GTM-, googletagmanager.com), and don't stop looking once you've found one instance — the point of this cause is specifically that two independent injection points can both be present at once.

Cause #5: Is One Tag Wired to Both a Custom Event and a Page View Trigger?

A GTM tag can have multiple triggers attached, and it fires if any of them match — that's "or" logic, not "and." A common accretion pattern: the purchase tag was originally built with a Custom Event trigger listening for a dataLayer push (an event: 'purchase' push driven by Shopify's own checkout_completed data) — correct and sufficient on its own. Later, while troubleshooting a suspected tracking gap, someone adds a second trigger to the same tag: a Page View trigger scoped to a URL pattern like contains /thank_you or contains /orders/, meant as a safety net. Both conditions are individually reasonable. On a normal checkout completion, both now match for the same page load — the dataLayer event fires and the URL matches — so the one tag fires twice.

This is the within-GTM sibling of the "two separate tags on the same trigger" pattern our GA4×GTM cross-check chapter flags directly as CROSS-003 — same underlying failure, one tag with a redundant trigger instead of two tags on one trigger. The fix is the same either way: identify which trigger is doing the real work — almost always the Custom Event one, since it reflects Shopify's own confirmed order data rather than a URL guess — and remove the redundant one instead of keeping both "just in case."

Catch a duplicate tag before it doubles your revenue

GA4 can't tell you "you have two tags firing" — it only sees events arrive, not the GTM configuration behind them. NiceLookingData's 44-check GTM audit inspects tag and trigger configuration directly, and when a GA4 audit exists in the same workspace, a dedicated 3-check integration chapter — including a specific duplicate-tag-firing check — compares both products at once. Run a free GTM audit.

The Fixes, Ranked

Work through these in order. The first two remove the duplicate at its source; the last two are structural guardrails worth having regardless of which cause you find.

1. Get transaction_id Right, Then Let GA4's Real Dedup Rule Do Its Job

GA4 does deduplicate purchase events, but the rule is narrower than most people assume: it collapses two purchase events only when they carry the exact same transaction_id value. Google's own ecommerce implementation guide is direct about the failure mode this whole post is about — its own code sample notes that a purchase event which "uses a different transaction ID from the previous purchase event" will not be deduplicated. GA4 isn't comparing order contents, timing, or customer identity. It's doing a literal string match on one parameter.

Two purchase events for the same real Shopify order, sent a second apart, dedupe cleanly if both carry transaction_id: "#1042". The same two events sent from two different tracking implementations — Shopify's checkout token from the native pixel, a Liquid order.order_number or order.name variable from a hand-built GTM tag — sail through as two entirely valid, entirely separate transactions, because as far as GA4 can tell, they are. Pick one stable source for the ID (Shopify's own order identifier, not something derived or reformatted) and make sure every mechanism that fires purchase uses it identically. One more documented gotcha worth knowing while you're in here: never send an empty string as transaction_id — GA4 deduplicates all purchase events carrying a blank transaction_id together, which silently undercounts revenue in the opposite direction. If purchases suddenly drop after you touch this parameter, check for a blank or undefined value before assuming something else broke.

2. Remove the Redundant Install — Pick One Path, Not Both

Decide between the native Google & YouTube app and a custom GTM pixel — not both. Whichever you keep, actively remove the other rather than leaving it dormant "just in case," since a script that's merely unused today can be reactivated by a future theme change or app update without anyone noticing. Use the search-and-remove approach from Google's own migration guidance: hunt for gtag, UA-, AW-, GTM-, google-analytics.com, googletagmanager.com, and googleadservices.com across Customer events, Additional Scripts, checkout.liquid, and theme.liquid. This is exactly what the app's own automatic tag-detection feature is built to surface — worth acting on the in-app notification instead of dismissing it.

3. Scope the Trigger to One Authoritative Signal

Audit every trigger attached to your purchase tag. If you find both a Custom Event trigger (fed by Shopify's own checkout_completed data) and a Page View trigger (a URL-pattern guess) both wired to the same tag, keep the Custom Event trigger and remove the other — a URL match is a weaker proxy for "an order was placed" than the actual dataLayer event, and it's the one more likely to fire in edge cases like a reopened order page. As a second layer of defense against a single trigger accidentally matching twice within one page load, set the tag's Advanced Settings → Tag Firing Options to "Once per Page" rather than "Unlimited." Be clear about what this does and doesn't cover, though: it stops a double-push within one load, not a repeat across a refresh — a refresh is a new page load as far as GTM is concerned, so this setting is a safety net for cause #5, not a substitute for fixing transaction_id from fix #1.

4. Add Once-Per-Order Logic as a Backstop

As extra insurance — especially useful for the refresh and bookmarked-link scenarios in cause #3 — add a client-side check-before-send guard: before pushing the purchase event, check whether you've already recorded this transaction_id in localStorage, and skip the send if so. If you're implementing this inside a Shopify custom Web Pixel specifically, note that the sandbox's storage access isn't the synchronous window.localStorage you'd use in a plain GTM tag — Shopify's Web Pixels API exposes browser.localStorage and browser.sessionStorage as asynchronous, Promise-returning methods, and the sandbox holds no state between separate event invocations. Structure the check as an awaited read before you decide whether to push the event forward, not a synchronous lookup. This won't fix a genuine double-install — two separate implementations don't share a storage key — but it's a cheap, real backstop against the specific failure mode where one mechanism fires more than once for the same order.

What If GA4 Is Running Low Instead of High?

Everything on this page explains GA4 counting a real order more than once. If your problem is the opposite — GA4 consistently reports fewer purchases or less revenue than Shopify's own numbers — that's a structurally different set of causes: consent-mode losses, ad blockers, tags that aren't firing at all, or Shopify sales channels that never touch a browser in the first place. Our companion piece ranks every cause of that version by likelihood. And if neither "too high" nor "too low" quite describes what you're seeing, our symptom-by-symptom GA4 diagnosis tool covers a dozen other patterns using the same most-likely-cause-first approach.

Frequently Asked Questions

Does GA4 automatically prevent duplicate purchase events?

Only in one narrow case: GA4 deduplicates two purchase events that carry the exact same transaction_id value. If your Shopify store sends the same order's purchase event twice with an identical order identifier attached both times — the most common example being a thank-you page refresh where the ID is read from stable order data — GA4 collapses them into one. It does not deduplicate based on order contents, timing, or customer identity, and it will not catch two tracking mechanisms that generate different transaction_id values for the same real-world order, which is the actual cause behind most Shopify duplicate-purchase reports.

Why does Shopify's Google & YouTube app duplicate my GTM purchase tag?

Because Shopify supports both as independent, fully valid tracking paths, and neither is aware the other exists. The Google & YouTube app (Settings → Customer events) fires GA4's purchase event using Shopify's own server-confirmed order data; a manually built GTM or gtag tag does the same thing from a completely separate implementation. If both are live, every real order produces two purchase events with two different transaction_id values, so GA4's dedup rule doesn't apply and both post. Google's own guidance is to migrate to one method and explicitly remove the other — the app even includes an automatic tag-detection feature and a dedicated cleanup guide built for this exact scenario.

Will refreshing the Shopify thank-you page create a duplicate purchase in GA4?

Usually not, as long as whatever fires the purchase event reads a stable identifier — Shopify's order ID, order number, or checkout token — from the page's own order data every time it re-executes. Because the transaction_id is identical on every reload, GA4's built-in deduplication catches the repeat and counts the order once. It becomes a real duplicate specifically when the transaction_id isn't stable — generated from a timestamp, a session value, or anything that changes between loads of the same page. Test it directly: reload the thank-you page a few times with GA4 DebugView open and check whether the transaction_id parameter stays identical across reloads or changes each time.

How do I find duplicate purchase events in BigQuery?

If GA4's BigQuery export is connected, query the events table for rows where event_name = 'purchase', group by the ecommerce.transaction_id field — a documented top-level string field in the export schema — and filter for any transaction_id with a row count greater than one across the date range you're checking. This gives you an exact list of duplicated orders rather than a sampled estimate from Realtime or a single order in DebugView, and it's the fastest way to quantify how much revenue a duplicate has actually inflated before you fix it.

Written by
Ludde Nyström — Founder, NiceLookingData

Analytics consultant turned founder. After years running the same GA4 and GTM audits across client engagements, Ludde built the audit into a product — so the pattern-matching takes a minute, not a meeting. More about Ludde →

Free tool

Run a free GA4 audit.

Connect your Google Analytics 4 property. Our auditor runs 61 checks and gives you an instant health score with a plain-English action plan.

Thanks for reading.