Skip to main content

Server-Side GTM on Cloud Run: The Complete 2026 Setup Guide

Provision a tagging server, map a first-party subdomain, migrate your tags in the right order, and keep the bill under control.

Server-side tagging has quietly moved from "interesting experiment" to "the thing that keeps your conversion data intact." Browser storage limits keep tightening, ad blockers keep getting better at pattern matching, and every measurement platform now wants event data that survives a page unload. A tagging server sits between your site and the vendors, gives you a first-party context for cookies, and lets you decide what data leaves your infrastructure.

This guide walks the full Cloud Run setup, from provisioning through the migration order that avoids double counting, plus what the bill actually looks like. Assume you already know your way around a GTM web container and GA4.

Why tagging servers stopped being optional

Three technical pressures pushed server-side tagging into the mainstream. Safari's Intelligent Tracking Prevention caps client-set cookies at seven days, and in some cases one day, which quietly shortens attribution windows for anyone using a JavaScript-set _ga cookie. Chrome's own privacy work has made third-party requests less reliable. And network-level blocking now catches roughly the same set of hostnames every list maintainer agrees on, which includes google-analytics.com and every major pixel domain.

A tagging server addresses all of these at once. Requests go to a subdomain of your own site, cookies get set with an HTTP Set-Cookie header from your own origin (so ITP's script-set cookie cap doesn't apply), and the request path is yours to name. You also get a single place where every outbound event passes through code you control. When legal asks whether you send email addresses to an ad platform, you answer by reading one container instead of auditing eleven tags across four properties.

What it is not: a way to hide tracking from users who declined consent. Consent state still has to travel to the server and be honored there.

Provisioning the server on Cloud Run

Start in Tag Manager. Create a container, choose Server as the target platform, and Tag Manager offers to provision a tagging server automatically. That path spins up a Cloud Run service in the project you pick and wires the container config for you. Fine for a first build. Most teams redeploy manually later for control over region, service account, and scaling floor.

The manual path is a single deploy of Google's published image. Grab the container config string from the server container's setup screen first (Admin, then Container Settings), because it goes in as an environment variable.

gcloud run deploy gtm-server \
  --image=gcr.io/cloud-tagging-10302018/gtm-cloud-image:stable \
  --region=us-central1 \
  --platform=managed \
  --allow-unauthenticated \
  --min-instances=3 \
  --max-instances=10 \
  --cpu=1 \
  --memory=512Mi \
  --set-env-vars=CONTAINER_CONFIG='PASTE_YOUR_CONFIG_STRING',PREVIEW_SERVER_URL='https://gtm-preview-xxxxx-uc.a.run.app'

You need two services, not one. The preview server runs the same image with RUN_AS_PREVIEW_SERVER=true and no min instances, so GTM's Preview mode can inspect requests without the main service holding debug state. Deploy it first, note its URL, then pass that URL into the main service as PREVIEW_SERVER_URL. Skip this and Preview mode silently fails to attach.

Pick the region closest to the bulk of your traffic. The tagging server adds a network hop to every measurement request, and a European site pointed at us-central1 pays about a hundred milliseconds for no reason. Multi-region with a global load balancer is a second-phase optimization, not a launch requirement.

Mapping a first-party tagging subdomain

The default *.run.app URL is useless for the purpose you built this for. run.app is on the Public Suffix List, so a cookie set there cannot be read by your main site, and browsers treat the request as cross-site anyway. You need a subdomain of the same registrable domain your site uses. If the site is shop.example.com, the tagging server should be something like metrics.example.com or gtm.example.com. Not example-tagging.com. Not a vendor's domain with a CNAME.

Two ways to get there. Cloud Run domain mapping is the quick option: Cloud Run, then Manage Custom Domains, add the mapping, then add the DNS records Google gives you. It's free and takes ten minutes, but it's not available in every region and gives you no control over the edge.

The other option is an external Application Load Balancer with a serverless network endpoint group pointing at the Cloud Run service, plus a Google-managed SSL certificate. This costs a few dollars a month in forwarding rule charges but buys you Cloud Armor, edge caching, and a stable anycast IP for an A record. If the site already sits behind a load balancer or CDN, use that and add the tagging server as another backend.

Avoid subdomain names that look like tracking. analytics.example.com and tags.example.com appear on public blocklists as generic patterns. Something neutral fares better and isn't evasion, since the endpoint stays fully visible in DevTools.

Pointing the web container at your server

In the web container, open the Google tag (the one carrying your GA4 measurement ID) and add a configuration parameter:

Parameter Name:  server_container_url
Value:           https://metrics.example.com

// If you load gtag.js directly instead of through GTM:
gtag('config', 'G-XXXXXXXXXX', {
  server_container_url: 'https://metrics.example.com',
  first_party_collection: true
});

Once that publishes, GA4 hits go to metrics.example.com/g/collect instead of google-analytics.com/g/collect. Open Network in DevTools, filter on collect, and confirm the host changed and the response is a 204. A 400 means the container config string is wrong or truncated. A 404 usually means the GA4 client isn't enabled.

The GA4 client in the server container claims those requests and turns them into an event object the server tags can read. It's enabled by default on a new container. You can give it a custom measurement path to move off /g/collect and sidestep path-based blocking, but only do that once you've confirmed blocking is actually costing you events.

Migrating GA4 and Google Ads first

Order matters, and the reason is simple: everything downstream depends on the GA4 event stream arriving cleanly at the server.

