← all articles

How to set up server-side tracking without breaking analytics

Server-side tracking is sold as a fix for ad blockers, Safari cookie limits and slow pages. Some of that is true. What the sales pages skip is that most migrations I have seen make reporting worse for a few weeks. Sessions get split, conversions get counted twice, referrers turn into your own tagging domain, and nobody notices until a client asks why revenue “fell 30%” when nothing changed.

This tutorial is for operators who already run GA4 and a few ad pixels through a normal web GTM container and want to move the collection to a server they control. I run this setup for several sites from Singapore, and the approach below is the one that has not cost me a reporting gap. The outcome: a server-side Google Tag Manager container on your own subdomain, GA4 and one ad platform fed from it, and a parallel run that proves the numbers match before you cut over.

One honest caveat first. Server-side tracking is not a magic recovery of lost data. It makes collection more durable and gives you control over what leaves your server. It does not remove the need for consent, and it will not make a broken tagging plan accurate.

what you need

  • a working web GTM container with GA4 already firing correctly (if your baseline is wrong, server-side will just move the wrongness)
  • a Google Cloud account with billing enabled, or a managed host such as Stape. Google’s server-side tagging docs cover both paths
  • control of DNS for your domain, so you can add a subdomain like data.yoursite.com
  • admin access to GA4 and to any ad platform you plan to forward events to
  • a consent setup that already works. if you need one, the theprivacywire blog covers the privacy side in more depth than I will here
  • a budget. Cloud Run is billed by usage and Google recommends at least three instances for production, so check the Cloud Run pricing page against your traffic before you commit. managed hosts charge per request tier instead
  • two to four hours for the build, then two weeks of parallel running

Do the work on a staging property or a low-traffic site first. I mean it. A tagging mistake on your best site costs real data you cannot backfill.

step by step

1. snapshot your current numbers

Before touching anything, export a baseline. In GA4, pull 28 days of sessions, key events, purchases or leads, and top 10 source/medium rows. Save it as a CSV.

Expected output: one file with daily sessions and daily conversions per channel.

If it breaks: if the numbers already look odd, stop and read search console numbers that do not add up first. the gap between tools has several normal causes and you need to know which ones you already have.

2. create the server container

In tagmanager.google.com, create a new container and choose “Server” as the target platform. When asked how to provision, either pick automatic provisioning on Google Cloud or paste in the details from your managed host.

Automatic provisioning creates a Cloud Run service for you. For testing, a single instance is fine. For production, follow Google’s guidance and raise the minimum instance count.

Expected output: a container with a default URL ending in run.app and a status of “running”.

If it breaks: a deploy that hangs usually means billing is not enabled on the Google Cloud project, or the Cloud Run API is off. enable both and retry.

3. put the server on your own subdomain

This is the step that decides whether you get any cookie benefit. A tagging server on something.run.app is a third party. A tagging server on data.yoursite.com is first party.

Add the DNS records your host gives you. The shape looks like this:

data.yoursite.com.   300   IN   A      203.0.113.10
data.yoursite.com.   300   IN   AAAA   2001:db8::10

(those are documentation example addresses, use the ones from your own provisioning screen.) Then set the custom domain in the server container settings.

Expected output: https://data.yoursite.com/healthz returns ok.

curl -i https://data.yoursite.com/healthz

If it breaks: certificate errors mean DNS has not propagated yet, wait and retry. if you use Cloudflare for the main domain, set the record to DNS only (grey cloud) while you test, so the proxy is not rewriting headers.

4. point GA4 at the server

In your web container, open the Google tag (the GA4 configuration) and add a server_container_url parameter with your subdomain. If you hardcode gtag instead of using GTM, it looks like this:

<script>
  gtag('config', 'G-XXXXXXX', {
    server_container_url: 'https://data.yoursite.com'
  });
</script>

Do not remove anything else. The web container still decides what fires and when. The server is only the receiving end.

Expected output: in GTM preview mode for the web container, requests to data.yoursite.com/g/collect appear instead of google-analytics.com.

If it breaks: no requests to your domain means the setting is on the wrong tag, or a second GA4 tag is still sending direct. search the container for every GA4 tag and fix the duplicates.

5. add the GA4 client and tag on the server

In the server container, the GA4 client should already exist from the default setup. Add a GA4 tag that sends to your measurement ID, triggered by the GA4 client.

Open the server container’s preview mode, load your site, and watch the incoming request appear, get claimed by the GA4 client, and fire the tag.

