<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ruben Cali</title>
    <description>The latest articles on DEV Community by Ruben Cali (@ruben_cali_1d63ebf3411a44).</description>
    <link>https://dev.to/ruben_cali_1d63ebf3411a44</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4105715%2F7afbee11-2cce-46c6-b014-4894348e0084.png</url>
      <title>DEV Community: Ruben Cali</title>
      <link>https://dev.to/ruben_cali_1d63ebf3411a44</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ruben_cali_1d63ebf3411a44"/>
    <language>en</language>
    <item>
      <title>What Server-Side GTM Really Costs a 20k-Visit WordPress Site</title>
      <dc:creator>Ruben Cali</dc:creator>
      <pubDate>Wed, 02 Sep 2026 08:12:03 +0000</pubDate>
      <link>https://dev.to/ruben_cali_1d63ebf3411a44/what-server-side-gtm-really-costs-a-20k-visit-wordpress-site-alp</link>
      <guid>https://dev.to/ruben_cali_1d63ebf3411a44/what-server-side-gtm-really-costs-a-20k-visit-wordpress-site-alp</guid>
      <description>&lt;p&gt;"You need a server-side container. Everyone's moving to it."&lt;/p&gt;

&lt;p&gt;That was the whole recommendation. A client had come to us with a report that made no sense.&lt;br&gt;
One channel had been cut from the media plan for underperforming, the client's own sales&lt;br&gt;
inbox disagreed, and the answer they had already been given by a consultant was a&lt;br&gt;
GTM server container, which is one alternative among several and not automatically the right&lt;br&gt;
one at their size. Nobody had priced it. Nobody had asked who would maintain it after launch.&lt;/p&gt;

&lt;p&gt;The recommendation is not wrong. It is just frequently unpriced, and the price is where the&lt;br&gt;
decision actually lives. Here is the breakdown we did, and the line where it flips.&lt;/p&gt;
&lt;h2&gt;
  
  
  What sGTM is
&lt;/h2&gt;

&lt;p&gt;Server-side Google Tag Manager moves tag execution off the browser. Instead of the visitor's&lt;br&gt;
browser loading vendor scripts and firing requests to &lt;code&gt;google-analytics.com&lt;/code&gt;, the browser&lt;br&gt;
sends one request to a collection endpoint you control. A container running on your own&lt;br&gt;
infrastructure receives it, transforms it, and forwards it server-to-server to whatever&lt;br&gt;
destinations you configure.&lt;/p&gt;

&lt;p&gt;The reason people reach for it is request origin. A request to &lt;code&gt;google-analytics.com&lt;/code&gt; is on&lt;br&gt;
every blocklist shipped with every content blocker. A request to &lt;code&gt;metrics.yourdomain.com&lt;/code&gt; is&lt;br&gt;
first-party, and Safari's Intelligent Tracking Prevention treats cookies set on that origin&lt;br&gt;
differently from third-party ones. The mechanism is real. That is not the part under dispute.&lt;/p&gt;

&lt;p&gt;What is under dispute is that "run a container" is doing an enormous amount of work in that&lt;br&gt;
sentence. The container is a Docker image you host. Google publishes it; Google does not run&lt;br&gt;
it for you.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it really costs (infra + maintenance + specialist)
&lt;/h2&gt;

&lt;p&gt;For the 20k-visit-per-month WordPress site that started this, here is what we actually put&lt;br&gt;
on the page. Treat every figure as a range to verify against current pricing, not a quote.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line item&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Monthly&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run tagging server&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$45–$120&lt;/td&gt;
&lt;td&gt;Google's own guidance is a minimum of three instances for production; one always-on instance is not the recommended configuration.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load balancer&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$18–$25&lt;/td&gt;
&lt;td&gt;Required for a custom domain on the standard setup.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom subdomain + TLS&lt;/td&gt;
&lt;td&gt;~$0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Cheap, but it is a DNS change someone has to own.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initial build&lt;/td&gt;
&lt;td&gt;$1,200–$3,500&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Container deploy, client/tag configuration, consent wiring, QA against the existing setup.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ongoing maintenance&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$150–$400&lt;/td&gt;
&lt;td&gt;Container image updates, monitoring, breakage when a tag template changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Specialist availability&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;The real cost. When it breaks, WordPress knowledge does not help you.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Call it $1,200–$3,500 up front and $210–$545 a month, on a site whose entire hosting bill was&lt;br&gt;
$29. The infrastructure is not the expensive part, the last two rows are. A tagging server&lt;br&gt;
is a production service. It has uptime, it has an owner, and when the owner leaves, the site&lt;br&gt;
has a piece of critical measurement infrastructure that nobody on the team can debug.&lt;/p&gt;

