Server-Side Cookies
The ability to set cookies via HTTP response headers is one of sGTM’s most valuable and least understood capabilities. Server-set cookies bypass Safari’s ITP restrictions on JavaScript cookies, resist browser extension clearing, and can be marked HttpOnly to prevent client-side access. Understanding how and when to use server-side cookie management separates basic sGTM deployments from implementations that deliver meaningfully better data.
The ITP problem and why server-set cookies help
Section titled “The ITP problem and why server-set cookies help”Safari’s Intelligent Tracking Prevention caps the expiration of first-party cookies set by JavaScript at 7 days (or 24 hours if the domain is classified as a tracker). This affects:
_gacookie (GA4 client ID) — capped at 7 days- Any first-party cookie your JavaScript sets for tracking purposes
The cap applies specifically to cookies set via document.cookie in JavaScript. It does not apply to cookies set via HTTP Set-Cookie response headers from a server the browser makes a first-party request to.
When your sGTM server runs at collect.yoursite.com and sets cookies in its HTTP response:
Set-Cookie: _ga=GA1.1.1234.1711900000; Max-Age=63072000; Domain=.yoursite.com; ...Safari treats this as a server-set first-party cookie. The 7-day cap does not apply. The cookie expires when the Max-Age or Expires attribute dictates — up to the maximum browsers allow (typically 400 days).
How sGTM sets cookies
Section titled “How sGTM sets cookies”Cookies are set in the HTTP response headers that sGTM sends back to the browser after processing a request. In client templates and tag templates, the setResponseHeader API writes these headers:
setResponseHeader( 'Set-Cookie', '_my_cookie=value; Max-Age=34560000; Path=/; Domain=.yoursite.com; Secure; SameSite=Lax');Automatic cookie setting: The GA4 client in sGTM automatically sets _ga, FPID, and _ga_XXXXXXXX cookies on every request via response headers. You do not need to configure this — it is the GA4 client’s default behavior.
Custom cookie setting: For custom identifiers or session tokens, write a custom client template or variable template that sets cookies via setResponseHeader.
Cookie attributes reference
Section titled “Cookie attributes reference”Every cookie should be explicitly configured. Missing attributes means browser defaults, which vary by browser version.
Set-Cookie: COOKIE_NAME=value ; Max-Age=34560000 // 400 days in seconds ; Path=/ // Available on all paths ; Domain=.yoursite.com // Available on all subdomains (leading dot) ; Secure // HTTPS only (required with SameSite=None) ; HttpOnly // Not accessible to JavaScript ; SameSite=Lax // Sent on same-site requests and top-level navigationsMax-Age vs Expires: Use Max-Age (seconds from now) rather than Expires (absolute timestamp). Max-Age is immune to client clock skew.
Domain with leading dot: Domain=.yoursite.com makes the cookie available on yoursite.com and all subdomains (www.yoursite.com, shop.yoursite.com). Without the leading dot, the cookie is restricted to the exact hostname it was set on (collect.yoursite.com) — which typically means your main site cannot read it.
Secure flag: Prevents the cookie from being sent over HTTP. Required with SameSite=None. Should be set on all tracking cookies since your sGTM endpoint requires HTTPS.
HttpOnly: Cookie is not accessible to JavaScript (document.cookie cannot read it). Use for FPID and any identifier you do not want exposed to browser-side code or extensions.
SameSite:
Strict: Only sent on same-site requests. Too restrictive for most tracking cookies.Lax: Sent on same-site requests and top-level cross-site navigation (a user clicking a link to your site). Appropriate for analytics cookies.None: Sent on all requests including cross-site (third-party context). RequiresSecure. Use only if you need the cookie available in third-party iframes or cross-site requests.
The FPID cookie
Section titled “The FPID cookie”FPID (First-Party ID) is the most important server-side cookie in sGTM. It is:
- Generated by the GA4 client as a UUID on first visit
- Set via HTTP response header (
HttpOnly, server-generated) - Read back on subsequent requests to provide a stable
client_id - Not accessible to JavaScript or browser extensions
- Prioritized over the
_gacookie by the GA4 client for user identification
The FPID provides more stable user identification than _ga because:
- It is
HttpOnly— ad blockers and extensions cannot find and delete it - It is set server-side — not subject to ITP’s JavaScript cookie cap
- Its value is a UUID, not the
GA1.1.<timestamp>.<timestamp>format that some ad blockers specifically target
In practice, FPID-enabled implementations see measurably better new-vs-returning user accuracy on Safari.
Custom cookie patterns
Section titled “Custom cookie patterns”Server-generated session identifier
Section titled “Server-generated session identifier”When you need a persistent session identifier that is not tied to GA4’s client_id:
// In a custom client template:const getCookieValues = require('getCookieValues');const generateRandom = require('generateRandom');const setResponseHeader = require('setResponseHeader');
// Read existing custom session cookieconst existingId = getCookieValues('_my_session_id')[0];
if (!existingId) { // Generate new identifier const newId = 'sid_' + generateRandom(1, 999999999).toString();
setResponseHeader( 'Set-Cookie', '_my_session_id=' + newId + '; Max-Age=34560000; Path=/; Domain=.yoursite.com; Secure; HttpOnly; SameSite=Lax' );}Cookie refresh pattern
Section titled “Cookie refresh pattern”Refreshing a cookie’s expiration on each visit prevents it from expiring on active users:
const getCookieValues = require('getCookieValues');const setResponseHeader = require('setResponseHeader');
const existingValue = getCookieValues('_ga')[0];
if (existingValue) { // Re-set the cookie with fresh expiration setResponseHeader( 'Set-Cookie', '_ga=' + existingValue + '; Max-Age=63072000; Path=/; Domain=.yoursite.com; Secure; SameSite=None' );}The GA4 client does this automatically for GA4 cookies. You only need this pattern for custom cookies.
Reading cookies from incoming requests
Section titled “Reading cookies from incoming requests”Cookies arrive at your sGTM server in the Cookie request header. Read them in client and tag templates:
const getCookieValues = require('getCookieValues');
// Read a specific cookie valueconst gaValue = getCookieValues('_ga')[0]; // returns array, take firstconst fpidValue = getCookieValues('FPID')[0];
// getCookieValues returns an array because cookies can have multiple values// For well-formed single-value cookies, [0] is the right patternIn tag templates and variable templates, you can also access cookies through the sGTM variable system: create a Cookie variable in the Variables section, and it reads the specified cookie from the request.
Consent and server-side cookies
Section titled “Consent and server-side cookies”In a properly implemented consent architecture:
- The user’s CMP collects consent
- Consent signals travel via the GA4 Measurement Protocol to your sGTM server (Consent Mode parameters)
- Your sGTM client or tags check consent state before setting tracking cookies
For the GA4 client specifically: FPID and _ga cookies should only be set when analytics_storage consent has been granted. You can implement this check in a custom client template that wraps the GA4 client’s cookie-setting behavior.
A simpler approach: implement consent enforcement on the client side (blocking GA4 tag from firing before consent), so no request reaches sGTM without consent. The server-side consent check then acts as a belt-and-suspenders confirmation.
Debugging cookie behavior
Section titled “Debugging cookie behavior”Verify cookies are being set correctly:
# Check what cookies the server sets in its responsecurl -sv https://collect.yoursite.com/g/collect \ -d "v=2&tid=G-XXXXXXXX&cid=test123&en=page_view" \ 2>&1 | grep -i "set-cookie"In the browser:
- DevTools → Application → Cookies →
yoursite.com - After a pageview, verify:
_gais present with ~2 year expirationFPIDis present with HttpOnly flag checked- Neither has a 7-day expiration (ITP is not affecting them)
Common mistakes
Section titled “Common mistakes”Setting cookies in a tag template for a subdomain that differs from the main site. If your sGTM runs at collect.yoursite.com and you set Domain=collect.yoursite.com (no leading dot), the cookie is only readable at that exact subdomain. Your main site at www.yoursite.com cannot see it. Always include the leading dot: Domain=.yoursite.com.
Setting HttpOnly on cookies that client-side code needs to read. The GA4 tag in the browser reads the _ga cookie to extract the client_id. If _ga is HttpOnly, client-side JavaScript cannot read it. Only mark FPID and identifiers that are exclusively server-side as HttpOnly.
Forgetting consent enforcement. Server-set cookies are technically invisible to browser-level consent enforcement. Ensure your sGTM server checks consent signals before setting tracking cookies.
Setting Max-Age=63072000 and not refreshing. A 2-year cookie that is never refreshed expires after 2 years even for active users. The GA4 client refreshes GA4 cookies automatically. Custom cookies need explicit refresh logic.