GTM 12 min read

Split GA4 Events Between Client-Side and Server-Side GTM: A Practical Routing Guide

Sending every GA4 event through server-side GTM is expensive and often unnecessary. The real skill is deciding which events belong client-side, which belong server-side, and how to route them without

A
Ashwani Bhasin
·

Sending every GA4 event through server-side GTM is expensive and often unnecessary. The real skill is deciding which events belong client-side, which belong server-side, and how to route them without double-counting. Most guides frame server-side as an all-or-nothing migration: spin up a tagging server, repoint your GA4 tag, and celebrate. Then the Cloud Run bill arrives, and half your events are page_views and scroll depths that never needed the round-trip in the first place.

After running this split for a dozen ecommerce and SaaS clients, I’ll say it plainly: a hybrid routing strategy is almost always the right answer. Cheap, low-stakes events stay client-side. Conversion, revenue, and consent-sensitive events go server-side where you can enrich them, hash PII, and control the outbound payload. Below is the framework, the config, the session-continuity plumbing, and the actual cost numbers.

Why hybrid beats full migration

The pitch for server-side GTM is real: better data quality, first-party cookies via a custom domain, PII redaction before hitting Google, ad-blocker resilience, and consent enforcement on the server. Those benefits matter for the events that drive decisions — purchases, leads, add-to-cart, signup.

They matter much less for a scroll_depth event fired by 40% of your traffic. That event will never be reconciled against a CRM record, never enriched with LTV, never routed to a CAPI endpoint. Sending it through a Cloud Run container costs you compute, egress, and log storage for zero analytical upside.

The hybrid model accepts this asymmetry. You get the accuracy where it pays back, and you keep the cheap events on the free client-side pipe.

The decision framework

I use four criteria to route any GA4 event. If an event scores high on two or more, it goes server-side. Otherwise it stays client-side.

CriterionQuestion to askServer-side if…
Business valueDoes this event feed a KPI, attribution model, or ads platform?Yes — revenue, leads, signups, cart events
Consent sensitivityDoes the payload contain PII or identifiers you must gate?Yes — email, phone, user_id, hashed identifiers
Enrichment needDo you need to add server-known data (LTV, margin, CRM ID)?Yes — anything requiring a lookup
Downstream fan-outWill this event also go to Meta CAPI, TikTok Events API, Klaviyo, etc.?Yes — multi-destination events

Applied to a typical Shopify store, the split usually looks like this:

EventRouteReasoning
page_viewClient-sideHigh volume, low decision value, no PII
scroll, click, video_progressClient-sideEngagement noise, no fan-out
view_item, view_item_listClient-sideHigh volume, no PII, rarely fanned out
add_to_cartServer-sideFed to Meta CAPI and Klaviyo, needs event_id dedupe
begin_checkoutServer-sideConsent-gated identifiers, CAPI fan-out
purchaseServer-sideRevenue truth, hashed PII, multi-destination
generate_lead / sign_upServer-sideUser_id assignment, CRM enrichment
search, filterClient-sideProduct analytics, low value per event

That split typically leaves 15–25% of your event volume on the server-side pipe. The other 75–85% stays on the free client-side GA4 endpoint. More on the cost numbers below.

Configuring the split without double-counting

Here’s where most implementations break. Analysts either duplicate events (client and server both fire, GA4 double-counts) or they miss events entirely because the routing logic has a gap.

The clean approach: use a single GA4 Event tag template in web GTM, and set server_container_url conditionally based on the event name. When the field is populated, the tag routes to your tagging server. When it’s empty, the tag routes to google-analytics.com directly. One tag, two paths, no duplication.

Step 1: Create a routing lookup variable

In your web container, create a Lookup Table variable called lookup - GA4 Route.

  • Input variable: {{Event}} (the GTM data layer event name, or a custom variable that mirrors your GA4 event_name)
  • Rows:
    • purchasehttps://sgtm.yourdomain.com
    • add_to_carthttps://sgtm.yourdomain.com
    • begin_checkouthttps://sgtm.yourdomain.com
    • generate_leadhttps://sgtm.yourdomain.com
    • sign_uphttps://sgtm.yourdomain.com
  • Default value: leave blank

That’s it. Every server-side event resolves to your tagging server URL. Everything else resolves to blank, meaning the GA4 tag will use the standard collection endpoint.

Step 2: Configure the GA4 Configuration / Event tag