Expected output: a green request in preview, with the GA4 tag status “succeeded”.

If it breaks: a request that no client claims means the path or the client config is off. a tag that fires but returns an error usually means the measurement ID is wrong. Google documents the underlying format in the Measurement Protocol reference if you want to see what a valid hit looks like.

6. run in parallel before you cut over

This is the step most tutorials skip, and it is the one that protects your reporting. Create a second GA4 property, send the server-side stream to that, and leave the original property on the old direct route. Run both for at least two weeks, covering a full weekly cycle.

Expected output: two properties with daily sessions within a few percent of each other. server-side often reads slightly higher because ad blockers stop blocking your collection.

If it breaks: a gap bigger than about 10 percent needs explaining before you cut over. compare landing pages and source/medium rows between the two. a pile of “(direct)” in the new property usually means referrer or cross-domain settings were lost. see pitfalls below.

7. forward one ad platform with deduplication

Pick one platform, not five. For Meta, the Conversions API documentation explains that when you send the same event from the browser pixel and from the server, you must send a matching event_name and event_id so the platform can deduplicate. Generate the ID once in the browser and pass it to both routes.

// web container custom JS variable: one id per event, reused by pixel and server hit
function() {
  return 'evt_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10);
}

Expected output: in the ad platform’s events manager, browser and server events show as received with a deduplication rate that is not zero.

If it breaks: conversions that doubled overnight mean the IDs do not match, or one side never sent one. check a single test purchase end to end before trusting the dashboard.

8. cut over and keep the old data readable

When the two-week comparison holds, switch the main property to the server route and retire the test property. Annotate the cut-over date in GA4 and in whatever reporting sheet your stakeholders look at. If you report to people who do not live in analytics, reporting seo work to someone who does not do seo covers how to explain a step change without losing their trust.

Expected output: a dated annotation and a before/after note in your baseline sheet.

If it breaks: a sudden drop right after the switch means something differs from the parallel property. flip the server_container_url back, which takes effect on the next container publish, and diff the two configurations.

common pitfalls

  • changing the tagging plan and the transport together: if you rename events or add parameters during the move, you cannot tell whether a change in numbers came from the move or the rename. migrate first, improve later
  • skipping the parallel run: the cut-over looks fine on day one and the damage shows up in month-end reports. two weeks of side-by-side data is cheap insurance
  • leaving the server on a third-party hostname: you pay for the infrastructure and get none of the first-party benefit. also check that the subdomain does not sit on a very different IP range from your main site, since Safari’s tracking prevention can still cap cookies in that case
  • double firing: the old browser tag stays live, the server tag also fires, and conversions count twice. this is the single most common bug I see. search every container for duplicate GA4 and pixel tags
  • ignoring consent: moving collection to your own server does not change what the law expects. consent mode and consent signals still need to flow through. this is not legal advice, so check your obligations with someone qualified in the markets you serve. If you run one site across more than one jurisdiction, running one site for two countries shows how I think about splitting rules by market

scaling this

From 10x to 100x to 1000x of your starting traffic, the work changes shape.

At 10x (a handful of sites, tens of thousands of hits a day), a single small server container is fine for testing, but run the production minimum Google recommends. The work is mostly tagging hygiene. You will spend more time on duplicate tags than on infrastructure.

At 100x (hundreds of thousands to low millions of hits a day), cost and uptime start to matter. Set up uptime monitoring on the /healthz path, alert on error rates in Cloud Run, and review your instance scaling settings monthly. This is also where managed hosts either pay for themselves or get expensive, so price both against actual request volume. A tagging outage means silent data loss, because the browser will not retry forever.

At 1000x (tens of millions of hits a day or many client properties), you want infrastructure as code for the container deployment, a staging server container that mirrors production, and a written change process. One person publishing a bad tag to a shared server container can break reporting for every site behind it. Split containers by client or by business unit so a mistake stays contained.

Across all three, keep a changelog of container versions with dates. When numbers move, the first question is always “what did we publish that week”, and you want the answer in thirty seconds.

where to go next

Written by Xavier Fok

disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-10-05.

for SEOs
Tracking rankings or scraping SERPs at scale?

Rank checkers and SERP crawlers get blocked and geo-skewed fast on datacenter IPs. Singapore Mobile Proxy runs real 4G/5G mobile IPs that search engines still trust, so your position data stays clean.

see plans →
read on
More from The SEO Desk

Technical SEO, link building, content and SERP strategy, and tool reviews for people who ship growth.

browse all articles →