Diagnosing Impression Discrepancies Between GAM and Google Analytics
Ad Manager counts an impression the moment the ad downloads to the user's device, even before the creative finishes loading. Google Analytics counts a pageview when the page tag fires, and, once the tags link, an ad_impression event only after the two tags exchange a shared ID. Every discrepancy between the systems starts from that ordering: the ad server counts at delivery, the analytics tag counts later or not at all, and the failure modes that widen the gap all live between those two trigger points. At one million monthly pageviews and above, the gap stops being a curiosity and becomes a reconciliation problem with a dollar figure attached, because someone eventually asks why the ad server reports more inventory than the analytics tool.
Google's guidance is blunt: Analytics tracks page views, Ad Manager counts ad impressions, and publishers should not compare the two numbers one to one. Google's discrepancy documentation names the documented causes, time zone aggregation, sampling after a hit threshold, passbacks that log two impressions against one pageview, iframes, and it also names the comparison method with the least noise: GAM inventory reports segmented by ad slots. The runbook below orders those causes by test cost, so the cheap explanations die before anyone opens a tag debugger.
What each system actually counts
GAM's standard Impressions metric counts after the ad downloads to the user's device, without requiring the ad content to load fully. GAM separately offers a render-based metric that counts when the creative begins to render, or when the first video frame shows. Two metrics answer two different questions: the standard metric reports how often the server delivered a file, the render metric reports how often a creative started to appear. Pick one before comparing anything, and label the report with the choice.
Upstream of both counters sit the request metrics. Total code served count measures how often the ad server responds to a request, and an empty response produces no code serve. Unfilled impressions equal total ad requests minus total code served count, and Google instructs publishers to exclude them before comparing, because they inflate Ad Manager numbers. A comparison that puts GA numbers against GAM ad requests starts with an inflated numerator, and no downstream tuning fixes that.
One more counting rule matters when you reconcile from files rather than from the UI. Data Transfer counts a code serve for every served impression, while reports count one per request. Build a reconciliation on Data Transfer rows, then check it against UI totals, and the counting rule alone creates a gap that no time zone fix will close. Decide once, in writing, which export the reconciliation uses.
The GA4 side behaves better than its Universal Analytics predecessor, but only when the tags link. When you link Ad Manager to Google Analytics, the two tags communicate through shared IDs, query_id on web and ad_event_id in apps, and GA4 collects ad_impression, ad_click and ad_query events automatically. Google's instructions for linking the tags cover the setup, and the ID handshake eliminates the discrepancy pattern publishers fought in Universal Analytics. Two rules protect the data: never send ad_impression manually through the Measurement Protocol, because it duplicates the events the link already collects, and note that manual sends of that event exist only for app streams. On web, the linked tags are the only source.
The names of the three collected events map cleanly onto GAM counters. GA4 ad_query events fire on ad requests, ad_impression events fire on impressions, and ad_click events fire on clicks. That symmetry means the unfilled gap should appear on both sides: GAM reports it as total ad requests minus total code served count, and GA4 shows the same shortfall as ad_query minus ad_impression. When both systems agree on the shortfall, delivery is healthy and the remaining question is trigger alignment. When GA4 logs no ad_query events at all, the tag link is dead, and nothing downstream means anything until someone repairs it.
Video adds its own trigger offsets. The render-based metric counts a video impression when the first frame shows, which sits later in the player lifecycle than the standard impression at download. Players inside cross-domain iframes break the ID handshake the same way display placements do, and ad blockers that strip third-party delivery leave GAM impressions with no matching analytics event. Reconcile video line items by placement, and expect the render metric to trail the standard one by the distance between download and first frame.
Pageviews and impressions also measure different objects, because a pageview belongs to a page while impressions belong to slots. One pageview can legitimately carry several impressions, and passback chains multiply the impression side further. The comparison that carries diagnostic value is therefore not pageviews against impressions: it is GA4 ad_impression events against GAM impressions, slot by slot, after you exclude unfilled impressions on the GAM side.
The documented causes, in the order worth testing
Each documented cause has a cheap test. Work them in this order, because every test costs minutes and eliminates a whole category before tag forensics consume the afternoon.
-
Time zone aggregation. GAM aggregates on its network reporting time zone, GA4 aggregates on the property time zone, and a mismatch splits each day at two different midnights. Daily rows drift, and weekend peaks land on different dates. Pull full weeks that contain both midnights, or align the time zones before exporting anything.
-
Sampling after the hit threshold. Once hit volume crosses the threshold, Analytics samples report data, and sampled totals estimate rather than count. The signature is scaling: quiet days reconcile, heavy days drift, with identical configuration on both sides.
-
Passbacks. A passback chain logs two GAM impressions against one pageview: the partner tag serves, declines the request, and passes it back, then a house or fallback line item serves again. GA4 records the pageview once. A slot whose GAM impressions run near twice its GA4 ad_impression events carries a passback chain, not a tracking fault.
-
Iframes. An ad serving inside an iframe still counts in GAM, while the pageview lives in the parent frame and the shared ID handshake has to cross the frame boundary, so GA4 can drop the ad_impression event entirely. Out-of-page formats and iframe-wrapped placements deserve their own reconciliation group, because they fail by a different mechanism than in-page slots.
-
Ad blocking. Ad blocking software can prevent third-party delivery after Ad Manager has already counted the impression, and one JavaScript snippet can load while the other does not. Direction varies by blocker and by page: some pages count in GAM and miss in GA4, others do the reverse. A gap that tracks your ad block rate across devices is a blocker signature, not a tag bug.
-
Unfilled impressions. Requests that return an empty response never produce a code serve, but request-side totals still include them. Unfilled impressions equal total ad requests minus total code served count. Exclude them before comparing, exactly as Google instructs, because they inflate the Ad Manager side and ad request totals headline most delivery dashboards.