If you’re still using the classic GA4 Configuration tag pattern (Google is migrating this into the Google Tag, but the parameter behaviour is the same), open the tag and add a field:

  • Field name: server_container_url
  • Value: {{lookup - GA4 Route}}

For the newer Google Tag setup, the same parameter goes into the “Configuration settings” of your Google Tag. If the variable resolves to an empty string, GA4 ignores it and defaults to the standard endpoint. That’s the behaviour we want.

Step 3: Verify the routing with a tiny custom template

If you want belt-and-braces confirmation, add a Custom JavaScript variable that logs the resolved route to the console during Preview:

function() {
  var eventName = {{Event}};
  var serverUrl = {{lookup - GA4 Route}};
  var route = serverUrl ? 'server-side (' + serverUrl + ')' : 'client-side (google-analytics.com)';
  if (window.console && console.debug) {
    console.debug('[GA4 Route]', eventName, '→', route);
  }
  return route;
}

Fire it as a variable on every tag. Now every event in Preview mode leaves a breadcrumb showing which pipe it took.

Step 4: Prevent the accidental double-send

The classic footgun: you have a separate GA4 Event tag for purchase that fires only on the server pipe, plus a catch-all GA4 Event tag that fires on all events (including purchase) on the client pipe. Result: purchase gets sent twice, once to each endpoint, and GA4 counts both.

Fix this by using a single generalised GA4 Event tag driven by the data layer, with the conditional server_container_url doing the routing. Do not maintain parallel tags. If you already have per-event tags, audit your trigger exceptions so that server-routed events are excluded from the client-only tags.

Session continuity across both paths

This is the part most guides skip, and it’s the source of the messiest bugs. When events travel through two different endpoints, GA4 must still see them as the same session with the same user. That means three identifiers have to remain consistent across both paths:

  • client_id — persistent user identifier from the _ga cookie
  • session_id — timestamp anchor from _ga_<MEASUREMENT_ID>
  • session_number — session count from the same cookie

The good news: if you’re using the standard GA4 tag and letting it read the first-party cookies on your web domain, these identifiers travel with the request automatically on both paths. GA4 packs them into the outgoing request from the same cookie source, whether the destination is Google’s endpoint or your tagging server.

The gotcha: if your tagging server is on a subdomain that doesn’t share cookies with the primary site, or if you’ve set the tagging server up to write its own _ga cookie via the GA4 Client, you can end up with two different client_id values for the same user.

Your tagging server subdomain must be on the same registrable domain as your site, and the _ga cookie must be set with a domain scope that covers both. For a site at www.example.com with a tagging server at sgtm.example.com, the _ga cookie should be set with Domain=.example.com. GA4’s automatic cookie behaviour handles this correctly by default; problems arise when someone has manually configured cookie_domain in the GA4 tag or in the sGTM GA4 Client.

Rule of thumb: leave cookie_domain unset in both containers unless you have a specific reason to override it. GA4’s default is auto, which resolves to the highest-level registrable domain, and that’s what you want.

Verifying session parity

In the sGTM Preview, open any incoming server-routed request and check the cid and sid parameters (client_id and session_id). Then compare them to what’s set in the browser’s _ga and _ga_XXXX cookies. They must match. If they don’t, the tagging server is minting new identifiers and your sessions are fragmenting.

Debugging the split setup

Three tools cover 95% of what you need.

Tag Assistant / Preview mode (web container)

In web Preview, every fired GA4 tag shows its outgoing request URL in the tag details. Look at the “Request URL” line:

  • https://www.google-analytics.com/g/collect?... → client-side path
  • https://sgtm.yourdomain.com/g/collect?... → server-side path

Fire a purchase event and a page_view in the same session. Confirm purchase goes to your server domain and page_view goes to Google’s domain. If they both go to the same place, your lookup variable is misconfigured.

sGTM Preview mode

Open Preview on the server container and reproduce the events. You should see requests appear for exactly the events you routed server-side — nothing more, nothing less. If a page_view shows up in sGTM Preview, you have a bleed: check the server_container_url variable and make sure it’s returning empty for that event.

Inside each event in sGTM Preview, expand the “Event Data” panel and confirm the enrichment your server-side tags are doing (hashed email, added user_id, injected currency) is actually happening before the outbound GA4 tag fires.

Server logs

