Extending Cookie Lifetimes
Safari’s ITP caps JavaScript-set cookies at 7 days. One technical fact makes this problem solvable: cookies set via HTTP Set-Cookie response headers from your own server are not subject to this cap. Safari honors the full max-age or expires you specify in server-set cookies, regardless of ITP.
This article covers the practical implementations — from sGTM’s built-in cookie refresh to Cloudflare Worker patterns — and the legal context that makes these approaches legitimate only when consent is present.
Why server-set cookies bypass ITP
Section titled “Why server-set cookies bypass ITP”ITP’s cookie caps specifically target JavaScript’s document.cookie API. When JavaScript sets a cookie, ITP knows the cookie was set client-side and applies the cap. When a server sets a cookie in an HTTP response header, the browser treats it as a server decision and honors the specified expiry without modification.
This distinction exists because ITP is targeting a specific attack vector: client-side JavaScript used by third-party trackers to set first-party cookies via CNAME cloaking or link decoration workarounds. The assumption is that a server setting a cookie on its own domain is doing so as a first party, not as a cross-site tracker.
The practical result: if your sGTM server at sgtm.yourdomain.com sets a _ga cookie in its HTTP response with a 2-year expiry, Safari will keep that cookie for 2 years. The same cookie set by Google Analytics JavaScript would be deleted in 7 days.
Method 1: The sGTM cookie refresh pattern
Section titled “Method 1: The sGTM cookie refresh pattern”Server-side GTM’s GA4 client can automatically handle cookie refresh as part of normal request processing. When a GA4 request passes through your sGTM server, the server reads the incoming _ga value from the request and re-sets it in the response headers with the full 2-year expiry.
This happens on every request that passes through sGTM — so every page view, event, or conversion that routes through your server refreshes the cookie lifetime. As long as users visit within 2 years, the cookie persists.
How to configure it:
In your sGTM GA4 Client tag, there is a setting for “Override Cookie on Response.” When enabled, the GA4 client will write the _ga value back to the response as a Set-Cookie header.
The key settings:
- Cookie name:
_ga - Domain: Your root domain (e.g.,
.yourdomain.comwith the leading dot for all subdomains) - SameSite:
Lax(required for cross-page navigation to work) - Secure:
true - HttpOnly:
false(must be false — GA4 JavaScript needs to read this cookie) - Max-age:
63072000(2 years in seconds)
After configuration, monitor the Network tab in Safari DevTools on your site. The cookie set in the server response should show the 2-year expiry, not the 7-day ITP cap.
Method 2: Cloudflare Worker cookie refresh
Section titled “Method 2: Cloudflare Worker cookie refresh”If you are using Cloudflare, you can implement cookie refresh at the CDN edge without running sGTM. A Cloudflare Worker intercepts responses from your origin server and injects Set-Cookie headers that refresh the _ga cookie.
// Cloudflare Worker: _ga cookie refresh// Deploy to a route pattern like *.yourdomain.com/*
addEventListener('fetch', event => { event.respondWith(handleRequest(event.request));});
async function handleRequest(request) { // Pass the request to your origin const response = await fetch(request);
// Only refresh cookies for HTML page responses const contentType = response.headers.get('Content-Type') || ''; if (!contentType.includes('text/html')) { return response; }
// Read the _ga cookie from the incoming request const cookieHeader = request.headers.get('Cookie') || ''; const gaMatch = cookieHeader.match(/_ga=([^;]+)/);
if (!gaMatch) { // No _ga cookie exists — nothing to refresh return response; }
const gaValue = gaMatch[1];
// Clone the response so we can modify headers const newResponse = new Response(response.body, response);
// Set the _ga cookie with a full 2-year expiry via server response header newResponse.headers.append('Set-Cookie', `_ga=${gaValue}; Domain=.yourdomain.com; Path=/; Max-Age=63072000; Secure; SameSite=Lax` );
return newResponse;}Deployment steps:
- Go to Cloudflare Workers in your Cloudflare dashboard
- Create a new Worker with the code above
- Replace
.yourdomain.comwith your actual domain - Deploy and assign a route that covers your site (
*.yourdomain.com/*) - Verify in Safari DevTools that the
_gacookie shows a 2-year expiry
AWS CloudFront Functions alternative:
// CloudFront Function: _ga cookie refresh (runs on viewer response)function handler(event) { var response = event.response; var request = event.request;
var cookies = request.cookies;
if (cookies && cookies['_ga']) { var gaValue = cookies['_ga'].value; var twoYears = 63072000;
response.cookies['_ga'] = { value: gaValue, attributes: `Domain=.yourdomain.com; Path=/; Max-Age=${twoYears}; Secure; SameSite=Lax` }; }
return response;}Method 3: Stape’s Cookie Keeper
Section titled “Method 3: Stape’s Cookie Keeper”Stape is a managed sGTM hosting provider that offers a feature called Cookie Keeper — a pre-built implementation of the cookie refresh pattern. It handles the technical setup of reading incoming cookies and re-setting them in server responses without requiring you to write custom Worker code.
Cookie Keeper is a “power-up” in Stape’s pricing, meaning it costs extra beyond the base sGTM hosting plan. For teams that are already using Stape for sGTM hosting and want a turnkey solution, it is a reasonable option. For teams who want full control over the implementation, the Cloudflare Worker or sGTM native approach is preferable.
Method 4: The FPID cookie (server-side identifier)
Section titled “Method 4: The FPID cookie (server-side identifier)”Beyond refreshing existing cookies, sGTM’s GA4 client also sets an FPID (First Party ID) cookie. FPID is a first-party user identifier set entirely by the server, independent of the JavaScript-set _ga cookie.
FPID provides a more durable identifier than _ga because:
- It is server-set from the first visit — never subject to ITP’s JavaScript cap
- It has a 400-day lifetime by default
- It is
HttpOnly: true— JavaScript cannot read or modify it, which also means it survives attempts to clear it via JavaScript
When FPID is present, the GA4 client in sGTM uses it as the primary identifier for the client_id, and the JavaScript-set _ga is used as a fallback. This means even if _ga expires after 7 days in Safari, the FPID survives and the user is still recognized. See FPID Cookie Management for full details.
Verifying your implementation
Section titled “Verifying your implementation”After implementing any cookie refresh method, verify it is working in Safari:
- Open Safari on a real iPhone or Mac (or use Safari’s Web Inspector with Responsive Design Mode)
- Clear all cookies and site data for your domain
- Visit your site — the
_gaand FPID cookies should be set on the first page load - Inspect the cookie expiry in Safari Developer Tools → Storage → Cookies
- If cookie refresh is working correctly,
_gashould show an expiry date approximately 2 years from now (not 7 days) - Simulate ITP’s behavior: delete
_gaand reload the page. The sGTM server should re-set_gawith the correct value
# Test cookie refresh with curl# This simulates what sGTM does: read the incoming cookie, write it back in the response
curl -v -H "Cookie: _ga=GA1.1.123456789.1700000000" \ https://sgtm.yourdomain.com/g/collect \ 2>&1 | grep -i "set-cookie"
# Expected output should include:# < set-cookie: _ga=GA1.1.123456789.1700000000; Max-Age=63072000; ...Legal context
Section titled “Legal context”Server-set cookie lifetime extension is not a consent bypass.
The question of whether this technique is legal is separate from whether it is technically possible. GDPR and ePrivacy require consent before setting non-essential cookies — regardless of whether they are set via JavaScript or via server response headers. A server-set _ga cookie without prior consent is the same legal violation as a JavaScript-set _ga cookie without consent.
What is different legally is the persistence. If a user has consented to analytics cookies, extending those cookies to their full 2-year lifetime (instead of being capped at 7 days by ITP) is entirely legitimate. You are not collecting new data — you are maintaining the user identifier that the user agreed to when they accepted your analytics cookies.
The legitimate use case: User consents → you set _ga and FPID → sGTM refreshes them on each visit → user retains the same identifier for the full consented period.
The illegitimate use case: User declines analytics cookies → you set _ga and FPID anyway via server headers → ITP cannot cap them → user is tracked against their explicit choice.
Only the first scenario is acceptable. Your consent management platform should gate the sGTM GA4 client (and therefore the cookie refresh) on analytics consent.
Common mistakes
Section titled “Common mistakes”Refreshing cookies for all users regardless of consent state
Section titled “Refreshing cookies for all users regardless of consent state”The sGTM cookie refresh should be conditional on consent. If a user has not consented to analytics cookies, the sGTM GA4 client should not be running — and therefore should not be setting or refreshing cookies. Configure your sGTM container to respect the consent state passed from the client.
Setting cookies on the wrong domain scope
Section titled “Setting cookies on the wrong domain scope”Cookies set by sGTM need to be accessible by the GA4 JavaScript on the client. If your sGTM server is at sgtm.yourdomain.com and you set the cookie with Domain=sgtm.yourdomain.com, the GA4 script on www.yourdomain.com cannot read it. Set the cookie with Domain=.yourdomain.com (leading dot) to cover all subdomains.
Using HttpOnly on the _ga cookie
Section titled “Using HttpOnly on the _ga cookie”_ga must be readable by JavaScript because the GA4 script needs to read the client_id to include it in outbound requests. Setting HttpOnly: true on _ga breaks the GA4 JavaScript. Only the FPID cookie is correctly set with HttpOnly: true — it is read by the server, not by JavaScript.