Six causes, six cheap tests, and none of them requires a code change. Any gap that survives all six needs slot-level forensics, which is where the runbook goes next.
Three patterns that recur in practice
The causes rarely show up alone, and three combinations explain the gaps that keep coming back.
The iframe slot that never matches. An in-read placement wrapped in a friendly iframe serves, renders, and counts in GAM, but GA4 never records its ad_impression event because the handshake fails at the frame boundary. The site-level gap looks alarming; the slot-level report isolates a single placement while everything else reconciles. The fix is a separate reconciliation group for that slot, not a global investigation.
The fallback that double counts. A partner passes back on a sports section, the house line item serves, and GAM logs two impressions against a single GA4 pageview. The section reconciles at nearly two to one, which is the passback signature rather than evidence of data loss. Renaming the house line item so it reports as its own row turns an unexplained ratio into an explained one.
The gap that moves by itself. Tags unchanged, configuration unchanged, yet the site-wide gap widens month over month. Traffic mix moved: device and country segments locate the drift, and when the growth concentrates where ad blocking runs highest, the tags are innocent. Compare the same segments in both systems before touching any code.
A runbook in five passes
Run the passes below on one full week of data, in order. Each pass either closes the gap or narrows it to a handful of slots small enough to inspect by hand.
-
Align the calendar. Check the GAM network reporting time zone against the GA4 property time zone before you export anything, then pull identical date ranges that cover full weeks.
-
Clean the GAM side. Pull the inventory report segmented by ad slot, the comparison Google calls the most precise. Exclude unfilled impressions. Choose between the standard impressions metric and the render-based metric, apply the same choice to every slot, and record the choice in the report name.
-
Clean the GA4 side. Pull ad_impression events from the linked integration, not sessions and not pageviews, and confirm the tags share query_id on web or ad_event_id in apps. If the events never arrive, the link is the fault, and repairing it comes before interpreting anything else. A slot that logs ad_query events but no ad_impression events fails between response and render, so check the iframe, the creative, and the blocker before blaming the tag.
-
Reconcile per slot. Sort slots by absolute gap, then classify the worst offenders: a ratio near two GAM impressions for every GA4 event signals a passback chain, a gap concentrated at day boundaries signals time zones, a missing event on iframe placements signals the handshake, and drift that scales with ad block rate signals blocking.
-
Bring in third parties last. Every vendor comparison needs the same three answers: what triggers the count, which time zone, and which universe of traffic. Google's checklist for third-party discrepancies walks those questions in order, and it doubles as the agenda for any vendor dispute call.
Write the output down: metric chosen, exclusions applied, slot-level gaps, cause per slot. Next month's reconciliation inherits the notes, and the causes you already assigned stop consuming review time. Agencies running this for publisher clients should deliver the notes as the artifact, because the artifact outlives the call.
Three false leads waste the most reconciliation hours. Comparing GA4 sessions to GAM impressions produces a meaningless ratio, because sessions aggregate users while impressions aggregate slots. Comparing a sampled GA4 exploration to an unsampled GAM report manufactures a difference out of the sampling, not the delivery. Comparing ad requests from one GAM report surface against code served from another mixes two columns that count different events by design. Each false comparison produces a gap that looks exactly like a real discrepancy, which is what makes them expensive.
Third-party counts follow the same rules
Demand partners and verification vendors count at their own trigger points, and every comparison fails until the triggers match. A partner counting rendered impressions trails GAM counting downloaded ones by exactly the distance between download and render. A partner that filters invalid traffic trails an unfiltered GAM view for the same structural reason. Before you open any vendor ticket, answer four questions for both systems: the trigger that increments the count, the time zone, the universe of included traffic, and the deduplication rule. Most disputes end at question one.

