Introduction
When building a web service that needs to track where users come from before they register, you inevitably face a foundational architecture decision: should you persist attribution data in cookies or in localStorage?
Both approaches share the same end goal — capture campaign parameters like UTM tags and click IDs on arrival, then submit that data to your backend when the user completes registration. But the paths they take, and the risks they carry, are meaningfully different.
This post compares the two approaches across implementation, privacy constraints, and advertising ecosystem compatibility — and explains why the best production systems use both.
Criteria for Comparison
To evaluate these two approaches fairly, we will examine them across the following dimensions:
- Write timing — when and how attribution data can be recorded
- Scope and shareability — whether data is accessible across subdomains or requests
- Persistence reliability — how likely the data survives until the user registers
- ITP and browser privacy impact — how modern privacy features affect each approach
- Trust model when submitted to backend — how verifiable the data is
- Ad platform compatibility — what advertisers and ad networks actually care about
Option A: Cookies
How It Works
Cookies are written either server-side on the first request, or client-side via JavaScript after parsing the landing URL. Once set, they are automatically included in every subsequent HTTP request to your domain — no extra code needed.
A typical flow:
- User lands on
/register?utm_source=google&utm_campaign=spring2026 - Server (or client JS) sets a cookie:
attribution=source:google;campaign:spring2026; Expires=... - User browses, returns, eventually registers
- Registration request automatically includes the cookie
- Backend reads and persists attribution
Pros
- Zero-effort transmission: cookies ride along on every request automatically
- Server-side write on first touch: you can capture attribution before any JavaScript executes
- Cross-subdomain sharing: a cookie scoped to
.example.comis readable byapp.example.com,www.example.com, etc. - Well-understood by legacy ad infrastructure: older analytics platforms and affiliate networks were built around cookies
Cons
- ITP (Intelligent Tracking Prevention) is severe: Safari, which holds significant market share particularly in Japan and mobile, aggressively shortens cookie lifetimes
- Non-interaction cookies may expire in 7 days
- In some configurations, as short as 24 hours
- Even a
Max-Age=365cookie can be truncated by the browser
- Registration gaps: if your users take more than a week to register after clicking an ad, your attribution cookie may already be gone
- SameSite and Secure misconfiguration: cross-site cookies require
SameSite=None; Secure, and misconfiguring this silently breaks attribution - Perceived as tracking: cookies carry a social and regulatory association with tracking, requiring integration with consent management in GDPR/APPI contexts
- Size constraints: cookies have per-cookie and total-domain size limits; storing rich attribution JSON is impractical
What to Watch Out For
Keep tracking cookies separate from session and auth cookies. Store only a compact set of fields — source_type, campaign_id, and first_touch_ts. Do not store raw query strings.
Option B: Query Parameter Capture to localStorage
How It Works
When the user arrives, client-side JavaScript parses the URL query parameters — UTM tags, gclid, fbclid, etc. — and persists them in localStorage. This data survives page navigations and browser restarts, but lives entirely in the client.
A typical structured payload:
{
"first_touch": {
"utm_source": "google",
"utm_campaign": "spring2026",
"ts": 1737432000
},
"last_touch": {
"utm_source": "email",
"utm_campaign": "follow-up",
"ts": 1737518400
}
}When the user registers, the form or API call explicitly reads localStorage and includes this payload in the request body.
Pros
- Not subject to ITP cookie restrictions:
localStoragedoes not share the same expiration mechanics as cookies - You control the lifecycle: data persists until explicitly cleared or the user wipes browser storage
- Richer storage capacity: you can store structured JSON without size anxiety
- Lower perceived privacy footprint: data is not automatically sent on every request
Cons
- Requires JavaScript execution: if the user has JS disabled, or if your framework renders server-side before JS loads, the first touch may be missed
- Cannot be read server-side directly: the backend only sees this data when the frontend explicitly submits it
- Subdomain isolation:
localStorageis strictly origin-scoped —app.example.comandwww.example.comcannot share it without a bridging mechanism (cookie relay, postMessage, or redirect) - Vulnerable to overwrite: if you do not implement a clear attribution strategy (first-touch wins vs. last-touch wins), later visits may silently overwrite earlier, more valuable attribution
- Fully client-controlled: the backend must treat this data as user-supplied and potentially unverifiable
What to Watch Out For
Whitelist only the parameters you actually need: utm_*, gclid, fbclid. Do not store raw referrer URLs — these can leak sensitive path information. Always include timestamps so you can validate attribution windows on the backend.
Comparison Table
| Dimension | Cookies | localStorage |
|---|---|---|
| Write timing | Server-side or client-side | Client-side JS only |
| Automatic request inclusion | Yes | No |
| Cross-subdomain sharing | Yes (with domain config) | No |
| ITP / Safari impact | Severe (7-day cap) | Minimal |
| Persistence reliability | Lower (browser may truncate) | Higher (user-controlled) |
| Backend trust level | Semi-trusted | Weakly trusted (client-supplied) |
| Storage flexibility | Low (size limits) | High (structured JSON) |
| Ad platform requirement | Not required | Not required |
| Requires consent handling | Often yes | Often yes |
On Ad Platforms: What Advertisers Actually Care About
This is one of the most commonly misunderstood aspects of attribution tracking. Ad platforms do not care whether you use cookies or localStorage. They cannot see either one.
What ad platforms need is one of two things:
1. A conversion pixel fired on the success page
// Google Ads example
gtag('event', 'conversion', {
'send_to': 'AW-XXXXXXXXX/XXXXXXXXX'
});
// Meta example
fbq('track', 'CompleteRegistration');This tells the platform "a conversion happened here." It is entirely independent of how you stored attribution data.
2. A server-side conversion API call
After your database confirms the registration was saved, your backend sends an HTTP call to the platform's conversion API (Google Enhanced Conversions, Meta CAPI, etc.) with the click ID and event details.
In both cases, your registration count comes from your own database. The ad platform never reads your cookies, never polls your website, and never automatically knows when a user registers. There is no mechanism by which writing document.cookie = "registered=true" increments anything on the advertiser's dashboard.
The role of cookies and localStorage in the ad ecosystem is narrower than most people assume:
They are not counters. They are connectors.
Their only job is to preserve the click ID (gclid, fbclid, etc.) from the moment of the ad click until the conversion event fires — so the platform can attribute that conversion to the original click. Whether you store that click ID in a cookie or in localStorage is an internal implementation choice that no ad platform will audit.
The only things that can break ad attribution are:
- The click ID being lost before conversion fires (cookie expired, localStorage overwritten)
- The conversion event not being sent at all
- The conversion firing outside the attribution window (typically 7–30 days depending on platform)
Recommendation: Use Both in Production
For any system that needs long-term, auditable, ad-compatible attribution, the right answer is not a choice between cookies and localStorage — it is using both with a defined fallback hierarchy.
Recommended Architecture
Step 1: Parse attribution parameters on landing
Extract utm_*, gclid, fbclid, and any custom parameters. Determine source_type (paid / organic / direct / referral).
Step 2: Dual-write on arrival
- Write a compact cookie (backup): store only
source_type,campaign_id, andfirst_touch_ts - Write a full JSON object to localStorage (primary): store the complete attribution context including click IDs and timestamps
Step 3: Submit unified payload at registration
{
"attribution": {
"source": "google",
"campaign": "spring2026",
"first_touch_ts": "2026-01-21T08:00:00Z",
"last_touch_ts": "2026-01-24T14:30:00Z",
"click_ids": {
"gclid": "Cj0KCQiA...",
"fbclid": null
},
"storage_source": ["localStorage", "cookie"]
}
}Step 4: Backend fallback logic
- Prefer localStorage data (richer, more reliable)
- Fall back to cookie data if localStorage is empty or malformed
- Record
storage_sourcein your attribution table for downstream auditability
Step 5: Fire conversion signals
After successful database write, trigger your conversion pixel and/or server-side API call to the relevant ad platforms. This is the only step that actually updates the platform's count.
Additional Backend Considerations
- Implement first-touch-wins as the default policy; allow explicit last-touch override for specific campaign types
- Validate that
first_touch_tsis within a reasonable window (e.g., no more than 90 days before registration) - Record
storage_sourceso you can later analyze data quality (localStorage-sourced attribution is generally more complete) - For multi-device journeys, attribution breaks regardless of storage mechanism — a logged-in user identity graph is needed for that case
Summary
| Cookies | localStorage | |
|---|---|---|
| Best for | First-touch capture, legacy compatibility, server-side read | Long-lived persistence, structured data, ITP resistance |
| Main risk | Expires before user registers (Safari/ITP) | Overwritten, subdomain isolation |
| Ad platform role | Click ID relay only | Click ID relay only |
| Conversion counting | Your DB + your conversion pixel | Your DB + your conversion pixel |
| Production recommendation | Use as fallback | Use as primary |
The mental model that clarifies everything:
Cookies exist to be understood by legacy systems. localStorage exists to survive the browser. Neither of them counts your registrations — your database does, and you tell the ad platform about it yourself.
If you are building a system meant to run for years with measurable ad ROI, dual-write from day one. The extra 10 lines of code will save you from invisible attribution gaps that are nearly impossible to diagnose retroactively.
