WooCommerce CRO Technique

How to find and fix duplicate GA4 purchase events in WooCommerce

This technique audits every route that can emit a GA4 purchase on a WooCommerce order, then removes duplicate emitters, ties the trigger to the real order-received state, and makes transaction_id stable and non-empty. It helps when GA4 purchase counts or revenue look too high versus WooCommerce, when the same order ID appears more than once in an audit, or when a thank-you-page refresh retriggers tracking.

Summary

Bottom Line: To fix duplicate GA4 purchases on WooCommerce, make sure exactly one implementation emits purchase, fire it only on the real WooCommerce order-received state, and always send one non-empty Woo order ID or order confirmation number as transaction_id.

  • GA4 only deduplicates web-stream purchases when the same non-empty transaction_id is sent; if you send transaction_id="", GA4 treats all blank IDs as the same transaction and deduplicates them together.
  • Duplicate WooCommerce purchases usually come from more than one emitter path: for example a plugin plus GTM, a hard-coded Google tag plus GTM, or a thank-you-page trigger that refires on refresh.
  • On classic templates, the key WooCommerce purchase hooks are woocommerce_before_thankyou and woocommerce_thankyou; on block themes, old thankyou.php customisations may stop running unless the Order Confirmation template is switched back to the legacy placeholder.
  • BigQuery is the cleanest audit surface because GA4 exports event_name, repeated event_params, user_pseudo_id, and ecommerce fields including ecommerce.transaction_id and ecommerce.purchase_revenue.
  • If you also send purchase data from server-side or another Google source, GA4 deduplicates on transactionId within the same property and keeps the first instance it receives, so mismatched client/server payloads are still a data-quality problem even when the duplicate does not create a second recorded order.