File-based comparisons add one trap from earlier: Data Transfer counts a code serve for every served impression, while reports count one per request. Two teams pulling what looks like the same GAM data from different surfaces will disagree, and each will call the other wrong. Name the export in the reconciliation notes every time.
Clicks deserve a separate warning. A click requires a post-impression action, and trackers intercept that action differently: redirect chains, pop-up blockers, and accidental-click filters each remove clicks after the impression. Treat the GA4 ad_click event as a directional signal, and reserve click-level disputes for fraud investigations rather than impression reconciliation.
Instrument the reconciliation, then stop repeating it
A reconciliation you run once is an investigation; a reconciliation you run monthly is infrastructure. Automate the pulls: the GAM reporting API for slot-level impressions, the GA4 Data API for ad_impression events, both landing in one table keyed by slot and date. Alert on drift per slot rather than on the site total, because the site total hides the one broken placement. Set the cadence around the payment cycle, not the calendar: run the slot comparison early enough in the month that a finding can still affect the invoice, and archive the notes with the month's delivery exports.
We operate an ad network, an SSP and an agency in one company, and we work with thousands of publishers, each above one million monthly pageviews, so this reconciliation runs on our side of the delivery too. OnlyMedia's reporting console refreshes delivery stats every ten minutes: CPM, fill rate, ad requests, pageviews, revenue, countries, devices and video detail, with API access, so a drifting slot surfaces within the hour. Teams that join request logs to reporting use our API documentation for OpenRTB and VAST as the reference for those integration points.
The payment cycle is where reconciliation pays for itself. We pay publishers Net 30, CPM based, no chasing and no sliding terms, so a delivery fault that surfaces on day 25 becomes a payment question on day 31 unless someone reconciles first. The publisher side of those terms, from the ten minute reporting console to Net 30 payments, sits on our publisher monetization pages, and the demand side, our curated programmatic demand network and its payout terms, lives on the homepage.
What a settled reconciliation looks like
After the passes, a residual gap remains, and the goal is a gap you can explain, not a gap of zero. Every surviving point of difference should map to a named cause: this slot serves in an iframe, this slot runs passbacks, this segment carries heavy ad blocking. A gap with no assigned cause is the only actionable kind, because it points at a tag fault, a broken link, or a misconfigured report rather than at arithmetic.
Re-run the slot-level comparison monthly, and re-classify instead of re-investigating. Last month's causes answer this month's questions in minutes. That cadence, more than any single fix, keeps Ad Manager and Google Analytics describing the same inventory.