GTM 11 min read

Mixing GTM Snippets with gtag('config')? Google Is Forcing You Onto gtag.js — Here's the Migration Path

If your site loads a GTM container alongside a hardcoded gtag('config') call, Google is now silently rerouting your tracking through gtag.js — and most sites won't notice until attribution breaks.

A
Ashwani Bhasin
·

If your site loads a GTM container alongside a hardcoded gtag('config', 'G-XXXXXXX') call, Google is now silently rerouting your tracking through gtag.js. Most sites won’t notice until attribution breaks, cross-domain linking stops working, or consent mode fires in the wrong order and starts dropping events. I’ve seen this on three client audits in the last month alone, and the pattern is always the same: a WordPress site with Site Kit installed, a GTM container added later by the agency, and nobody realised both were running config commands against the same measurement ID.

Here’s what’s actually happening, how to find it on your own properties in about ten minutes, and the three ways out depending on how your stack is built.

What Google Actually Changed

The short version: when Google’s loader detects both a GTM container (gtm.js) and a direct gtag('config', ...) call on the same page targeting the same GA4 property, it now forces the page onto the gtag.js loader and treats your GTM container as a secondary tag source rather than the primary one. Previously these two were allowed to coexist in a messy but functional way, with each one firing its own config and GA4 deduplicating hits server-side (badly, but it worked).

The new behaviour means a few things in practice:

  1. The gtag.js library gets injected into the page even if you never referenced it directly, because your gtag('config') call pulls it in.
  2. GTM’s GA4 Configuration tag no longer “owns” the initial pageview on hybrid pages. The gtag.js config call fires first, sets the session, and GTM’s config tag effectively becomes a no-op or, worse, overwrites fields.
  3. Consent Mode signals get evaluated against whichever loader fires first. If gtag.js initialises before GTM applies your consent defaults, you get a window of unconsented hits leaking out.
  4. Cross-domain linking parameters set in your GTM GA4 Config tag can be stripped or ignored because gtag.js already decorated outbound links with its own (empty) linker config.

PPC Land and Search Engine Roundtable both flagged this change, but neither published a working remediation path. The guidance from Google amounts to “pick one implementation method,” which is correct and useless if you’re the one who has to figure out which hardcoded snippets are breaking your attribution.

Why Google Did This

Hybrid implementations have been a support nightmare for Google for years. When a merchant complains that their ecommerce events are duplicating or their conversions dropped 40% overnight, the first thing any Google rep has to do is figure out whether gtag.js, GTM, or both are firing. By collapsing hybrid pages onto a single loader, Google removes an entire class of ambiguity from their diagnostics. It also lets them push consent mode v2 and the new unified google_tag concept harder, because the loader hierarchy is now predictable.

The side effect is that thousands of sites running “GTM with a backup gtag snippet” (a surprisingly common pattern) now have a tracking layer that behaves differently than it did last quarter.

Auditing Your Site in 10 Minutes

Don’t trust Tag Assistant for this first pass. It will tell you GTM is firing and gtag is firing and give both green checks. What you want to know is whether there’s a gtag('config', ...) call anywhere on the page outside of what GTM injected.

Step 1: Check the raw HTML

View source (not inspect — view source, Ctrl+U) on your homepage, a product page, and your checkout. Search for:

gtag('config'
gtag("config"
googletagmanager.com/gtag/js
G-
UA-

Any direct reference to gtag/js?id=G-XXXXXXX in the raw HTML means something is loading gtag.js outside of GTM. That something is your problem.

Step 2: Check the network tab

Open DevTools, go to Network, filter by collect, and reload the page. You’re looking for requests to google-analytics.com/g/collect or analytics.google.com/g/collect. Click one and check the request payload for the _s parameter (hit sequence number) and the cid (client ID).

If you see two separate cid values in the first few hits of a page load, you have two instances of GA4 initialising with different client IDs. That’s the clearest sign of a hybrid implementation gone wrong.

Step 3: Grep your theme and plugins

On WordPress, SSH in or use the file manager and run:

grep -r "gtag('config'" wp-content/themes/
grep -r "gtag(\"config\"" wp-content/themes/
grep -r "googletagmanager.com/gtag/js" wp-content/
grep -rn "G-[A-Z0-9]\{10\}" wp-content/themes/ wp-content/plugins/

On Shopify, you need to check:

  • theme.liquid (search for gtag, googletagmanager, G-)
  • checkout.liquid if you’re on Plus
  • Any custom pixels you’ve added under Settings → Customer events
  • The native GA4 integration under Settings → Apps → Google

I’ve found rogue config calls hiding in:

  • Old UA snippets that were “updated” to GA4 by someone pasting a measurement ID into the wrong block
  • Theme child files that override the parent’s analytics include
  • WordPress plugins like MonsterInsights, Site Kit, GA Google Analytics, and WP-Rocket’s optional analytics proxy
  • Shopify theme sections added by previous developers for “conversion tracking enhancements”
  • Google Ads conversion snippets that quietly include a config line

Step 4: Check GTM for duplicate configs

Inside your GTM container, filter tags by type. If you have more than one GA4 Configuration tag (or Google Tag, in the newer terminology) targeting the same measurement ID, you have a second kind of hybrid problem that lives entirely inside GTM. This usually happens when someone imports a template or clones a container.

The Three Migration Paths

There is no universally correct answer here. Pick based on who maintains the site, how much custom event logic you have, and whether you need server-side tagging.

PathBest ForEffortFlexibilityCommon Risk
Fully GTM-managedAgencies, marketing teams, sites with 10+ custom eventsMediumHighRequires removing all hardcoded snippets
Fully gtag.js-managedSmall sites, developer-owned stacks, simple event setsLowLowHard to extend without code deploys
Google Tag + GTMSites needing Google Ads conversion linker + GTM eventsMediumHighEasy to misconfigure and double-count

Path 1: Fully GTM-Managed

This is what I recommend for 80% of ecommerce and content sites. Remove every gtag call from your theme, plugins, and page templates. GTM becomes the single source of truth.

Steps:

  1. In GTM, confirm you have one Google Tag (or GA4 Configuration tag) set to fire on All Pages, with Send a page view checked.
  2. Remove any <script src="...gtag/js?id=G-..."> tag from <head> in your theme.
  3. Remove any inline window.dataLayer = window.dataLayer || []; function gtag(){...} gtag('config', 'G-...') blocks.
  4. If you use Site Kit on WordPress, disconnect the Analytics module (not the whole plugin) so it stops injecting its own snippet. Site Kit will still work for Search Console and PageSpeed data.
  5. If you use Shopify’s native GA4 connection, disconnect it under Settings → Apps → Google and let GTM handle everything. See our Shopify service if you need help with the custom pixel work this requires post-Checkout Extensibility.
  6. Publish GTM. Verify in DebugView that pageviews, scrolls, and your ecommerce events all have the same client_id and session_id across events.

Path 2: Fully gtag.js-Managed

Use this when the site has fewer than five events, no marketing team to speak of, and the developer wants control at the code level. Common for SaaS marketing sites and small B2B.

<!-- In <head>, before any other analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('consent', 'default', {
    'ad_storage': 'denied',
    'ad_user_data': 'denied',
    'ad_personalization': 'denied',
    'analytics_storage': 'denied',
    'wait_for_update': 500
  });
  gtag('config', 'G-XXXXXXX', {
    'send_page_view': true,
    'cookie_flags': 'SameSite=None;Secure'
  });
</script>

Then delete your GTM container entirely, or at minimum unpublish it and remove the container snippet from the site. Running both at once is exactly the problem you’re trying to fix.

Path 3: Google Tag + GTM (The Hybrid Done Right)

Sometimes you genuinely need both. The usual reason: you have Google Ads conversion tracking that was set up through the Google Ads interface (which installs a Google Tag), and you also run GTM for everything else. In this case, the Google Tag must be the primary loader and GTM must not fire its own GA4 Configuration tag.

The setup:

  1. Install the Google Tag (not a gtag('config') for GA4 specifically — the newer unified Google Tag snippet, which looks like gtag('config', 'GT-XXXXXXX') where GT is Google Tag’s own ID).
  2. In the Google Tag admin UI at tagmanager.google.com (yes, Google Tag admin now lives there), add your GA4 measurement ID as a destination.
  3. In GTM, do not create a GA4 Configuration tag. Only create GA4 Event tags.
  4. Configure the GA4 Event tags to use “Google Tag” as the measurement ID source, inheriting from the on-page Google Tag.

This works because the Google Tag registers itself as the owner of the GA4 destination, and GTM’s event tags attach to it rather than creating a second config. For the setup side of this, we usually handle it inside our GA4 service because it’s genuinely fiddly.

Shopify and WordPress Specific Gotchas

Shopify

The native “Google & YouTube” app installs a Google Tag that includes a GA4 destination by default. If you also have GTM installed via theme.liquid, you are automatically in a hybrid configuration. Post-Checkout Extensibility, the only sanctioned way to add analytics to checkout is via Custom Pixels under Customer events — but if you’ve been on Shopify long enough to have a checkout.liquid fossil (Plus merchants), you likely have a hardcoded GA4 snippet there too that needs to come out.

The order of operations I use:

  1. Disconnect the Google & YouTube app’s GA4 destination (keep it for Google Merchant Center if needed).
  2. Remove all gtag references from theme.liquid, checkout.liquid, and any snippets.
  3. Install GTM via theme.liquid and (for checkout) via a Custom Pixel.
  4. Build the GA4 Config tag once in GTM. Build ecommerce event tags against Shopify’s web_pixels events.

WordPress

Site Kit is the most common offender. It injects its own gtag.js snippet and there is no setting to disable just the on-page script while keeping the Site Kit dashboard. You either disconnect the Analytics module or you tolerate the hybrid.

Second most common: a conversion tracking plugin that was installed in 2019 for Google Ads and never audited. These frequently include a gtag('config', 'AW-XXXXXXX') call that also pulls in gtag.js and inherits any GA4 destinations linked to that Ads account. Deactivate and remove, don’t just disable.

Third: caching plugins. WP-Rocket and similar sometimes have a “delay JavaScript execution” setting that reorders your GTM snippet to fire after a user interaction. If your gtag snippet is in a different location in the page, it fires first, and now the loader hierarchy flips depending on whether the user moves their mouse before the page finishes loading. Fun to debug.

Validating the Migration

After you’ve cleaned things up, don’t just open Tag Assistant and call it done. Here’s the sequence I run:

1. Tag Assistant with Preview Mode

Enable Preview mode in GTM, navigate your site, and check the Tags Fired panel. You should see your Google Tag or GA4 Configuration tag fire exactly once per page. If you see “GA4 Config” fire once and also see an “unmanaged” gtag event in the Variables tab, there’s still a snippet somewhere.

2. Network Request Diff

Record the Network tab for a product page load before and after your changes. Filter for collect. Before migration you’ll typically see 2–4 /g/collect requests in the first second (one from each loader instance). After migration you should see 1 request per actual event, with monotonically increasing _s sequence numbers all sharing the same cid.

3. DebugView

Open GA4 → Admin → DebugView. Add ?gtm_debug=x or install the GA Debugger extension. Load your site and watch events arrive. The things to confirm:

  • Each event has a session_id parameter and it’s the same across the session
  • engagement_time_msec increments properly (if you see events with zero engagement time stacking up, you have a race condition)
  • page_view fires once per navigation, not two or three times
  • Your ecommerce events (view_item, add_to_cart, purchase) have intact items arrays

4. Realtime Report Sanity Check

Realtime is a lousy debugging tool but a great sanity check. If after your migration your Realtime user count drops by 50% and stays there, you’ve accidentally removed the only working tag. If it doubles, you still have both firing.

Common Mistakes and Troubleshooting

“I removed the snippet but GA4 traffic halved.” You removed the one that was actually working. The gtag snippet in the theme was the primary data source and GTM was mostly idle. Re-enable GTM’s GA4 Configuration tag and verify it fires on All Pages before removing anything else.

“DebugView shows two client IDs for the same session.” Something is still double-loading. Check for a plugin or app that injects scripts dynamically after page load (chat widgets and personalisation tools are repeat offenders). Also check your consent management platform — some CMPs inject their own gtag snippet when consent is granted.

“My ecommerce events stopped working after switching to Path 3.” The GA4 Event tags in GTM are probably still set to a hardcoded measurement ID instead of inheriting from the Google Tag. Open each event tag, change the Measurement ID field to use the Google Tag inheritance option, and republish.

“Consent mode is denying everything now.” If you set wait_for_update in gtag.js but your consent banner updates consent via GTM, the two don’t talk to each other properly in a hybrid setup. Move consent management entirely to one layer. If you use a CMP template in GTM, remove the gtag consent default call from your HTML.

“Server-side GTM is sending duplicate events.” Server-side containers happily accept hits from both your gtag.js loader and your web GTM container. If both are active and both point to the same server container, every event is duplicated at the server. Fix the client-side hybrid first, then the server-side counts will self-correct.

Key Takeaways

  • Google’s loader now force-promotes gtag.js on any page where it detects both a GTM container and a direct gtag('config') call, which breaks consent timing and cross-domain linking on hybrid implementations.
  • Audit your site by searching raw HTML for gtag('config', grepping your theme and plugin directories, and watching the Network tab for duplicate /g/collect requests with different cid values.
  • Pick one implementation path: fully GTM-managed for most ecommerce and marketing sites, fully gtag.js for small developer-owned stacks, or Google Tag + GTM only when Google Ads conversion tracking forces your hand.
  • Shopify’s native GA4 integration and WordPress Site Kit are the two biggest sources of accidental hybrid setups. Disconnect their analytics modules before building your proper implementation.
  • Validate with DebugView and Network diffs, not Tag Assistant alone. One page_view per navigation, one cid per session, incrementing _s values.
  • If your traffic drops after cleanup, you removed the loader that was actually working. Reinstate it, verify the replacement fires correctly, then remove the original.
#GTM#gtag.js#Google Tag#migration

Share this article

Want This Implemented Correctly?

Let our team apply these concepts to your specific setup — with QA validation and 30 days of support.