&lt;p&gt;That was the number that ended the conversation. Not because it was unaffordable, but because&lt;br&gt;
nobody could explain what the site would get for it that it did not already have.&lt;/p&gt;
&lt;h2&gt;
  
  
  When it's worth it
&lt;/h2&gt;

&lt;p&gt;It is worth it, and I want to be precise about when, because the sGTM-is-overkill&lt;br&gt;
take is just as lazy as the everyone's-moving-to-it one.&lt;/p&gt;

&lt;p&gt;Buy the container when you have &lt;strong&gt;multiple destinations&lt;/strong&gt;. The moment you are fanning one&lt;br&gt;
event out to several platforms, server-side, with different payload shapes and different&lt;br&gt;
consent rules per destination, you want a transformation layer, and sGTM is a good one.&lt;/p&gt;

&lt;p&gt;Buy it when you need &lt;strong&gt;transformation you cannot do at the source&lt;/strong&gt;, enrichment from an&lt;br&gt;
internal API, hashing, stripping fields for a data-residency requirement.&lt;/p&gt;

&lt;p&gt;Buy it when &lt;strong&gt;someone owns it&lt;/strong&gt;. If there is a person whose job includes this container, the&lt;br&gt;
maintenance line item stops being a risk and becomes a salary you were already paying.&lt;/p&gt;

&lt;p&gt;Buy it when the &lt;strong&gt;traffic maths works&lt;/strong&gt;. At high volume, the infrastructure cost per event&lt;br&gt;
collapses and the build cost amortises to nothing.&lt;/p&gt;

&lt;p&gt;None of those were true for a WordPress site sending WooCommerce events to exactly one&lt;br&gt;
destination.&lt;/p&gt;
&lt;h2&gt;
  
  
  When a plugin is enough
&lt;/h2&gt;

&lt;p&gt;Here is the part that gets skipped. If your destination list is &lt;em&gt;one item long&lt;/em&gt; and that item&lt;br&gt;
is GA4, almost everything sGTM gives you is capability you are paying for and not using.&lt;/p&gt;

&lt;p&gt;The transport problem, requests originating from your own domain instead of a blocked&lt;br&gt;
third-party one, does not require a container. It requires an endpoint on your domain that&lt;br&gt;
can accept an event and forward it server-to-server. WordPress already has one. The REST API&lt;br&gt;
is first-party by definition, it is authenticated, and it runs on infrastructure you are&lt;br&gt;
already paying for and already know how to debug.&lt;/p&gt;