For volume verification, enable request logging on the tagging server (Cloud Run logs, or whatever platform you host on). Run a query that counts requests by event_name over 24 hours, then compare that to GA4’s DebugView or the Realtime report filtered by the same events. The numbers should be within a few percent (some difference is normal due to bot filtering and consent drops).

If your server logs show 500K requests/day but you’re only routing purchase and add_to_cart, something is misrouted. Common cause: a stray tag with a hardcoded server_container_url in a field.

The cost model

Here’s what actually pushed us toward hybrid on almost every account. Server-side GTM on Cloud Run is cheap per request, but the per-request cost compounds fast at ecommerce volumes.

A rough baseline for a properly-tuned tagging server on Cloud Run (2 vCPU, 1GB RAM, minimum 3 instances for latency stability): about $0.15–$0.25 per million requests in raw compute, plus egress that scales with payload size. Preview mode and logging add non-trivial overhead if left permanently on.

Take a mid-size Shopify store doing 500K sessions/month with ~7 GA4 events per session on average. That’s 3.5M events/month.

ScenarioServer events/monthApprox monthly server costNotes
Full migration3.5M$180–$260All events routed, plus CAPI fan-out overhead
Hybrid (purchase + cart + lead)700K$45–$7580% reduction in server volume
Client-side only0$0No CAPI, no enrichment, no consent server enforcement

The hybrid split typically cuts server-side request volume by 60–80% compared to a full migration. At larger volumes (10M+ events/month) the delta becomes significant enough to fund an entire analytics contract. And the events you drop from the server pipe are precisely the ones that didn’t need it.

There’s a second-order cost worth naming: sGTM instance minimums. If your server is sized for peak load, you’re paying for that headroom whether you route 10M events or 2M. Cutting volume by 70% often lets you reduce your instance floor from 5 to 2, which multiplies the savings.

For teams building this out from scratch, we usually pair the routing work with a broader tagging audit — see our GTM service for how that engagement typically runs.

Common Mistakes and Troubleshooting

Mistake 1: Parallel tags for the same event. Someone builds a “Purchase - Server” tag and forgets to exclude purchase from the generic “All Events - Client” tag. GA4 receives two purchase events with different transaction_id handling and double-counts revenue. Fix: consolidate to one tag per event with conditional routing.

Mistake 2: Hardcoding the server URL in the tag field. Once the URL is hardcoded, every event routes server-side regardless of the lookup. Always bind server_container_url to a variable, not a literal string.

Mistake 3: Different cookie_domain values between web and server GA4 Clients. Produces client_id fragmentation. Leave both on auto.

Mistake 4: Trusting DebugView for volume validation. DebugView is sampled and delayed. Use the Realtime report and BigQuery export for actual counts.

Mistake 5: Forgetting consent state on the server pipe. Server-side tags don’t automatically respect Consent Mode signals unless you pass them through. If the GA4 Client on the server doesn’t see gcs and gcd parameters, downstream Meta CAPI or Google Ads Enhanced Conversions tags will fire against denied users. Verify consent parameters are in the incoming payload before any fan-out tag runs.

Mistake 6: Enrichment happens after the GA4 tag fires. In sGTM, tag order matters. If you’re using a Transformation to hash email addresses, make sure it runs before the GA4 tag, not after. Otherwise you send unhashed PII to Google.

Mistake 7: Assuming ad-blocker resilience on client-routed events. The events staying on the client pipe are still blockable. That’s fine for page_view and scroll, but if you thought server-side gave you full resilience across all events, hybrid explicitly doesn’t. Route anything you need bulletproof to the server.

Key Takeaways

  • A hybrid client/server routing strategy typically cuts server-side GTM request volume by 60–80% compared to full migration, with no loss in data quality on the events that matter.
  • Use one GA4 Event tag with a conditional server_container_url bound to a Lookup Table variable. Do not build parallel tags for the same event.
  • Session continuity depends on cookie-domain consistency. Leave cookie_domain on auto in both web and server containers unless you have a specific reason otherwise.
  • Route by four criteria: business value, consent sensitivity, enrichment need, and downstream fan-out. Two or more hits means server-side.
  • Debug the split in three places: web Preview (request URL per tag), sGTM Preview (only routed events should appear), and server logs (volume matches expectations).
  • The events that benefit from server-side are the ones you’d fight to recover if lost. Everything else is noise that doesn’t deserve the compute bill.
#GA4#GTM#Server-Side Tagging#Event Tracking

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.