The double-purchase bug, and how to make sure it never bites you
You build a clean GA4 setup. Purchases fire. Numbers look great. Two weeks later your client emails you a screenshot: GA4 says 240 transactions for the week, Shopify shows 120, and the entire conversion-rate dashboard is suddenly meaningless.
The cause is almost always the same. Customers refresh the thank-you page. They use the back button to check the order. They click the link in the confirmation email and land back on the receipt. Each visit re-fires the purchase event with the same transaction ID. GA4 counts each as a fresh sale. Your CRM, which writes once on actual order, doesn't.
The fix is mechanical: block any purchase event whose transaction ID you've already seen.
I do this with the Transaction ID Logger template that Simo Ahava maintains in the GTM gallery. It stores the most recent transaction ID in a first-party cookie and exposes a comparison variable you can wire into a blocking trigger. Setup is straightforward — install the template, point a variable at the cookie key, build a regex-table that returns true when the incoming transaction ID matches the stored one, and use that as an exception trigger on your GA4 purchase tag.
If you're already running server-side GTM, there's a cleaner version of the same fix. Stape maintains a Duplicate Transaction Checker variable for sGTM that does the comparison on the server instead of relying on a browser cookie. Source on GitHub, installable from the sGTM template gallery in two clicks. Two reasons I prefer it once a client is on sGTM: the deduplication now blocks duplicates for every destination tag (GA4, Google Ads, Meta CAPI, all of them) from a single point, instead of needing the cookie-comparison wired into each web-side tag; and the storage isn't dependent on a cookie that might be missing, ITP-scoped, or domain-mismatched. The trade-off is needing to be on sGTM in the first place — for clients still purely client-side, Simo's template remains the right move.
Two things I see go wrong with the client-side cookie approach in implementations I audit:
Testing once and shipping. The bug only shows up when a real visitor revisits the receipt — your QA-mode test in Tag Assistant won't catch it. The proper test: fire a real test purchase, refresh the confirmation page three times, and look at DebugView. Three purchase events with the same transaction_id means the trigger isn't blocking. One means you're done.
Cookie domain mismatch. If the cookie is scoped to www.yoursite.com but the customer lands on the receipt via the apex (no www), the cookie isn't readable on the second visit and the block never fires. Always set the cookie domain to the apex with a leading dot, and verify it in browser devtools after a test purchase.
Once it's live, your purchase count stops being a moving target. Everything downstream — ROAS, attribution, channel performance — stops lying to you in proportion to whichever campaigns happen to send people back to the receipt page most.
If you find this in someone else's account during an audit, fix it before you touch anything else. Nothing else matters until the purchase count is real.