&lt;p&gt;The GA4 Measurement Protocol is a documented HTTP endpoint. A forwarded event is a POST with&lt;br&gt;
a JSON body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXX&amp;amp;api_secret=SECRET"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "client_id": "1234567.7654321",
    "events": [{
      "name": "purchase",
      "params": {
        "transaction_id": "wc_8841",
        "currency": "EUR",
        "value": 129.95
      }
    }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire protocol the container would have been speaking on your behalf. The hard&lt;br&gt;
parts are not in that request. They are &lt;code&gt;client_id&lt;/code&gt; continuity across the client/server&lt;br&gt;
boundary, deduplication when both sides fire, consent state, and getting WooCommerce's&lt;br&gt;
actual order data into the payload at the right point in the checkout lifecycle. Those are&lt;br&gt;
WordPress problems, solved in WordPress, by code that already has access to the order object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;sGTM container&lt;/th&gt;
&lt;th&gt;WordPress-side forwarding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Destinations&lt;/td&gt;
&lt;td&gt;Many, with per-destination logic&lt;/td&gt;
&lt;td&gt;One (GA4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly infrastructure&lt;/td&gt;
&lt;td&gt;$60–$145&lt;/td&gt;
&lt;td&gt;$0 — your existing host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build effort&lt;/td&gt;
&lt;td&gt;Days, specialist&lt;/td&gt;
&lt;td&gt;Minutes, setup wizard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who debugs it at 2am&lt;/td&gt;
&lt;td&gt;A tagging specialist&lt;/td&gt;
&lt;td&gt;Whoever already maintains the site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transformation layer&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;None to speak of&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ceiling&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;You will outgrow it if you add destinations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We build the right-hand column. It is called Easy Server Side Tracking, it routes WordPress&lt;br&gt;
and WooCommerce events through your own REST endpoint (or your own Cloudflare Worker, if you&lt;br&gt;
want the edge version) to the GA4 Measurement Protocol, and it is free up to 10,000 events a&lt;br&gt;
month with no signup to download it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not fix
&lt;/h2&gt;

&lt;p&gt;Server-side delivery fixes the transport. It does not fix identity, and the difference&lt;br&gt;
matters more than most write-ups admit.&lt;/p&gt;

&lt;p&gt;It does not give you cross-device identity. A visitor on mobile and the same person on&lt;br&gt;
desktop are still two people unless you have a login to tie them together.&lt;/p&gt;

&lt;p&gt;It does not override consent. If someone denies consent, you do not get to track them by&lt;br&gt;
moving the request server-side. That is the same processing with a different origin. Consent&lt;br&gt;
Mode v2 and modelling are what you have, and modelled data is an estimate.&lt;/p&gt;

&lt;p&gt;It does not undo ITP for returning visitors. First-party cookie lifetime limits still apply&lt;br&gt;
to what the browser stores; a server-side endpoint changes where the request goes, not how&lt;br&gt;
long Safari keeps the identifier.&lt;/p&gt;

&lt;p&gt;And it does not repair history. Events that were dropped last quarter are gone. Nothing&lt;br&gt;
recovers them retroactively. You are fixing the capture rate from today forward, which means&lt;br&gt;
your year-over-year comparison will show a jump that is an artefact of the fix, not growth.&lt;br&gt;
Annotate it, or you will misread your own dashboard in six months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;We did not set out to build a product. We set out to stop having the same conversation, which&lt;br&gt;
went: the campaign worked, the report says it didn't, and the gap between those two facts is&lt;br&gt;
now a budget decision. That campaign had skewed technical, its audience blocked more than&lt;br&gt;
average, and it got cut on numbers that were bent rather than merely incomplete. The next&lt;br&gt;
quarter's spend moved to a channel that measured better and converted worse, and by the time&lt;br&gt;
the revenue reporting drifted far enough from the WooCommerce admin for anyone to notice, the&lt;br&gt;
decision was two quarters old.&lt;/p&gt;

&lt;p&gt;So we built the narrow thing. GA4 only, WordPress only. No multi-destination fan-out, no&lt;br&gt;
transformation layer, no container to babysit. Everything we left out was left out on purpose,&lt;br&gt;
because each one was a reason the site owner would need to call someone.&lt;/p&gt;

&lt;p&gt;If you run it, run it in hybrid mode alongside your existing gtag.js for a week, dedup keys&lt;br&gt;
mean nothing double-counts, and compare the two event counts. If it does not capture more&lt;br&gt;
than what you have now, uninstall it. That is a real outcome and we would rather know.&lt;/p&gt;

&lt;p&gt;What we actually want back is the breakage. Which WooCommerce extension writes an order in a&lt;br&gt;
way that produces a purchase event you did not expect? That is the class of bug we cannot&lt;br&gt;
find on our own machines.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on Easy Server Side Tracking, the plugin mentioned above.&lt;br&gt;
The measurement method in this article works with any server-side setup.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>analytics</category>
      <category>webdev</category>
      <category>googleanalytics</category>
    </item>
  </channel>
</rss>