How To Implement

  • Take a baseline before you touch anything

    In GA4, save the current figures from the Ecommerce purchases or Monetization view for the same date range you use in WooCommerce reporting; in WooCommerce, save the matching Revenue report view. If GA4 is ahead of WooCommerce on the same basis, duplicated purchase firing is one plausible cause to investigate, but keep the before-state so you can confirm the fix narrowed the variance rather than simply suppressing tracking. Measurement note: use the same date range and wait for normal GA4 processing before judging the outcome.

  • Audit repeated and blank transaction IDs first

    If BigQuery is linked, query the events_* tables for event_name = 'purchase' and group by ecommerce.transaction_id; flag any blank IDs and any transaction ID appearing more than once. The GA4 export explicitly exposes event_name, event_params, user_pseudo_id, ecommerce.transaction_id, and ecommerce.purchase_revenue, which makes this the cleanest raw audit surface. Remember that GA4 daily export tables can update for up to three days after the event date, and same-day data may also appear in events_intraday_YYYYMMDD.

     SELECT
     ecommerce.transaction_id,
     COUNT(*) AS purchase_rows,
     COUNT(DISTINCT user_pseudo_id) AS users,
     MIN(TIMESTAMP_MICROS(event_timestamp)) AS first_seen,
     MAX(TIMESTAMP_MICROS(event_timestamp)) AS last_seen,
     SUM(ecommerce.purchase_revenue) AS revenue
     FROM `project.analytics_PROPERTY_ID.events_*`
     WHERE _TABLE_SUFFIX BETWEEN '20260501' AND '20260531'
     AND event_name = 'purchase'
     GROUP BY 1
     HAVING ecommerce.transaction_id IS NULL
     OR ecommerce.transaction_id = ''
     OR COUNT(*) > 1
     ORDER BY purchase_rows DESC, transaction_id;
  • If BigQuery is not available, use GA4 Explore plus DebugView rather than guessing

    GA4 exposes transaction_id as the Transaction ID dimension, and ecommerce data flows into reports, Explorations, BigQuery, and the Data API. Build a free-form exploration filtered to purchase, add Transaction ID and Event count, and look for blanks or suspicious reuse. Then use DebugView for live confirmation on a test order.

  • Reproduce the problem with one controlled test order

    Open GTM Workspace → Preview to launch Tag Assistant, place a test order, land on the order confirmation page, and then refresh that page once. Google’s own conversion docs warn that confirmation-page reloads can fire the conversion again if the tag is attached directly to that page, and Tag Assistant shows which tags fired and in what order. If you see a second purchase-like send after refresh, you have a real refire problem rather than a reporting-only anomaly.

  • Reduce the store to one purchase emitter path

    Check all of the usual WooCommerce routes: – WooCommerce → Settings → Integrations if you use the official Google Analytics integration. WooCommerce’s own plugin FAQ warns that duplicate Analytics code causes conflicts, and its documentation warns that multiple tracking-code instances on the same page can cause issues. – Google Tag Manager → Workspace → Tags for any GA4 Event tag, Google Ads conversion tag, or Custom HTML tag that also sends purchase. Google’s docs explicitly warn that gtag.js and GTM running simultaneously can conflict, and that the same Google tag configured two or more times on a page can create duplicate data or mixed settings. – Theme and snippet code such as functions.php, a code-snippets plugin, header/footer injectors, or a child-theme WooCommerce override. On classic themes, the thank-you template can be overridden at yourtheme/woocommerce/checkout/thankyou.php, and that template runs woocommerce_thankyou. Keep one authoritative purchase path and remove or disable the others.

  • Tie the trigger to the real WooCommerce order-received state, not to a generic page view rule

    For classic shortcode checkout or classic templates, the safest Woo-native checkpoints are the thank-you hooks and Woo conditional functions: woocommerce_before_thankyou, woocommerce_thankyou, is_order_received_page, or is_wc_endpoint_url( 'order-received' ). Do not rely on a loose URL match such as “page path contains /checkout/”, and do not hard-code the endpoint slug because WooCommerce lets merchants change checkout endpoints under WooCommerce → Settings → Advanced.

  • Make transaction_id dynamic, unique, and never blank

    Google’s ecommerce docs require transaction_id on purchase, describe it as the unique identifier of a transaction, and state that it helps avoid duplicate purchase events. Google’s GA4 help also says the ID must be unique for each order and warns specifically not to send an empty string because GA4 will deduplicate all blank IDs together. In WooCommerce terms, the practical choice is the order ID or order confirmation number returned by the actual order object, not a static value and not a client-generated guess.

  • Handle Cart & Checkout Blocks and block themes as a separate case

    In block themes, go to Appearance → Editor → Templates → WooCommerce → Order Confirmation and inspect whether the site is using the modern Order Confirmation template or the legacy placeholder. WooCommerce documents that if you switch the template back to the Order Confirmation Block placeholder, the site will use the legacy template again and “custom code on that template” will run; without that switch, an old thankyou.php customisation may no longer control purchase tracking. This is a common source of confusion after a theme or checkout migration.

  • If you run client-side and server-side purchase tracking together, deduplicate intentionally

    Google’s Data Manager API documentation says GA4 deduplicates the same event across sources within the same property using transactionId, and it keeps the first instance received. That means you should either emit purchase from one side only, or make sure both sides use the same transaction ID and that you understand which payload is likely to arrive first. If client and server use different IDs, GA4 has nothing to reconcile and both can count separately.

  • Retest after the fix and check for refresh refires

    Re-run the same test order in Tag Assistant and GA4 DebugView, refresh the order confirmation page, and confirm you do not see a second purchase send. If you use Datalayer for WooCommerce, its Ecommerce Advanced settings include “Trigger Purchase only once”, which exists specifically to stop a thank-you-page refresh from sending the purchase again; that is a sensible benchmark for how your final behaviour should look, even if you are not using that plugin.

  • If you build a one-off WooCommerce audit tool, make it HPOS-safe

    WooCommerce says HPOS uses dedicated order tables and has been the stable default for new installs since WooCommerce 8.2, and its developer docs advise using wc_get_orders / WC_Order_Query instead of direct database or WordPress post-table queries. That matters if you create an admin checker that loops through orders and verifies tracking meta or thank-you-page output.

How To Measure

The primary KPI is duplicate-transaction rate: the share of raw purchase rows that are either blank-ID purchases or repeated transaction IDs for the same audit window. The cleanest read is in BigQuery on event_name = 'purchase', grouped by ecommerce.transaction_id; if BigQuery is not available, use GA4 Explore with the Transaction ID dimension, Event count, and a filter for purchase, then use DebugView for live QA.

The secondary KPI is post-fix GA4-vs-Woo variance. Read GA4 in the Ecommerce purchases / Monetization reporting layer and compare it with the matching date range in WooCommerce → Analytics → Revenue. Success looks like this: every completed Woo order resolves to one purchase, blank transaction IDs fall to zero, repeated transaction IDs stop appearing in your audit, and GA4 moves materially closer to WooCommerce after ordinary reporting lag has passed. GA4 ecommerce data can take roughly a day to appear in standard reports, and BigQuery daily tables can update for up to three days after the event date, so do not judge the fix from a same-day partial read.

