Someone compares GA4 revenue to the orders table and the numbers do not match. It is one of the most common analytics support requests, and the cause is nearly always one of five things.
A note before the list: GA4 and your database will never match exactly. Consent, ad blockers and network failures guarantee a gap. The realistic target is GA4 reporting somewhat under your database, consistently. If GA4 is reporting more revenue than you actually took, that is not sampling noise — that is a bug.
Cause 1: refresh re-fires the purchase event
The most frequent cause of inflated revenue.
The purchase event fires on the confirmation page. The customer refreshes, or bookmarks it, or returns via browser history. The event fires again. One order, several purchases.
The fix is two layers:
const key = `purchase_${transactionId}`;
if (sessionStorage.getItem(key)) return;
sessionStorage.setItem(key, "1");
plus a stable transaction_id so GA4 deduplicates server-side. Use both — sessionStorage does not survive a new tab.
Symptom: GA4 revenue is higher than the database, and GA4 transactions exceed order count.
Cause 2: missing or unstable transaction_id
GA4's deduplication depends entirely on transaction_id. If it is absent, every purchase event counts. If it is generated with Date.now() or a random value, each re-fire produces a new id and dedup never engages.
It must be your order number, issued by the server. This also makes reconciliation possible at all — without a shared key you cannot compare row by row.
Symptom: duplicate purchases despite a dedup guard.
Cause 3: firing before server verification
If the event fires when the payment gateway's client-side handler reports success, you capture transactions that never completed: failed signature verification, gateway timeouts after the callback, cancelled captures.
Fire purchase only after your server has verified and persisted the order, using the order number the database issued.
Symptom: GA4 shows transactions with no matching order row.
Cause 4: consent blocking the tag
Now the leading cause of under-reporting.
With Consent Mode configured and analytics_storage denied, GA4 tags do not write cookies and, if you have set them to require consent explicitly, may not fire at all. Every user who declines is missing from revenue.
This is expected and correct — but it must be understood, or you will chase a "bug" that is your privacy implementation working.
Quantify it: measure your accept rate and check whether the shortfall is roughly proportional. If 30% decline and GA4 is 25–30% low, the system is behaving as designed.
Symptom: GA4 consistently and proportionally below the database.
Cause 5: currency and tax mismatch
Three variants:
Currency mismatch. You price in USD but charge through a gateway in INR, and send one figure with the other's currency code. GA4 converts using the property currency and the totals diverge.
Tax and shipping inconsistency. GA4's
valueshould be the total the customer paid. If you sometimes send the pre-tax subtotal, revenue sits low by exactly the tax rate.Discounts applied twice, or not at all. The value is computed client-side from list prices while the server charges a coupon-adjusted amount.
Send the server-computed total — the number you actually charged — never a client-side recalculation.
Symptom: a consistent percentage offset rather than a random one. A consistent offset is a formula error; a random one is a firing error.
A reconciliation checklist
Take one full day, at least 48 hours old so processing has completed:
Export GA4 purchases with
transaction_idand revenue (the BigQuery export is ideal).Export orders for the same window.
Join on order number and compare.
Then classify:
In GA4, not in the database → firing before verification, or duplicates
In the database, not in GA4 → consent, ad blockers, or a tag failing to fire
In both, different values → currency, tax or discount handling
Each bucket points at exactly one of the causes above.
Monitoring for drift
Once reconciled, keep it reconciled. Implementations rot — a checkout refactor silently changes the payload and nobody notices for a quarter.
A weekly automated comparison of GA4 and database revenue, alerting past a threshold
An alert if GA4 transactions ever exceed order count, which is always a bug
Re-run the reconciliation after any checkout or payment change
Trust in analytics is slow to build and instant to lose. A number nobody believes is worse than no number, because it still gets used in decisions — just with an argument attached.