First-Party Server Tagging: The Case That Survived Google's Cookie Reversal
Google cancelled third-party cookie deprecation in April 2025. The argument for server-side tagging did not depend on it — ITP's seven-day cookie cap, ad-blocker loss, and main-thread cost are the real drivers, and all three still apply.
First-Party Server Tagging: The Case That Survived Google’s Cookie Reversal
Disclosure: We build first-party server-side tagging as a commercial service. This article argues for an architecture we sell. Read the “when it is not worth it” section with that in mind — it is the honest half.
Most articles on this topic open by announcing the death of the third-party cookie. That framing is wrong, and it has been wrong for over a year.
Google is not deprecating third-party cookies in Chrome. After announcing the plan in 2020 and delaying it repeatedly, Google shifted to a “user choice” model in July 2024, and in April 2025 confirmed it would not ship the choice prompt and would not deprecate third-party cookies at all. Chrome retains its existing cookie controls. The UK Competition and Markets Authority subsequently moved to release Google from the Privacy Sandbox commitments tied to the deprecation plan.
If your case for re-architecting measurement rests on an imminent Chrome deadline, you have no case. That deadline was cancelled.
The case for server-side tagging was always stronger than that deadline. Here is what it actually rests on.
The three real drivers
1. Safari and Firefox blocked cross-site tracking years ago
Safari’s Intelligent Tracking Prevention has blocked third-party cookies by default since 2020. Firefox’s Enhanced Tracking Protection did so from 2019. Brave and DuckDuckGo reject them out of the box.
Chrome’s decision changed nothing for that traffic. A meaningful share of the web has been effectively cookieless for years, and for many B2B and developer-facing sites the Safari share is well above the global average.
2. The seven-day cap is the one that actually hurts
This is the detail most discussions skip, and it is the sharpest technical argument in the set.
ITP does not only restrict third-party cookies. It caps the lifetime of cookies set client-side via document.cookie at seven days — and in some cross-site navigation cases, twenty-four hours. This applies to first-party cookies. Your own domain. Your own analytics.
Google Analytics sets _ga from JavaScript. On Safari, that cookie expires after seven days regardless of the two-year max-age in the code.
The consequence:
| Reality | What your analytics reports |
|---|---|
| One user, weekly visits over 3 months | ~12 separate new users |
| Research in March, purchase in May | Two unrelated sessions, attribution lost |
| Any consideration cycle beyond a week | Systematically broken |
For a business with a long sales cycle — which is most B2B — returning-visitor data on Safari is not slightly noisy. It is structurally wrong, and it fails in a direction that inflates new-user counts and destroys attribution.
Cookies set server-side with the Set-Cookie HTTP header, from your own domain, are not subject to the seven-day cap — provided the server setting them looks genuinely first-party to Safari. That qualification is load-bearing, and the next section explains why.
The qualification most articles omit
Safari 16.4 (April 2023) extended ITP’s heuristics to server-set cookies. A cookie set via Set-Cookie can still be capped at seven days if WebKit classifies the setting server as third-party. Two triggers are documented in practice:
- CNAME cloaking. If
metrics.yoursite.comis a CNAME to a vendor domain, Safari treats the cookie as if the vendor set it directly. - IP mismatch. If the tracking subdomain resolves to an IP whose leading octets differ from the main site’s, Safari treats it as a third-party response.
This matters directly for the architecture in this article. If yoursite.com is on one host and metrics.yoursite.com is a Cloudflare Worker, the two may resolve to unrelated IP ranges — and Safari may cap your carefully server-set cookie at seven days anyway, silently, exactly as if you had never migrated.
Three ways to handle it:
| Approach | Trade-off |
|---|---|
Same-origin path (yoursite.com/collect) rather than a subdomain |
Simplest and most robust; requires routing on the main origin |
| Reverse proxy so the collector shares the origin’s IP range | Works; adds infrastructure complexity |
| Accept the cap on Safari | Loses the primary benefit for the traffic that most needs it |
The first is the one to prefer. Route /collect on your primary domain and forward internally. There is no subdomain, no CNAME, no IP divergence, and nothing for ITP’s heuristics to classify as third-party. Most edge platforms let you attach a Worker or middleware to a path on the apex domain, so this costs you nothing architecturally.
Verify rather than assume:
# Do these share a leading IP range?
dig +short yoursite.com
dig +short metrics.yoursite.com
Then test empirically: set the cookie, wait eight days without visiting, return in Safari, and check whether the cookie survived. That is the only test that actually answers the question, because WebKit does not document the heuristics precisely and they change between releases.
3. Ad-blockers and the main thread
Blocklists match on domain. google-analytics.com, googletagmanager.com, connect.facebook.net are on every list. Estimates of blocked traffic vary enormously by audience — published figures range from under 10% to over 40%, and any single number quoted without an audience qualifier is not worth much. For a developer-facing site, assume the high end. Measure your own by comparing server log hits against tag-reported pageviews.
Meanwhile the client-side tag stack is expensive. GTM plus GA4 plus a Meta pixel plus a session-recording tool routinely adds up to more JavaScript than the site’s own application code. We are not going to quote you a kilobyte figure, because it depends entirely on which tags are in your container — open the DevTools Network panel, filter to your tag domains, and read your own number. Whatever it is, all of it parses and executes on the main thread, competing with your code. Tag containers are also a governance problem: someone with container access can push a script to production without a code review, a deploy, or a CSP change.
The architecture
Three moves:
- Serve the tagging endpoint from a subdomain of your own domain —
metrics.yoursite.com, not a vendor domain. - Set the identity cookie server-side, with
Set-Cookie,HttpOnly,Secure,SameSite=Lax. - Dispatch to vendors server-to-server, from your infrastructure, not from the browser.
Browser ──▶ metrics.yoursite.com ──▶ your collector ──┬──▶ GA4 Measurement Protocol
│ (first party) ├──▶ Meta Conversions API
│ └──▶ your own warehouse
└── receives Set-Cookie: uid=...; HttpOnly; Secure
The browser makes one request to your own domain. Everything downstream happens on your infrastructure.
The collector
// Cloudflare Worker on metrics.yoursite.com
const ALLOWED_ORIGIN = 'https://yoursite.com';
export default {
async fetch(request, env, ctx) {
if (request.method === 'OPTIONS') return preflight();
if (request.method !== 'POST') return new Response('Method not allowed', { status: 405 });
const origin = request.headers.get('Origin');
if (origin !== ALLOWED_ORIGIN) return new Response('Forbidden', { status: 403 });
// --- Consent gate. Nothing proceeds without it. ---
const consent = parseCookie(request, 'consent_state');
if (!consent?.analytics) {
return json({ ok: true, recorded: false }, origin);
}
let uid = parseCookie(request, 'uid');
const isNew = !uid;
if (isNew) uid = crypto.randomUUID();
const event = await request.json();
const enriched = {
...event,
uid,
ts: Date.now(),
// Server-side geo. No client lookup, no extra round trip.
country: request.cf?.country,
region: request.cf?.region,
// Truncate the IP. Full IP is personal data under GDPR and you
// do not need it for analytics.
ip_prefix: truncateIp(request.headers.get('CF-Connecting-IP')),
ua: request.headers.get('User-Agent'),
referer: event.referer ?? null,
};
// Respond immediately. Vendor dispatch happens after the response.
ctx.waitUntil(Promise.allSettled([
sendToGA4(enriched, env),
consent.advertising ? sendToMeta(enriched, env) : Promise.resolve(),
sendToWarehouse(enriched, env),
]));
const headers = corsHeaders(origin);
if (isNew) {
// Server-set. HttpOnly. Not subject to ITP's 7-day cap.
headers.append('Set-Cookie',
`uid=${uid}; Max-Age=63072000; Path=/; Domain=.yoursite.com; ` +
`HttpOnly; Secure; SameSite=Lax`
);
}
return new Response(JSON.stringify({ ok: true }), { headers });
},
};
function truncateIp(ip) {
if (!ip) return null;
if (ip.includes(':')) return ip.split(':').slice(0, 3).join(':') + '::'; // IPv6 /48
return ip.split('.').slice(0, 3).join('.') + '.0'; // IPv4 /24
}
ctx.waitUntil is the important line: the browser gets its response immediately while vendor dispatch continues in the background. Client latency does not depend on how many vendors you fan out to.
GA4 via Measurement Protocol
async function sendToGA4(event, env) {
const url = `https://www.google-analytics.com/mp/collect` +
`?measurement_id=${env.GA4_MEASUREMENT_ID}` +
`&api_secret=${env.GA4_API_SECRET}`;
await fetch(url, {
method: 'POST',
body: JSON.stringify({
client_id: event.uid,
timestamp_micros: event.ts * 1000,
non_personalized_ads: !event.consent_advertising,
events: [{
name: event.name,
params: {
page_location: event.url,
page_title: event.title,
engagement_time_msec: event.engagement ?? 1,
session_id: event.session_id,
...event.params,
},
}],
}),
});
}
Be aware of the limitation: Measurement Protocol events are not identical to gtag events. Some automatic dimensions are missing, and a few GA4 reports behave differently. Test against a debug property before switching over. Anyone who tells you it is a drop-in replacement has not shipped it.
The client
The entire browser-side implementation:
// ~1 KB. No vendor SDK.
const ENDPOINT = 'https://metrics.yoursite.com/collect';
export function track(name, params = {}) {
const payload = JSON.stringify({
name, params,
url: location.href,
title: document.title,
referer: document.referrer || null,
session_id: sessionId(),
});
// sendBeacon survives page unload and does not block.
if (navigator.sendBeacon) {
navigator.sendBeacon(ENDPOINT, new Blob([payload], { type: 'application/json' }));
return;
}
fetch(ENDPOINT, {
method: 'POST',
body: payload,
credentials: 'include',
keepalive: true,
}).catch(() => {});
}
A small hand-written module replacing an entire vendor tag stack. Measure the delta on your own site — filter the Network panel to your tag domains before and after — and publish the figure you get rather than one from an article.
What this does not do
Three honest limits.
It is not a consent workaround. First-party server-side collection is still processing personal data under GDPR. You still need a lawful basis, still need consent for analytics cookies under ePrivacy, and still owe access and deletion rights. Moving the collection point does not change the legal position. The consent gate in the code above is load-bearing, not decorative. Vendors who market server-side tagging as a way to “recover” data from users who declined consent are describing a compliance violation.
It is not invisible to blockers, and should not try to be. Blocklists increasingly include first-party endpoints on known patterns, and uBlock Origin maintains rules for common server-side tagging paths. More importantly: deliberately engineering around a user’s explicit choice to block tracking is a position I would not defend. The legitimate benefit here is accuracy for consenting users, not reach into non-consenting ones.
It is not free. You now operate infrastructure. Someone must monitor the collector, handle vendor API changes, manage schema drift, and stay on top of Measurement Protocol’s differences from gtag. This is a real engineering commitment. For a small site with short consideration cycles, the client-side stack is probably fine and this is over-engineering.
Migrating without losing continuity
The mistake is a hard cutover. You lose the ability to distinguish “the new pipeline is broken” from “the new pipeline is more accurate,” and those look identical in a dashboard.
Run both for a full attribution window. Send events client-side and server-side simultaneously to two separate GA4 properties. Compare daily. The server-side property should show:
| Metric | Expected change | Reason |
|---|---|---|
| Total events | Up | Ad-blocker recovery on consenting users |
| New users | Down | ITP no longer re-creating the same visitor weekly |
| Returning users | Up | The other side of the same correction |
| Sessions per user | Up | Identity persists past seven days |
| Bounce rate | Roughly flat | Should not move much; if it does, investigate |
New users falling while total events rise is the signature of a correct migration. If both rise together, you are probably double-counting — check that the client-side tag is genuinely removed and not merely hidden behind a flag that still fires.
Identity stitching across the cutover. Existing visitors carry a _ga cookie with a client ID. Read it once and adopt it, so historical users are not reset:
// One-time migration path in the collector
let uid = parseCookie(request, 'uid');
if (!uid) {
// Adopt the existing GA client ID if present, so returning
// visitors keep their identity across the migration.
const ga = parseCookie(request, '_ga'); // GA1.1.1234567890.1698765432
const legacyId = ga?.split('.').slice(-2).join('.');
uid = legacyId ?? crypto.randomUUID();
}
_ga is script-readable, so this only works while the client-side tag is still present — which is precisely why the dual-run period matters. After the cutover the legacy cookie expires and every visitor is on the server-set identifier.
Keep a kill switch. An environment variable that reverts to client-side tagging without a deploy. Measurement outages are silent — nobody files a bug because analytics is slightly wrong — so you want the rollback path to be one config change rather than an emergency release.
When it is worth it
Worth doing when: your Safari share is significant, your sales cycle exceeds a week, you have a real warehouse and use the data for decisions, or your Core Web Vitals are being dragged down by tag weight.
Not worth doing when: you are a small site, your consideration cycle is a single session, you have no data engineering capacity, or you are doing it primarily to circumvent ad-blockers — which is both technically fragile and the wrong reason.
Frequently asked questions
Is Chrome deprecating third-party cookies? No. Google shifted to a user-choice model in July 2024 and confirmed in April 2025 that it would not ship the prompt and would not deprecate third-party cookies. Chrome keeps its existing cookie controls. Safari and Firefox block them by default and have for years.
Then why move to server-side tagging? Safari’s ITP caps client-set cookies at seven days, which breaks returning-visitor tracking and attribution for any consideration cycle longer than a week. Ad-blockers remove a share of client-side tags entirely. Client tag stacks add significant main-thread JavaScript — measure yours in the Network panel. None of these depended on Chrome’s plan.
Does a server-set cookie avoid the seven-day cap?
Usually, but not automatically. The cap applies to cookies written via document.cookie. Cookies set with the Set-Cookie header are exempt only if Safari classifies the setting server as genuinely first-party. Since Safari 16.4, CNAME cloaking or an IP range that diverges from your main site can trigger the cap on server-set cookies too. Serving the collector from a path on your primary domain avoids the problem entirely.
Does this let me track users who declined consent? No, and attempting it is a GDPR and ePrivacy violation. Server-side collection is still processing personal data. Gate it on consent — the example code does exactly that.
Will ad-blockers block my first-party endpoint? Some will. Blocklists include first-party endpoints matching known patterns. Treat accuracy for consenting users as the benefit, not evasion of users who opted out.
Is GA4 Measurement Protocol equivalent to gtag? Not quite. Some automatic parameters are absent and certain reports behave differently. Test against a debug property before migrating production traffic.
What does this cost to run? Compute cost scales with request volume and varies by platform, so price it against your own traffic using the provider’s current calculator rather than a figure quoted in an article. The cost that people underestimate is engineering time: monitoring, vendor API changes, and schema maintenance are ongoing, not one-off.
Should I use Google’s server-side GTM instead of building this? It is a reasonable middle path if you want vendor tooling and a UI. You run a tagging server (usually on Google Cloud), which costs more than an edge worker and keeps you inside Google’s ecosystem. Building your own gives control and lower cost at the price of maintenance.
Sources
- Privacy Sandbox: next steps — Google’s user-choice announcement, July 2024
- WebKit: Full Third-Party Cookie Blocking and More — the seven-day cap on script-written cookies
- Snowplow: Safari ITP and cookie lifetimes — analysis of the Safari 16.4 change to server-set cookies
- GA4 Measurement Protocol reference
Corrections
25 July 2026. An earlier draft of this article stated that server-set cookies are exempt from Safari’s seven-day cap without qualification. That was incomplete: since Safari 16.4, cookies set by a server Safari classifies as third-party — via CNAME cloaking or a divergent IP range — can still be capped. The section on first-party architecture has been rewritten and now recommends a same-origin path rather than a subdomain. The original claim would have led readers to build an architecture that silently failed to deliver its main benefit.
Claims last verified 25 July 2026. This area has reversed direction before; we date claims and revise. See our editorial standards.