The most useful segments are payment method, device category, and checkout implementation path if you changed one route before another—for example classic thank-you code versus a block-based Order Confirmation template. On WooCommerce stores, duplicates often cluster by payment gateway because gateway-specific thank-you hooks or scripts can add another purchase emitter. The relevant guardrails are: purchase capture rate must not drop below real Woo orders, AOV and RPV should not collapse because the purchase payload lost value or items, refund handling must still reconcile to the same transaction_id, and blank-ID rate must stay at zero.

Pitfalls

  • Myth: “GA4 will automatically save me from duplicate purchases.” Not fully. GA4’s purchase deduplication rule is documented for web streams, it depends on the same non-empty transaction_id, and it does not rescue you from blank IDs, reused IDs, or mismatched client/server IDs.
  • Mistake: fixing the visible GTM tag but leaving a plugin or hard-coded tag live. WooCommerce’s own plugin docs warn that duplicate Analytics code and multiple tracking-code instances on the same page cause issues, while Google warns that multiple tracking methods or repeated on-page Google tag configuration can create duplicate data or mixed settings.
  • Mistake: triggering on a generic thank-you URL rule instead of the real WooCommerce completion state. WooCommerce exposes dedicated order-received conditionals and hooks, and it also allows checkout endpoints to be changed under WooCommerce → Settings → Advanced. A loose page-view rule is easier to break during a checkout customisation, redirect, or endpoint rename.
  • Edge case: a block-theme migration can make an old thankyou.php fix silently stop working. WooCommerce’s Order Confirmation template documentation is explicit that switching back to the legacy placeholder is what makes custom code on that old template run again.
  • Mistake: comparing incomplete data and calling the fix done. Standard GA4 ecommerce reporting is not instant, and BigQuery daily export tables can still be updated for up to three days after the event date. A same-day “looks fixed” check is often a false positive.

Examples

FAQs

Sources & Further Reading

  • Set up a purchase event | Google Analytics – — Official GA4 ecommerce implementation guide covering where a purchase event belongs and how to validate it. Date: 9 October 2024.
  • Events | Google Analytics – — Official event reference showing that transaction_id is required on purchase and helps avoid duplicate purchase events. Date: 8 June 2026.
  • [GA4] Minimize duplicate key events with transaction IDs – — Official GA4 help article explaining web-stream purchase deduplication and warning that transaction_id="" deduplicates all blank purchases together. Date: no publication date shown on page.
  • Send events | Data Manager API – — Official Google documentation explaining cross-source deduplication by transactionId and that GA4 keeps the first instance received. Date: 27 May 2026.
  • BigQuery Export schema – — Official schema reference for GA4 export tables, including events_YYYYMMDD, events_intraday_YYYYMMDD, event_params, user_pseudo_id, and ecommerce.transaction_id. Date: no publication date shown on page.
  • Monitor events in DebugView – — Official GA4 debugging guide for validating live event collection during QA. Date: no publication date shown on page.
  • Preview and debug containers – — Official GTM/Tag Assistant guide showing how to inspect which tags fired and in what order. Date: no publication date shown on page.
  • Google Analytics for WooCommerce Documentation – — Official WooCommerce extension documentation warning that multiple tracking-code instances on the same page can cause issues. Date: live documentation page; page date not shown in opened lines.
  • Google Analytics for WooCommerce plugin page – — Official plugin listing stating that duplicate Google Analytics code causes conflicts in sales tracking and that settings live under WooCommerce → Settings → Integrations. Date: live plugin page; page date not shown in opened lines.
  • Google tag management – — Official Google help article warning that the same Google tag configured multiple times on the same page can create duplicate data or mixed settings. Date: no publication date shown on page.
  • Use the Google tag for Google Ads conversion tracking – — Official Google help article warning about multiple tracking methods such as gtag.js and GTM together, and explaining that the event snippet belongs on the conversion page. Date: no publication date shown on page.
  • Automatically collected events – — Official GA4 help article warning that manual plus automatic event collection can create duplicates. Date: no publication date shown on page.

Want us to implement this for you?

We run measured CRO consultancy for WooCommerce. If you want help prioritising, testing & implementing these improvements, tell us about your store.

Book Pilot

About This Page

  • Written By: Eliot Webb – Founder & WooCommerce CRO Consultant
  • Last Reviewed: 17 Jun 2026
  • Last Updated: