Few things undermine trust in analytics faster than a revenue number that’s too good to be true. You open GA4, see purchases and revenue running 20–40% ahead of what your backend or Shopify dashboard reports, and suddenly every number on the page is suspect. Nine times out of ten the culprit is the same: duplicate purchase events.
Here’s how to confirm it’s happening, find the cause, and fix it for good.
First, confirm you actually have duplicates
Don’t guess — measure. Three quick checks:
- Compare sources of truth. Put GA4 purchase revenue for a date range next to your payment processor or store backend for the same range. A consistent gap (not random noise) points to double‑counting.
- Check for repeated
transaction_ids. In an Exploration, useTransaction IDas a dimension andEvent countas a metric. Any transaction ID with a purchase count of 2+ is a duplicate. If you have the BigQuery export, aGROUP BY transaction_id HAVING COUNT(*) > 1makes this instant. - Watch it live. Complete a test purchase with DebugView open and count how many
purchaseevents land for one order.
A quick myth to kill: GA4 does not reliably de‑duplicate repeated
purchaseevents in standard reports just because they share atransaction_id. A unique transaction ID is essential, but you can’t lean on it to quietly clean up double‑firing. You have to stop the event firing twice.The usual causes (in order of how often I see them)
1. The confirmation page gets re‑loaded
Your
purchaseevent fires on the order‑confirmation page. The customer refreshes it, bookmarks it, hits back, or the browser restores it from cache — and the event fires again. This is the single most common cause.
2. Two GA4 installations
A hardcoded gtag.js snippet and a GA4 tag in GTM both live on the site. Every event — including purchase — gets sent twice. Easy to create during a migration, easy to miss.
3. A trigger that’s too loose
The purchase tag is firing on a condition that matches more than once — an All Pages trigger, a History Change in a single‑page app, or a dataLayer event that gets pushed on every route change.
4. The ecommerce object is never cleared
In single‑page apps especially, the previous ecommerce payload lingers in the dataLayer and gets read again on a later event, re‑sending the same transaction.
5. Client and server both send it
If you’ve added server‑side tagging but left the web container also sending purchase, you get one from each.
The fixes
Send a unique, stable transaction_id on every purchase. Use the real order ID. This won’t de‑dupe for you, but it’s what makes detection — and every fix below — possible.
Guard the event so it can only fire once per order. This is the durable fix. Mark an order as “tracked” the first time and refuse to fire again:
// Fire the purchase push only once per order
var orderId = 'ORDER-12345'; // from your server
var key = 'ga4_purchase_' + orderId;
if (!localStorage.getItem(key)) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null }); // clear any previous payload
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: orderId,
value: 129.00,
currency: 'USD',
items: [ /* ... */ ]
}
});
localStorage.setItem(key, '1');
}
The most robust version of this flags the order server‑side (so the event can’t re‑fire even across devices or cleared storage) — which is exactly how I solved it on a Laravel store where refreshes and retries were inflating revenue: see de‑duplicating GTM purchase events in Laravel.
Tighten the trigger. Fire only on the true conversion condition — a dedicated purchase dataLayer event, not All Pages or a generic route change. In SPAs, make sure the purchase pushes exactly once in your checkout flow.
Remove the duplicate GA4 install. Pick one — GTM or hardcoded gtag — and delete the other. For most teams, keeping everything in GTM is cleaner.
Decide where server‑side purchases come from. If you run sGTM, send purchase from one place only, and use transaction_id for Measurement Protocol de‑duplication.
Verify the fix
Re‑run the same test: one order should now produce exactly one purchase in DebugView, each transaction ID should appear once in your Exploration, and your GA4 revenue should fall back into line with your backend. Give it 24–48 hours and re‑compare the totals.
When the numbers have to be right
Duplicate purchases are usually a symptom of a tagging setup that grew in a hurry — and they’re rarely the only thing off once you start looking. If your GA4 revenue doesn’t match your books and you need it to, this is the kind of problem I fix for a living.