TL;DR
Using multiple GTM containers has been officially supported since 2015. However, all containers must share the same dataLayer — using different dataLayer names causes trigger issues and data loss. Modern GTM implementations require Consent Mode v2 (GDPR/CCPA), Server-side tagging, and GA4 event schema as essential components.
| Topic | 2019 Approach | 2025+ Current State |
|---|---|---|
| dataLayer | Different name per GTM | Single dataLayer required |
| Instantiation | dataLayer = [{...}] | window.dataLayer = window.dataLayer || [] |
| Analytics | Universal Analytics | GA4 (UA deprecated) |
| Privacy | Optional | Consent Mode v2 required |
| Tagging | Client-side only | Server-side preferred |
Why Multiple GTM Is Needed?
In most cases, a single GTM container is the best solution. However, multiple containers may be unavoidable in these scenarios:
- Different teams/agencies: Ad agency wants to manage their own tags
- Access control: Only certain people should have access to specific tags
- Multi-tenant structures: Different business units need independent tracking on the same domain
- Vendor requirements: Third-party software mandates their own GTM
Important: When an agency or vendor requests to install their own GTM container, the preferred approach is to grant them access to your GTM first. This ensures full visibility over changes.
dataLayer: Legacy vs Current Approach
Legacy Approach (2019)
2019 recommendation: Different dataLayer names could be used for each GTM container. For example, first container uses
dataLayer, second container usesnewLayer.
<!-- 2019: Different dataLayer names (NO LONGER RECOMMENDED) -->
<script>(...,'dataLayer','GTM-XXXX');</script>
<script>(...,'newLayer','GTM-YYYY');</script>
Current Approach (2025+)
Google’s current documentation clearly states:
“Using more than one data layer can cause some triggers to stop working and could have other implications.” — Google Tag Manager Developer Guide
All containers must share the same dataLayer:
<!-- Correct: Single dataLayer, multiple containers -->
<script>
window.dataLayer = window.dataLayer || [];
</script>
<!-- GTM Container 1 -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXX');</script>
<!-- GTM Container 2 -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-YYYY');</script>
dataLayer Instantiation
Legacy method (problematic):
dataLayer = [{'pageTitle': 'Home'}];This usage completely overwrites the existing dataLayer.
Correct method:
// Safe instantiation - preserves existing data
var dataLayer = window.dataLayer = window.dataLayer || [];
// Always add data using push()
dataLayer.push({
'pageTitle': 'Home',
'pageType': 'home'
});
Key Considerations for Multiple Containers
1. Shared dataLayer = Shared Events
Since all containers share the same dataLayer, events pushed by one container are visible to other containers. This means:
Advantage: Ensures data consistency Risk: Unintended tag triggers may occur
Solution: Keep event names specific. Use unique names like agency_remarketing_click instead of generic gaEvent.
2. Event Naming Convention
// ❌ Wrong: Generic event name
dataLayer.push({ 'event': 'click' });
// ✅ Correct: Specific and namespaced
dataLayer.push({ 'event': 'brand_cta_click' });
dataLayer.push({ 'event': 'agency_form_submit' });
3. PII (Personal Data) Risk
All data pushed to the shared dataLayer is accessible by all containers. If one container pushes PII (name, email, phone), other containers can also access this data.
// ⚠️ Warning: This data is visible to ALL containers
dataLayer.push({
'event': 'form_submit',
'userEmail': 'user@example.com', // PII!
'userName': 'John Doe' // PII!
});
Consent Mode v2 Integration
Consent Mode v2 is now mandatory for GDPR, CCPA, and other privacy regulations.
Basic Setup
// BEFORE the GTM snippet
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Default consent state (before user permission)
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied'
});
// Update after user grants permission
gtag('consent', 'update', {
'ad_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted',
'analytics_storage': 'granted'
});
Regional Consent Settings
// Default denied for EU users
gtag('consent', 'default', {
'ad_storage': 'denied',
'analytics_storage': 'denied',
'region': ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE',
'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV',
'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK',
'SI', 'ES', 'SE']
});
// Granted for other regions
gtag('consent', 'default', {
'ad_storage': 'granted',
'analytics_storage': 'granted'
});
GA4 Event Schema (Replacing Enhanced Ecommerce)
Legacy approach (Universal Analytics Enhanced Ecommerce):
dataLayer.push({ 'event': 'addToCart', 'ecommerce': { 'currencyCode': 'USD', 'add': { 'products': [{ 'id': '123', 'name': 'Product', 'price': 99.90 }] } } });
GA4 Ecommerce Event Schema:
dataLayer.push({ ecommerce: null }); // Clear previous ecommerce data
dataLayer.push({
'event': 'add_to_cart',
'ecommerce': {
'currency': 'USD',
'value': 99.90,
'items': [{
'item_id': '123',
'item_name': 'Product Name',
'item_brand': 'Brand',
'item_category': 'Category',
'price': 99.90,
'quantity': 1
}]
}
});
GA4 Standard Events
| Event | Description |
|---|---|
view_item | Product detail page view |
add_to_cart | Add to cart |
remove_from_cart | Remove from cart |
begin_checkout | Start checkout |
add_payment_info | Payment info entry |
add_shipping_info | Shipping info entry |
purchase | Purchase completion |
Server-Side GTM
Server-side GTM has become the standard for enterprise projects in 2025.
Advantages
- Performance: Reduces client-side script load
- Privacy: Data processing in first-party context
- Ad-blocker bypass: Server-side requests aren’t blocked
- Data control: PII masking and filtering
Basic Architecture
[Browser] → [Client-side GTM] → [Server-side GTM Container] → [GA4, Ads, Meta...]
↓
[First-party domain]
Platform Integrations: Shopify
Shopify runs GTM as a Custom Pixel in a sandbox environment. This provides security but introduces some limitations.
Important: GTM setup on Shopify requires advanced JavaScript knowledge. Custom pixels are not supported by Shopify — troubleshooting is your responsibility.
Shopify Sandbox Limitations
- Limited direct DOM access
- No
document.cookieaccess - Limited
localStorage/sessionStorage - Third-party scripts run inside the sandbox
GTM Custom Pixel Setup
// dataLayer and gtag definition
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// GTM snippet (without HTML tags)
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer', 'GTM-XXXXXXX');
// Consent Mode v2
gtag('consent', 'update', {
'ad_storage': 'granted',
'analytics_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted',
});
Forwarding Shopify Events to GTM
Use Shopify’s analytics.subscribe() API to listen for standard events and push to dataLayer:
// Page view
analytics.subscribe("page_viewed", (event) => {
window.dataLayer.push({
event: "page_viewed",
timestamp: event.timestamp,
client_id: event.clientId,
url: event.context.document.location.href,
page_title: event.context.document.title,
});
});
// Product view
analytics.subscribe("product_viewed", (event) => {
window.dataLayer.push({
event: "product_viewed",
timestamp: event.timestamp,
client_id: event.clientId,
url: event.context.document.location.href,
product_id: event.data?.productVariant?.product?.id,
product_title: event.data?.productVariant?.title,
product_sku: event.data?.productVariant?.sku,
});
});
// Add to cart
analytics.subscribe("product_added_to_cart", (event) => {
window.dataLayer.push({
event: "product_added_to_cart",
timestamp: event.timestamp,
client_id: event.clientId,
url: event.context.document.location.href,
price: event.data?.cartLine?.merchandise?.price?.amount,
product_title: event.data?.cartLine?.merchandise?.product?.title,
quantity: event.data?.cartLine?.quantity,
total_cost: event.data?.cartLine?.cost?.totalAmount?.amount,
});
});
// Checkout complete
analytics.subscribe("checkout_completed", (event) => {
window.dataLayer.push({
event: "checkout_completed",
timestamp: event.timestamp,
token: event.data?.checkout?.token,
client_id: event.clientId,
email: event.data?.checkout?.email,
orderId: event.data?.checkout?.order?.id,
currency: event.data?.checkout?.currencyCode,
value: event.data?.checkout?.totalPrice?.amount,
tax: event.data?.checkout?.totalTax?.amount,
shipping: event.data?.checkout?.shippingLine?.price?.amount,
});
});
Shopify → GA4 Event Mapping
| Shopify Event | GTM Trigger | GA4 Event |
|---|---|---|
page_viewed | Custom Event | page_view |
product_viewed | Custom Event | view_item |
product_added_to_cart | Custom Event | add_to_cart |
product_removed_from_cart | Custom Event | remove_from_cart |
cart_viewed | Custom Event | view_cart |
checkout_started | Custom Event | begin_checkout |
checkout_address_info_submitted | Custom Event | add_shipping_info |
payment_info_submitted | Custom Event | add_payment_info |
checkout_completed | Custom Event | purchase |
Migrating Legacy dataLayer.push Code
Legacy method (in theme.liquid):
<script> dataLayer.push({ event: 'email_signup', email: customer.email }); </script>
New method (using Shopify.analytics.publish):
<!-- In theme.liquid -->
<script>
Shopify.analytics.publish('email_signup', { email: customer.email });
</script>
// In custom pixel
analytics.subscribe("email_signup", (event) => {
window.dataLayer.push({
'event': 'email_signup',
'email': event.customData.email,
});
});
GTM Trigger Setup
Create a Custom Event trigger in GTM for each Shopify event:
- Triggers → New → Custom Event
- Event name:
checkout_completed(or relevant event) - This trigger fires on: All Custom Events
- Connect the tag to this trigger
GA4 Tag Configuration
Tag Type: Google Analytics: GA4 Event
Measurement ID: G-XXXXXXXX
Event Name: purchase (or relevant GA4 event)
Event Parameters:
- transaction_id: {{DLV - orderId}}
- value: {{DLV - value}}
- currency: {{DLV - currency}}
- items: {{DLV - items}}
Testing: Use Shopify Pixel Helper and Google Tag Assistant together. Tag Assistant’s “Troubleshoot tag” feature won’t detect pixels inside the sandbox — check dataLayer manually.
noscript Tags
Each GTM container requires a noscript tag:
<!-- GTM Container 1 noscript -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- GTM Container 2 noscript -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-YYYY"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
Testing and Debugging
GTM Preview Mode
Preview Mode should be activated separately for each container:
- Start Preview for GTM-XXXX
- Start Preview for GTM-YYYY in a separate tab
- Check both debug panels on the target page
Tag Assistant
Use Google Tag Assistant to check:
- Which tags are firing
- dataLayer contents
- Consent state
Debug with Container ID
// Check specific container's state
console.log(window.google_tag_manager['GTM-XXXX']);
console.log(window.google_tag_manager['GTM-YYYY']);
Common Errors and Solutions
| Error | Cause | Solution |
|---|---|---|
| Triggers not working | Different dataLayer names | Use single dataLayer |
| Data loss | Using dataLayer = [] | Use window.dataLayer = window.dataLayer || [] |
| Duplicate events | Generic event names | Use namespaced names |
| PII leakage | Sensitive data in shared dataLayer | Process data server-side |
| Consent violation | Missing Consent Mode | Integrate Consent Mode v2 |
Resources
- Google Tag Manager Data Layer Documentation
- Consent Mode Implementation Guide
- GA4 Ecommerce Events Reference
- Server-side Tagging Guide
- Shopify GTM Custom Pixel Tutorial
- 01 All GTM containers must share a single dataLayer
- 02 dataLayer instantiation: window.dataLayer = window.dataLayer || []
- 03 Consent Mode v2 is mandatory for GDPR/CCPA
- 04 Server-side GTM recommended for performance and privacy
- 05 GA4 event-driven model, Universal Analytics deprecated
+ Can I use multiple GTM containers?
Yes, officially supported since 2015. However, all containers must share the same dataLayer.
+ Should I use different dataLayers for each GTM?
No. Google's current recommendation is for all containers to use a single dataLayer. Different dataLayers can cause trigger issues.
+ What is Consent Mode v2 and why is it required?
A mandatory user consent management system for GDPR/CCPA compliance. It enables tags to fire based on user permissions.
+ Should I use Server-side GTM?
Recommended for performance, privacy, and ad-blocker bypass. It became standard for enterprise projects in 2025.
+ How do I install GTM on Shopify?
Install as a Custom Pixel. It runs in a sandbox environment, use analytics.subscribe() to listen to Shopify events and push to dataLayer.