PhaseWhat movesWhat to verify before the next phase
1GA4 config and events via server_container_urlRealtime report matches pre-migration volume within normal daily variance; user counts stable across three days
2Server-side Conversion Linker_gcl_aw and _gcl_dc cookies present with an HTTP-only style lifetime, set from your own domain
3Google Ads conversion tracking and remarketingAds conversion counts within a few percent of the client-side tag over a full week
4Third-party pixels via conversion APIsPlatform-side dedupe rate showing browser and server events pairing correctly

For GA4, don't run both paths at once hoping to compare. Sending the same event to google-analytics.com and your tagging server produces duplicate sessions in one property. Cut the whole property over, watch three days, and keep the old config in a paused GTM version so rollback is one publish away.

Google Ads is the phase people rush. Add the Conversion Linker tag in the server container, triggered on the GA4 client's page_view, before you add any conversion tags. It reads gclid from the landing page URL and writes the _gcl_* cookies server-side, which is where the attribution durability actually comes from. Then add the Google Ads Conversion Tracking tag with your conversion ID and label, triggered off the GA4 event name representing that conversion.

Expect a small dip in reported Ads conversions during the first 48 hours as the cookie population rebuilds. If it hasn't recovered inside a week, your Conversion Linker isn't firing on landing pages.

Third-party pixels and server-side clients

Meta, TikTok, LinkedIn, Pinterest, and Reddit all publish conversion APIs, and the community template gallery has a server-side tag for each. The pattern is identical across them: the tag fires on a GA4 event, reads the event data, and posts to the vendor's API using an access token stored in a container variable rather than page source.

Deduplication is the part that goes wrong. Keep the browser pixel running alongside the server call and pass an identical event_id in both: generate a UUID in the web container, send it as a GA4 event parameter, map it to the CAPI tag's Event ID field. Forward fbp and fbc cookie values too, since the server can't read them otherwise. Check the platform's event matching quality panel after a week. Near-zero deduplication means your IDs aren't matching and you're double-counting.

There's a second flavor: server-side clients that claim non-GA4 requests directly. The Meta pixel client intercepts requests the browser pixel would have sent to Facebook and handles them on the server. Cleaner in principle, heavier to debug. Start with the GA4-triggered pattern.

Cost expectations and how to control them

Cloud Run bills on vCPU-seconds, GiB-seconds, and request count, with a monthly free tier a low-traffic site can sometimes live inside. The dominant cost for a tagging server is almost never request volume. It's the minimum instance floor, because those containers bill for the hours they sit idle.

Do the arithmetic yourself rather than trusting a blog number, including this one. Three min instances at 1 vCPU and 512 MiB across 730 hours is roughly 2,190 vCPU-hours and 1,095 GiB-hours of idle allocation before a single request arrives. Multiply by the current rate for your region. Request-driven CPU on top is a rounding error next to the floor.

SettingProduction valueEffect on cost and reliability
Min instances2 to 3The single biggest line item. Below 2, cold starts drop events during traffic spikes
Max instances10 or higherCosts nothing until used. Set it too low and you get 429s during campaign bursts
CPU1 vCPURarely the bottleneck. Going to 2 doubles idle cost for little gain
Memory512 MiBEnough unless you run many heavy templates. Watch for OOM restarts in logs
CPU allocationOnly during request processingMeaningfully cheaper than always-on. Fine unless a template does background work
Concurrency80 (default)Lowering it multiplies instance count and cost. Leave it alone

Two habits keep the bill honest. Set a budget alert in Cloud Billing at whatever number would make you uncomfortable, and check the metrics tab monthly to see whether your min instance floor is actually needed at 3 AM. Scheduling a lower overnight floor via Cloud Scheduler saves real money for a regional business and nothing for a global one.

Monitoring requests and error rates

The Cloud Run service detail page gives you request count, latency, container instance count, and billable instance time out of the box. Those four charts answer most questions. Put them on one Cloud Monitoring dashboard and bookmark it.

Set alerting policies for the things that actually indicate breakage:

Container logging is off by default for cost reasons. Turn on console logging while debugging a specific tag, then turn it back off, because Cloud Logging ingestion is billed by volume and a busy tagging server produces a lot of it. For ongoing visibility, log only failures, and include the event name and vendor so the line is actionable on its own.

Mistakes that show up in audits

The same handful of problems appear in nearly every server-side container we look at.

  1. Min instances left at zero. Cold starts on a tagging server take seconds. Every request that arrives during one either waits or fails, and traffic spikes are exactly when it matters.
  2. Tagging subdomain on a different registrable domain. Cookies set there are third-party. All the durability benefit disappears and nobody notices until attribution quietly degrades.
  3. Consent state never forwarded. The web container respects Consent Mode; the server receives an event with no consent signal and fires everything. Pass consent as event parameters and gate server tags on them.
  4. No Conversion Linker on the server. Ads tags fire, conversions record, attribution is worse than before, and the team concludes server-side "didn't work."
  5. Preview server not configured. Debugging becomes guesswork, so people publish straight to production to test.
  6. Access tokens pasted into tag fields as plain text. Anyone with container read access can lift your Meta or TikTok token. Use the built-in variable types and restrict permissions.

One more, less obvious: the server container has its own version history and its own publish button. A change that works in Preview does nothing in production until you publish that container specifically. Two containers, two publishes, easy to conflate at 6 PM.

Rolling this out without drama

A sane sequence takes about two weeks of calendar time and maybe eight hours of work. Week one: deploy both Cloud Run services, map the subdomain, verify the GA4 client claims requests in Preview, then cut GA4 over and watch it. Week two: Conversion Linker, then Google Ads, then one third-party pixel at a time with a dedupe check after each.

Keep the old web container version paused rather than deleted, write down which numbers you expect to move and by how much, and warn whoever reads the Ads dashboard that a short dip is coming. Most of the pain in these migrations comes from surprise, not from the technology.