User Tools

Site Tools


privacy:browser_storage

Browser Storage Beyond Cookies

A cookie is one of at least six places a website can leave something on the visitor's machine, and it is the only one your crawler probably records. This page is about the others: Web Storage (localStorage and sessionStorage), IndexedDB, Cache Storage and the service worker that owns it, the HTTP and favicon caches when they are used to hold an identifier, and the historical WebSQL and Flash LSO. What you do with the cookies themselves is Cookies; whether your crawl carries state from one site to the next is Stateful stateless; identifying a visitor without storing anything at all is Fingerprinting. This page is the fourth corner: state that persists, is not a Set-Cookie, and is therefore missing from most measurements that claim to be about persistence.

The field measures cookies and reports it as storage. Of the 1,120 papers in our corpus that ran an automated web crawl, 124 (11.1%) ever name Web Storage at all (localStorage, sessionStorage, “Web Storage” or “DOM storage”); 28 (2.5%) name IndexedDB, and 8 (0.7%) the Cache API. Hand-auditing the 120 papers in the whole 5,859-paper corpus that name a storage API at least three times leaves 43 that actually measured a non-cookie client-side store — across seventeen years and seven venues, fewer than three a year. (Both denominators matter and they are different: 1,120 for the mention rates, 5,859 for the candidate set.)

That is not a gap in coverage so much as a gap in validity. The one paper that reports third-party access shares for cookies, localStorage and IndexedDB from the same crawl found that 90.98% of localStorage accesses and 72.49% of IndexedDB accesses on Tranco's top 10,000 sites are performed by third-party scripts, against 89.84% of cookie accesses [1Kancherla, Gayatri Priyadarsini; Goel, Dishank; Bichhawat, Abhishek (2025): "Least Privilege Access for Persistent Storage Mechanisms in Web Browsers", in: Proceedings of the ACM Web Conference. (DOI)]. So the third-party share of localStorage access is not a smaller version of the cookie problem — it is the same share — and IndexedDB is not far behind. That is a share of accesses, not a volume: it does not say the stores are equally busy. And exactly one paper in this corpus has measured it. (One crawl, one instrument, 2025: a modified Firefox Nightly 98.0a1 over tranco-list.eu/list/LYLP4. The measure is who performs the access, not whether a value is shared across sites.)

What to Read First

  • Least Privilege Access for Persistent Storage Mechanisms in Web Browsers [1Kancherla, Gayatri Priyadarsini; Goel, Dishank; Bichhawat, Abhishek (2025): "Least Privilege Access for Persistent Storage Mechanisms in Web Browsers", in: Proceedings of the ACM Web Conference. (DOI)], TheWebConf 2025 — the only paper in the corpus that instruments cookies, localStorage and IndexedDB together on one crawl and reports a comparable figure for each. ([2Zhou, Yuchen; Evans, David (2015): "Understanding and Monitoring Embedded Web Scripts", in: Proceedings of the IEEE Symposium on Security and Privacy. (DOI)] instrumented all three a decade earlier, but reported policy alarms rather than shares.) Read it for the numbers, and for the fact that it had to be written in 2025.
  • Awakening the Web's Sleeper Agents [3Karami, Soroush; Ilia, Panagiotis; Polakis, Jason (2021): "Awakening the Web's Sleeper Agents: Misusing Service Workers for Privacy Leakage", in: Proceedings of the Network and Distributed System Security Symposium. (Link)], NDSS 2021 — service workers and the Cache Storage they own, measured across the Alexa top million. The best single treatment of a store that is not a key-value pair.
  • Tales of Favicons and Caches [4Solomos, Konstantinos; Kristoff, John; Kanich, Chris; Polakis, Jason (2021): "Tales of Favicons and Caches: Persistent Tracking in Modern Browsers", in: Proceedings of the Network and Distributed System Security Symposium. (Link)], NDSS 2021 — the favicon cache is a store; when they measured it, it survived “clear browsing data” and it survived incognito. Read it as the general lesson about caches, not as a favicon paper.
  • The Web Never Forgets [5Acar, Gunes; Eubank, Christian; Englehardt, Steven; Juarez, Marc; Narayanan, Arvind; Díaz, Claudia (2014): "The Web Never Forgets: Persistent Tracking Mechanisms in the Wild", in: Proceedings of the ACM SIGSAC Conference on Computer and Communications Security. (DOI)], CCS 2014 — the origin of “measure the store, not the cookie”. Its IndexedDB figure (20 sites out of 100,000) is the 2014 baseline everything since should be compared against.
  • Navigating Murky Waters [6Ali, Mir Masood; Chitale, Binoy; Ghasemisharif, Mohammad; Kanich, Chris; Nikiforakis, Nick; Polakis, Jason (2023): "Navigating Murky Waters: Automated Browser Feature Testing for Uncovering Tracking Vectors", in: Proceedings of the Network and Distributed System Security Symposium. (Link)], NDSS 2023 — the enumeration. Twenty-one distinct browser mechanisms usable as a tracking vector, thirteen of them cross-site, tested across 126 versions of seven browsers. If you are asking “what else is there”, this is the list.
  • Then, for the reason your own numbers may be wrong: What a reset actually resets, which measures what each reset in a modern crawler does and does not clear.

The Stores, as Measurement Targets

The properties that matter for a measurement are not the API's ergonomics. They are: can your crawler see it, is it partitioned by top-level site, and what makes it go away. The last column is what separates a store you can measure once from a store whose contents depend on how long your crawl ran.

Store How a crawler sees it Partitioned by top-level site What removes it
Cookies HTTP headers and document.cookie; the one store a crawler records by default No in Chrome (CHIPS is opt-in per cookie); yes in Firefox since 103; Safari blocks third-party cookies outright rather than partitioning them Expiry, Clear-Site-Data: "cookies", a cookie clear
localStorage JS read, CDP DOMStorage, or the profile on disk. Silent unless you ask Yes in Chrome since 115, Firefox and Safari; not in a default Playwright crawl — see below Clear-Site-Data: "storage", site-data clear, quota eviction. No expiry of its own
sessionStorage Same, but scoped to the tab Yes Closing the tab. A per-visit crawler destroys it by construction
IndexedDB JS read, CDP IndexedDB, or storage/default/<origin>/idb/ in the profile Yes, same as localStorage Same as localStorage. No expiry
Cache Storage JS caches API or CDP CacheStorage. Usually written by a service worker Yes Same as localStorage; also whatever the service worker's own eviction code does
Service worker registration navigator.serviceWorker.getRegistrations() or CDP ServiceWorker. Runs outside the page context, so a request recorder attached to the page misses its traffic [7Hantke, Florian; Snyder, Peter; Haddadi, Hamed; Stock, Ben (2025): "Web Execution Bundles: Reproducible, Accurate, and Archivable Web Measurements", in: Proceedings of the USENIX Security Symposium. (Link)] Yes Unregistration, Clear-Site-Data: "storage"
HTTP cache Not a readable store: you infer it from whether a request was re-issued Partitioned by a network isolation key in Chromium and Gecko; WebKit partitions “some kinds of network state” [8Snyder, Peter; Karami, Soroush; Edelstein, Arthur; Livshits, Benjamin; Haddadi, Hamed (2023): "Pool-Party: Exploiting Browser Resource Pools for Web Tracking", in: Proceedings of the USENIX Security Symposium. (Link)] Clear-Site-Data: "cache", cache clear. Not by clearing cookies
Favicon cache Not readable at all; probed by timing [4Solomos, Konstantinos; Kristoff, John; Kanich, Chris; Polakis, Jason (2021): "Tales of Favicons and Caches: Persistent Tracking in Modern Browsers", in: Proceedings of the Network and Distributed System Security Symposium. (Link)] Not in 2021, when it was measured. Browsers have partitioned network state since; whether the favicon cache specifically is partitioned in each browser today is not established here1) Very little, in 2021: it survived “clear browsing data” and incognito
WebSQL Historical Removed from Chromium in version 119, late 2023. [6Ali, Mir Masood; Chitale, Binoy; Ghasemisharif, Mohammad; Kanich, Chris; Nikiforakis, Nick; Polakis, Jason (2023): "Navigating Murky Waters: Automated Browser Feature Testing for Uncovering Tracking Vectors", in: Proceedings of the Network and Distributed System Security Symposium. (Link)] tested it as a live tracking vector earlier that year and was right at the time; if you are reproducing a pre-2024 storage inventory, this is the row that has since gone away2)
Flash LSO Historical Flash reached end of life in December 2020. Present in the corpus only as 2014-era respawning work [5Acar, Gunes; Eubank, Christian; Englehardt, Steven; Juarez, Marc; Narayanan, Arvind; Díaz, Claudia (2014): "The Web Never Forgets: Persistent Tracking Mechanisms in the Wild", in: Proceedings of the ACM SIGSAC Conference on Computer and Communications Security. (DOI)]

Three consequences a methods section has to face.

localStorage and IndexedDB have no expiry. A cookie carries a lifetime and a paper can report the distribution of it; a localStorage key has none, and the only things that remove it are the user, the quota system, and — on Safari only — a tracking-prevention timer. Sites use it accordingly. In a crawl of Tranco sites with eight browser profiles the authors counted 12 localStorage keys per site on average (SD 31, min 1, max 2,720), and explained the pattern by noting that “the data stored in local storage has a longer lifespan than cookies” [9Demir, Nurullah; Urban, Tobias; Pohlmann, Norbert; Wressnegger, Christian (2024): "A Large-Scale Study of Cookie Banner Interaction Tools and their Impact on Users' Privacy", in: Proceedings on Privacy Enhancing Technologies, pp. 5-20. (DOI)]. If your longitudinal design assumes state decays, it does not.

A long crawl can lose the storage it already recorded. Web Storage is capped at roughly 5 MiB per origin, but IndexedDB and Cache Storage share a quota measured in tens of percent of the disk, and under storage pressure the browser evicts by origin, least-recently-used first: “The data from the least recently used origin is deleted… When an origin's data is evicted by the browser, all of its data, not parts of it, is deleted at the same time.”3) A stateful crawl over tens of thousands of origins in a container with a small disk is therefore a crawl in which the earliest origins' storage may no longer exist by the time you dump the profile. If your design accumulates storage rather than reading it per visit, read out at the end of each visit, or record the disk headroom you ran with.

Safari deletes it anyway, and that is a vantage-point variable. WebKit's tracking prevention “deletes all cookies created in JavaScript and all other script-writeable storage after 7 days of no user interaction with the website”, and names IndexedDB, LocalStorage, media keys, SessionStorage, and service worker registrations and cache as the storage covered.4) A crawl driving Safari and a crawl driving Chrome are therefore not measuring the same store, and neither is measuring what the other one's users experience.

Reading the Stores Out of a Crawl

What each capture method actually returns

Measured, not recalled. One local origin writes a marker into every store; three readout methods are then compared. A blank is a silent hole: the call succeeds and the value is simply absent.

Store page.evaluate() CDP storageState() storageState({indexedDB: true})
cookie
localStorage
sessionStorage
IndexedDB
Cache Storage ✓ (entry URLs)
service worker registration

Playwright 1.62.1, Chromium 151.0.7922.34, Linux. Reproduce with the script below.

Read off the three things that will cost you data.

  • Playwright's storageState() is the obvious way to serialise a profile, and it captures two of the six stores by default. Its own documentation is accurate and easy to misread: it returns “cookies, local storage snapshot, IndexedDB snapshot and virtual WebAuthn credentials”, and IndexedDB is behind an option that is off unless you pass { indexedDB: true }.5) sessionStorage, Cache Storage and service workers are not in it at all, and there is no option that adds them.
  • CDP sees everything, and hands IndexedDB back as a debugger handle rather than a value. IndexedDB.requestData returns a Runtime.RemoteObject: a primitive arrives in .value, an object arrives as a truncatable preview, and reading a large record needs a follow-up Runtime.getProperties. Budget for that before you write the extraction.
  • Omit indexName. Passing the empty string — the form in a lot of older sample code, and what the protocol description used to suggest — fails in Chromium 151 with Could not get index. The current protocol marks the parameter optional: “Index name. If not specified, it performs an object store data request.”6) The failure is loud, which is the good case; the surrounding try/catch in someone else's crawler is what makes it silent.

The code

storage_readout.mjs
// What a crawler actually captures when a page uses more than cookies.
//
// One local origin writes a cookie, localStorage, sessionStorage, IndexedDB,
// Cache Storage and a service worker, then three readout methods are compared:
//
//   1. page.evaluate()            - same-origin JS, what most crawlers do
//   2. Chrome DevTools Protocol   - DOMStorage / IndexedDB / CacheStorage / ServiceWorker
//   3. context.storageState()     - Playwright's own profile serialiser
//
// Run:  PLAYWRIGHT_BROWSERS_PATH=/path/to/browsers node storage_readout.mjs
//
// A missing store is a silent hole: the readout returns successfully and the
// value is simply absent, which is why this is worth measuring rather than
// recalling.
 
import http from 'node:http';
import { chromium } from 'playwright';
import pkg from 'playwright/package.json' with { type: 'json' };
 
const MARK = 'ID-4b7f2a';
 
const PAGE = `<!doctype html><meta charset=utf-8><title>store</title>
<script>
window.__ready = (async () => {
  document.cookie = 'ck=${MARK}; SameSite=Lax; max-age=3600';
  localStorage.setItem('ls', '${MARK}');
  sessionStorage.setItem('ss', '${MARK}');
  await new Promise((res, rej) => {
    const r = indexedDB.open('store', 1);
    r.onupgradeneeded = () => r.result.createObjectStore('kv');
    r.onsuccess = () => {
      const tx = r.result.transaction('kv', 'readwrite');
      tx.objectStore('kv').put('${MARK}', 'idb');
      tx.oncomplete = () => { r.result.close(); res(); };
      tx.onerror = () => rej(tx.error);
    };
    r.onerror = () => rej(r.error);
  });
  const c = await caches.open('cachev1');
  await c.put('/cached.txt', new Response('${MARK}'));
  await navigator.serviceWorker.register('/sw.js');
  return 'done';
})();
</script>`;
 
const SW = `self.addEventListener('install', () => self.skipWaiting());`;
 
const server = http.createServer((req, res) => {
  if (req.url === '/sw.js') {
    res.writeHead(200, { 'content-type': 'application/javascript', 'cache-control': 'no-store' });
    return res.end(SW);
  }
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
  res.end(PAGE);
});
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const ORIGIN = `http://127.0.0.1:${server.address().port}`;
 
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(ORIGIN + '/');
await page.evaluate(() => window.__ready);
// NOT `controller !== undefined`: before a page is controlled `controller` is
// `null`, so that predicate is true on the first evaluation and waits for
// nothing. `ready` resolves once the registration is active, which is what the
// readout below actually needs.
await page.evaluate(() => navigator.serviceWorker.ready);
 
// ---- 1. same-origin page.evaluate -----------------------------------------
const viaEval = await page.evaluate(async () => {
  const idb = await new Promise((res) => {
    const r = indexedDB.open('store', 1);
    r.onsuccess = () => {
      const g = r.result.transaction('kv', 'readonly').objectStore('kv').get('idb');
      g.onsuccess = () => { const v = g.result ?? null; r.result.close(); res(v); };
      g.onerror = () => { r.result.close(); res(null); };
    };
    r.onerror = () => res(null);
  });
  const c = await caches.open('cachev1');
  const hit = await c.match('/cached.txt');
  return {
    cookie: /ck=([\w-]+)/.exec(document.cookie)?.[1] ?? null,
    localStorage: localStorage.getItem('ls'),
    sessionStorage: sessionStorage.getItem('ss'),
    indexedDB: idb,
    cacheStorage: hit ? await hit.text() : null,
    serviceWorkers: (await navigator.serviceWorker.getRegistrations()).length,
  };
});
 
// ---- 2. Chrome DevTools Protocol ------------------------------------------
const cdp = await context.newCDPSession(page);
const frameTree = await cdp.send('Page.getFrameTree');
const { storageKey } = await cdp.send('Storage.getStorageKeyForFrame', {
  frameId: frameTree.frameTree.frame.id,
});
 
await cdp.send('DOMStorage.enable');
const dom = async (isLocal) =>
  (await cdp.send('DOMStorage.getDOMStorageItems', {
    storageId: { storageKey, isLocalStorage: isLocal },
  })).entries;
 
const dbNames = await cdp.send('IndexedDB.requestDatabaseNames', { storageKey });
await cdp.send('IndexedDB.enable');
// `indexName` is optional: "If not specified, it performs an object store data
// request." Passing the empty string - the form in a lot of older sample code -
// fails in Chromium 151 with "Could not get index". Both are asserted here so a
// future Chromium change shows up as a failed assertion rather than as silence.
let idbEmptyIndexError = null;
try {
  await cdp.send('IndexedDB.requestData', {
    storageKey, databaseName: 'store', objectStoreName: 'kv',
    indexName: '', skipCount: 0, pageSize: 10,
  });
} catch (e) { idbEmptyIndexError = e.message.split('\n')[0]; }
const idbData = await cdp.send('IndexedDB.requestData', {
  storageKey, databaseName: 'store', objectStoreName: 'kv', skipCount: 0, pageSize: 10,
});
 
const cacheList = await cdp.send('CacheStorage.requestCacheNames', { storageKey });
const cacheEntries = await cdp.send('CacheStorage.requestEntries', {
  cacheId: cacheList.caches[0].cacheId,
  skipCount: 0,
  pageSize: 10,
});
const swReg = await cdp.send('ServiceWorker.enable').then(() =>
  new Promise((res) => {
    cdp.on('ServiceWorker.workerRegistrationUpdated', (e) => res(e.registrations));
    setTimeout(() => res([]), 2000);
  })
);
const cdpCookies = await context.cookies(ORIGIN);
 
const viaCdp = {
  cookie: cdpCookies.find((c) => c.name === 'ck')?.value ?? null,
  localStorage: (await dom(true)).find((e) => e[0] === 'ls')?.[1] ?? null,
  sessionStorage: (await dom(false)).find((e) => e[0] === 'ss')?.[1] ?? null,
  // CDP hands back a Runtime.RemoteObject, not the value: for a primitive the
  // string is in `.value`, for an object you get a truncatable `preview`.
  indexedDB: idbData.objectStoreDataEntries[0]?.value?.value
    ?? idbData.objectStoreDataEntries[0]?.value?.description ?? null,
  indexedDBDatabases: dbNames.databaseNames,
  cacheStorage: cacheEntries.cacheDataEntries[0]?.requestURL ?? null,
  serviceWorkers: swReg.length,
};
 
// ---- 3. Playwright storageState, with and without the indexedDB opt-in -----
function readState(state) {
  const origin = state.origins.find((o) => o.origin === ORIGIN) ?? {};
  return {
    cookie: state.cookies.find((c) => c.name === 'ck')?.value ?? null,
    localStorage: origin.localStorage?.find((e) => e.name === 'ls')?.value ?? null,
    sessionStorage: origin.sessionStorage?.find((e) => e.name === 'ss')?.value ?? null,
    indexedDB: JSON.stringify(origin.indexedDB ?? null).includes(MARK) ? MARK : null,
    cacheStorage: JSON.stringify(state).includes('cachev1') ? 'present' : null,
    serviceWorkers: JSON.stringify(state).includes('sw.js') ? 'present' : null,
  };
}
const viaStateDefault = readState(await context.storageState());
const viaState = readState(await context.storageState({ indexedDB: true }));
 
const STORES = ['cookie', 'localStorage', 'sessionStorage', 'indexedDB', 'cacheStorage', 'serviceWorkers'];
const show = (v) => (v === null || v === 0 ? 'MISSING' : String(v));
console.log(`Playwright ${pkg.version}, Chromium ${browser.version()}`);
console.log(`marker written to every store: ${MARK}\n`);
const COLS = [
  ['page.evaluate()', viaEval, 18],
  ['CDP', viaCdp, 34],
  ['storageState()', viaStateDefault, 16],
  ['storageState({indexedDB:true})', viaState, 30],
];
console.log('store'.padEnd(16) + COLS.map(([h, , w]) => h.padEnd(w)).join(' '));
console.log('-'.repeat(16) + ' ' + COLS.map(([, , w]) => '-'.repeat(w)).join(' '));
for (const s of STORES) {
  console.log(s.padEnd(16) + ' ' + COLS.map(([, o, w]) => show(o[s]).padEnd(w)).join(' '));
}
console.log('\nCDP IndexedDB databases seen:', JSON.stringify(viaCdp.indexedDBDatabases));
console.log('CDP CacheStorage names seen:', JSON.stringify(cacheList.caches.map((c) => c.cacheName)));
console.log("CDP IndexedDB.requestData with an empty indexName:", idbEmptyIndexError ?? 'no error');
 
await browser.close();
server.close();

Its output, unedited:

Playwright 1.62.1, Chromium 151.0.7922.34
marker written to every store: ID-4b7f2a

store           page.evaluate()    CDP                                storageState()   storageState({indexedDB:true})
---------------- ------------------ ---------------------------------- ---------------- ------------------------------
cookie           ID-4b7f2a          ID-4b7f2a                          ID-4b7f2a        ID-4b7f2a                     
localStorage     ID-4b7f2a          ID-4b7f2a                          ID-4b7f2a        ID-4b7f2a                     
sessionStorage   ID-4b7f2a          ID-4b7f2a                          MISSING          MISSING                       
indexedDB        ID-4b7f2a          ID-4b7f2a                          MISSING          ID-4b7f2a                     
cacheStorage     ID-4b7f2a          http://127.0.0.1:36849/cached.txt  MISSING          MISSING                       
serviceWorkers   1                  1                                  MISSING          MISSING                       

CDP IndexedDB databases seen: ["store"]
CDP CacheStorage names seen: ["cachev1"]
CDP IndexedDB.requestData with an empty indexName: cdpSession.send: Protocol error (IndexedDB.requestData): Could not get index

What OpenWPM records, and what it does not

The field's standard crawler is worth being precise about, because “we used OpenWPM” is read by reviewers as “storage was recorded” and it is not.

  • Cookies are instrumented by default: cookie_instrument defaults to True.
  • localStorage and sessionStorage are instrumented only if you turn on js_instrument, which defaults to False. When you do turn it on, the default collection is collection_fingerprinting, and that collection does include window.Storage, window.localStorage and window.sessionStorage — so the accesses land in the javascript table. A crawl that left js_instrument at its default has no storage record at all.
  • IndexedDB, Cache Storage and service workers are not instrumented, and the string indexeddb does not appear anywhere in the repository. Verified by grep over the whole tree at v0.36.0.
  • The profile tarball does contain them. DumpProfileCommand archives the entire Firefox profile directory, so storage/default/<origin>/idb/ is inside the tar; it is simply never parsed. The command's own validation checks for cookies.sqlite, places.sqlite and storage.sqlite — the third being localStorage, renamed from webappsstore.sqlite when Firefox 137 migrated it. If you need IndexedDB out of an OpenWPM crawl there are two routes and neither is the default. For the contents, the profile tarball already has them and you write the reader. For the accesses, js_instrument_settings takes an arbitrary list of objects — collection_fingerprinting is only a shortcut, and the same shortcut syntax that yields {"window": ["localStorage"]} takes a bare object name such as IDBObjectStore to instrument that whole interface. Untested here: we read the schema and the shortcut expander, we did not run a crawl with it.7)

Partitioning: Your Crawler and a Real Browser Disagree

Since Chrome 115 a real Chrome user's third-party localStorage, IndexedDB and Cache Storage are keyed by the top-level site, so the same third party embedded on two different sites gets two separate stores. Cookies are explicitly not part of that change.8) Firefox goes further and partitions those same APIs statically — always, regardless of the user's privacy setting — while partitioning cookies dynamically since Firefox 103.9)

Which browser partitions which store has been a moving target, and Snyder et al. [8Snyder, Peter; Karami, Soroush; Edelstein, Arthur; Livshits, Benjamin; Haddadi, Hamed (2023): "Pool-Party: Exploiting Browser Resource Pools for Web Tracking", in: Proceedings of the USENIX Security Symposium. (Link)] recorded the state of it in 2023 while showing that partitioning storage is not the end of the problem: “WebKit partitions DOM storage and some kinds of network state, Gecko partitions DOM storage and network state, and Chromium partitions network state”, and their own attack works through browser resource pools — a cross-site channel that no amount of clearing storage removes, and that was exploitable in every major browser they tested.

Your crawler may be turning that off. Measured here, with three arms and the same Chromium binary in all three: two different first-party sites embed the same third-party iframe, which writes localStorage under site A and reads it back under site B.

Chromium 151.0.7922.34

Playwright default                       site A wrote TP-VALUE, site B read TP-VALUE  -> NOT partitioned
partitioning re-enabled                  site A wrote TP-VALUE, site B read null  -> PARTITIONED
Puppeteer 25.9.0 default                 site A wrote TP-VALUE, site B read null  -> PARTITIONED

Two automation libraries driving the identical browser build disagree about whether third-party storage is shared across sites, and neither documents it where you would look. Playwright launches Chromium with ThirdPartyStoragePartitioning in its –disable-features list, because its storageState serialiser cannot express partitioned storage; Puppeteer does not. The window matters and is narrower than it looks: Chrome partitioned for all users in July 2023, and Playwright started opting out in 1.48.0, released 8 October 202410) — so a crawl on Playwright 1.47 or earlier partitioned like real Chrome, and one on 1.48 or later does not. A measurement of third-party localStorage made with default Playwright since October 2024 is measuring a browser that stopped existing for real users in mid-2023, and will over-count cross-site persistence. Since 2022 the engine decides, not you has the maintainers' own account of why the flag is there and the warning that it is expected to stop being available.

The fix is not one line, because Playwright passes its whole disabled list as a single argument and –disable-features beats –enable-features: adding –enable-features=ThirdPartyStoragePartitioning on its own changes nothing, measured. What works is to rebuild the disabled list without that one feature. The script below does that with ignoreDefaultArgs plus a replacement argument; a later mutation test found the replacement argument alone is sufficient, because Chromium takes the last –disable-features it is given — so treat the ignoreDefaultArgs line as belt-and-braces rather than as the load-bearing part.

partition_probe.mjs
// Is third-party *storage* partitioned in the browser your crawler launches?
//
// Two different first-party origins (localhost:A and 127.0.0.1:B - different
// sites) embed the SAME third-party origin in an iframe. The third party writes
// `localStorage` under site A and tries to read it back under site B.
//
//   value comes back  -> one shared third-party storage: NOT partitioned
//   null comes back   -> per-top-level-site storage: partitioned
//
// Cookies are a separate mechanism and are NOT what this probe tests.
//
// Run: PLAYWRIGHT_BROWSERS_PATH=/path/to/browsers node partition_probe.mjs
 
import http from 'node:http';
import { chromium } from 'playwright';
import puppeteer from 'puppeteer';
import puppeteerPkg from 'puppeteer/package.json' with { type: 'json' };
const puppeteerVersion = puppeteerPkg.version;
 
const serve = (body) =>
  http.createServer((q, r) => {
    r.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
    r.end(body(q.url));
  });
 
const third = serve((url) => `<!doctype html><script>
  if (${JSON.stringify('?write')} === location.search) localStorage.setItem('tp', 'TP-VALUE');
  parent.postMessage(localStorage.getItem('tp'), '*');
</script>`);
await new Promise((r) => third.listen(0, '127.0.0.1', r));
const TP = `http://127.0.0.1:${third.address().port}`;
 
const topPage = (q) => `<!doctype html><script>
  window.__got = new Promise((res) => addEventListener('message', (e) => res(e.data)));
</script><iframe src="${TP}/${q}"></iframe>`;
const writer = serve(() => topPage('?write'));
const reader = serve(() => topPage(''));
await new Promise((r) => writer.listen(0, '127.0.0.1', r));
await new Promise((r) => reader.listen(0, '127.0.0.1', r));
 
async function run(label, launchOptions) {
  const browser = await chromium.launch(launchOptions);
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto(`http://localhost:${writer.address().port}/`); // site A
  const wrote = await page.evaluate(() => window.__got);
  await page.goto(`http://127.0.0.1:${reader.address().port}/`); // site B, a different site
  const read = await page.evaluate(() => window.__got);
  console.log(
    `${label.padEnd(40)} site A wrote ${String(wrote)}, site B read ${String(read)}  -> ${
      read === null ? 'PARTITIONED' : 'NOT partitioned'
    }`
  );
  await browser.close();
}
 
// Playwright's `--disable-features=` list is one combined argument, and
// --disable-features beats --enable-features, so the control arm has to drop
// the whole default argument and pass it back minus the one feature. The list is
// read from the running browser rather than hard-coded, so this keeps working
// when Playwright changes it.
const probe = await chromium.launch({ args: ['--enable-automation'] });
const version = probe.version();
const session = await probe.newBrowserCDPSession();
const cmdline = (await session.send('Browser.getBrowserCommandLine')).arguments;
await probe.close();
const disableArg = cmdline.find((a) => a.startsWith('--disable-features='));
const without = disableArg
  .slice('--disable-features='.length)
  .split(',')
  .filter((f) => f !== 'ThirdPartyStoragePartitioning');
 
console.log(`Chromium ${version}`);
console.log(`Playwright's default: ${disableArg}\n`);
await run('Playwright default', {});
await run('partitioning re-enabled', {
  ignoreDefaultArgs: [disableArg],
  args: [`--disable-features=${without.join(',')}`, '--enable-features=ThirdPartyStoragePartitioning'],
});
 
// Puppeteer drives the same engine but does not disable the feature. Point it
// at Playwright's Chromium binary so the engine build is identical and the only
// difference is the automation library's default flags.
const pup = await puppeteer.launch({ executablePath: chromium.executablePath(), headless: true });
const pupPage = await pup.newPage();
await pupPage.goto(`http://localhost:${writer.address().port}/`);
const pupWrote = await pupPage.evaluate(() => window.__got);
await pupPage.goto(`http://127.0.0.1:${reader.address().port}/`);
const pupRead = await pupPage.evaluate(() => window.__got);
console.log(
  `${`Puppeteer ${puppeteerVersion} default`.padEnd(40)} site A wrote ${String(pupWrote)}, site B read ${String(pupRead)}  -> ${
    pupRead === null ? 'PARTITIONED' : 'NOT partitioned'
  }`
);
await pup.close();
 
third.close(); writer.close(); reader.close();

Whichever way you set it, report it: “Playwright 1.62.1, third-party storage partitioning left at Playwright's default (disabled)” is a sentence a reviewer can check and a future reader can correct for. It is also the sentence that decides whether your third-party storage figure is comparable to [1Kancherla, Gayatri Priyadarsini; Goel, Dishank; Bichhawat, Abhishek (2025): "Least Privilege Access for Persistent Storage Mechanisms in Web Browsers", in: Proceedings of the ACM Web Conference. (DOI)]'s.

"We Cleared Cookies" Is Not a Reset

What a reset actually resets measures what each reset in a modern crawler clears, and the short version is that clearCookies() clears cookies and nothing else. Two things belong here rather than there.

The per-origin reset. If your design is paired visits — visit, reset one site, revisit — throwing away the whole browser context resets every site at once and destroys the control. The Clear-Site-Data response header resets one origin. Measured, in the same harness as above:

Clear-Site-Data: cookie localStorage sessionStorage IndexedDB Cache Storage service worker HTTP cache
"cookies" cleared survived survived survived survived survived survived
"storage" survived cleared cleared cleared cleared cleared survived
"cache" survived survived survived survived survived survived cleared
"cookies", "storage", "cache" cleared cleared cleared cleared cleared cleared cleared
"*" cleared cleared cleared cleared cleared cleared cleared

Chromium 151.0.7922.34. The three directives partition cleanly and none of them is a superset of another, so the useful ones are "storage" when you want to reset the client-side stores without touching the cookie jar, and "*" when you want everything. The HTTP-cache column is observed indirectly, by whether an immutable subresource is re-requested from the origin; that read has to happen in a fresh page, because Chromium's per-renderer in-memory resource cache will otherwise serve the subresource without consulting the disk cache and the column comes out wrong. That is a measurement bug worth knowing about on its own: within one page lifetime you cannot tell a disk-cache hit from a memory-cache hit.

And the browser resets things behind you. Chrome's bounce-tracking mitigations flag sites that appear in a stateful redirect chain, and “if the site does not have any user interaction recorded within the last 45 days and third-party cookies are blocked, then the site storage will be deleted shortly after the next redirection flow is triggered through this site”; they launched by default in October 2023 for users who block third-party cookies.11) A crawler blocks third-party cookies far more often than it interacts with a site, so a long crawl in that configuration is one in which the browser is deleting the state you are trying to measure. Safari's seven-day cap does the same thing on a different timer.

Article 5(3) of the ePrivacy Directive is written about “the storing of information, or the gaining of access to information already stored, in the terminal equipment of a subscriber or user”. It does not say cookie. The EDPB's Guidelines 2/2023 on the technical scope of Article 5(3), version 2.0, adopted 7 October 2024, work through a non-exhaustive list of use cases — URL and pixel tracking, local processing, tracking based on IP only, intermittent and mediated IoT reporting, and unique identifiers — and treat browser caching as storage: distributing a tracking URL to the terminal “does constitute storage, at the very least through the caching mechanism of the client-side software. As such, Article 5(3) ePD is applicable, even if this storage is not permanent”.12)

And the frame is a live target. The Commission's Digital Omnibus proposal of 19 November 2025 would lift terminal-equipment consent out of ePrivacy and into the GDPR for natural persons' personal data, leaving Article 5(3) to cover only the non-personal remainder — which, for this page, is exactly the storage keys that are not identifiers. Legal enforcement has the text, the procedure number and the current status; it is a proposal, not law, and a paper whose legal analysis has to survive review in 2027 should say which regime it assumed and on what date.

The corpus says the compliance literature has not followed. Of the 402 papers that assessed a law, 35 (8.7%) name a non-cookie client-side store anywhere in the text, and 4 measured one. Of the 283 that assessed the GDPR or the ePrivacy Directive, 27 (9.5%) and 3. This is a mention test — it does not prove those papers checked only cookies — but a violation study whose instrument cannot see localStorage cannot report a violation that lives there. Whether those papers say so as a limitation is not a question a mention probe can answer — a sentence like “we do not cover other storage mechanisms” contains no storage-API identifier and would not match — so this page does not claim it. It is a straightforward thing for someone to read and count.

Two papers show what is being missed. Kancherla et al. [10Kancherla, Gayatri Priyadarsini; Bielova, Nataliia; Santos, Cristiana; Bichhawat, Abhishek (2025): "Johnny Can't Revoke Consent Either: Measuring Compliance of Consent Revocation on the Web", in: Proceedings on Privacy Enhancing Technologies. (DOI)] collected consent records from both cookies and localStorage and found them disagreeing: on three websites “the TCString returned by __tcfapi showed negative consent, while the TCString stored in the localStorage remained unchanged”, that is, still positive. A study that read the cookie and stopped would have recorded compliance. And Demir et al. [9Demir, Nurullah; Urban, Tobias; Pohlmann, Norbert; Wressnegger, Christian (2024): "A Large-Scale Study of Cookie Banner Interaction Tools and their Impact on Users' Privacy", in: Proceedings on Privacy Enhancing Technologies, pp. 5-20. (DOI)] measured localStorage object counts alongside cookies across eight banner-interaction profiles, which is the only way to see that a tool which suppresses cookies may not suppress anything else.

No label database will help you. The Open Cookie Database's column is even named “Cookie / Data Key name”, but its schema has no field for which store a key lives in, so a localStorage key and a cookie of the same name are indistinguishable in it. The CookieBlock training set [11Bollinger, Dino; Kubicek, Karel; Cotrini, Carlos; Basin, David (2022): "Automating Cookie Consent and GDPR Violation Detection", in: 31st USENIX Security Symposium (USENIX Security 22), pp. 2893-2910. USENIX Association, Boston, MA. (Link)] is keyed on cookie name and domain (see OneTrust and CookieBot Dataset), and Cookiepedia's classification documentation discusses cookies only — it names “online tracking technologies” as a separate topic without saying what that covers, so treat non-cookie coverage there as unestablished rather than absent.13) The one that claims more is Cookiedatabase.org, which states that “on Cookiedatabase.org you can consult information about cookies, Local Storage, pixels and other tracking technologies” — we could not extract a per-store count from it, so treat it as a lead rather than a resource with known coverage.14) In practice a storage measurement classifies by value entropy, by provenance (which script wrote it, and is that script on a filter list), or by hand — the same three routes as Cookies, minus the database.

Use in Publications

How the population was built

There is no schema field for this. classification.target has no storage value, and detection.phenomenon is free text that names a store in only 20 papers. So the population comes from the full text in two published steps.

Step 1, mechanical. Count occurrences of six storage-API identifiers in each paper's paper.cols.txt, whitespace-collapsed so a PDF line break inside a phrase does not silently drop a hit. The exact patterns:

localStorage     /local\s?Storage|localstorage|\bDOM storage\b|\bWeb Storage\b/gi
sessionStorage   /session\s?Storage|sessionstorage/gi
IndexedDB        /Indexed\s?DB|indexeddb/gi
serviceWorker    /service\s?workers?/gi
CacheAPI         /Cache\s?Storage|caches\.(open|match|keys)|\bCache API\b/gi
WebSQL           /Web\s?SQL/gi

A paper is a candidate if the sum of those six counts is at least 3. That threshold is published rather than tuned, because a mention threshold is a candidate set and not a finding. ETag, favicon, respawn, evercookie and Flash cookie are counted and reported but deliberately do not decide candidacy: they fire on a large non-storage literature (HTTP caching, phishing favicons, botnet resurrection).

Step 2, hand audit. All 120 candidates were read and assigned a tier, decided before the reading started:

  • Tier A — the paper reports an empirical measurement of a non-cookie client-side store: its prevalence, its contents, identifiers in it, accesses to it, or whether it survives a clearing action.
  • Tier B — the paper does not measure the store but instruments, clears, blocks or models it as part of its method or threat model. These are the papers a methods section cites, not the results.
  • out — not about a browser store at all (CPU, CDN and CI caches; native or Android filesystem “local storage”; mini-program storage), or the only hit is a related-work sentence.
Tier Papers Share of the 120 candidates
A — measured a non-cookie browser store 43 35.8%
B — instrumented, cleared or modelled it only 49 40.8%
out — not about a browser store 28 23.3%

So the threshold's precision is 76.7% for “is about a browser store” and 35.8% for “measured one”. The per-paper assignments, with a reason each, and the below-threshold residue are on browser_storage.

How often the field even names a store

Population N names at least one store share names one three or more times share
ran an automated web crawl 1,120 146 13.0% 70 6.3%
measured the web platform 1,622 205 12.6% 99 6.1%
whole corpus 5,859 294 5.0% 120 2.0%

Per store, among the 1,120 papers that ran an automated web crawl, counting each paper once:

Store Crawl papers naming it Share of 1,120
localStorage (incl. “Web Storage”, “DOM storage”) 124 11.1%
sessionStorage 38 3.4%
Flash LSO 34 3.0%
evercookie / supercookie 34 3.0%
respawn / resurrect 41 3.7%
service worker 30 2.7%
IndexedDB 28 2.5%
favicon 27 2.4%
ETag 21 1.9%
Cache API / Cache Storage 8 0.7%
WebSQL 5 0.4%

And it is not growing. Tier A papers as a share of all papers in the bucket, for 2010–2013, 2014–2017, 2018–2021, 2022–2024 and 2025–2026 (four-, four-, four-, three- and two-year, so the last two are not comparable widths): 0.8%, 0.4%, 0.8%, 0.9%, 0.7%. Seventeen years, no detectable trend. The bucket counts behind those shares are 4, 3, 11, 17 and 8 Tier-A papers, which is too few to distinguish no trend from noise, so read the row as “nothing visible at this sample size” rather than as a demonstration of flatness. 2025–2026 is provisional — CCS and IMC 2026 have not been held, IEEE S&P and WWW 2026 are incompletely selected — so read that last cell as a lower bound, not as a decline.

The other store the crawler cannot see

A service worker runs in its own context. Hantke et al. [7Hantke, Florian; Snyder, Peter; Haddadi, Hamed; Stock, Ben (2025): "Web Execution Bundles: Reproducible, Accurate, and Archivable Web Measurements", in: Proceedings of the USENIX Security Symposium. (Link)], building a reproducible measurement recorder, report plainly that “service workers also live in their own context and are not recorded in .web behaviors”, and their solution was to hook the serviceWorker API and forbid registration altogether. That is a defensible choice and they state it; the failure mode is the crawl that does neither, records the page's requests, and reports a request count that silently excludes everything the service worker fetched. Subramani et al. [12Subramani, Karthika; Yuan, Xingzi; Setayeshfar, Omid; Vadrevu, Phani; Lee, Kyu Hyung; Perdisci, Roberto (2020): "When Push Comes to Ads: Measuring the Rise of (Malicious) Push Advertising", in: Proceedings of the ACM Internet Measurement Conference. (DOI)] give the size of what is being excluded from the blocking side: measuring web-push advertising, all tested ad blockers blocked 0 service worker scripts, and EasyList filtered under 2% of the requests those workers subsequently issued.

What has actually been measured

Each figure with the paper's own denominator, read from the paper rather than from the extraction, because the extraction's metric names the unit and not the population.

Paper Denominator Measured
[1Kancherla, Gayatri Priyadarsini; Goel, Dishank; Bichhawat, Abhishek (2025): "Least Privilege Access for Persistent Storage Mechanisms in Web Browsers", in: Proceedings of the ACM Web Conference. (DOI)] WWW 2025 Tranco top 10,000 90.98% of all localStorage accesses, 72.49% of all IndexedDB accesses and 89.84% of all cookie accesses are made by third-party scripts. In at least 16% of third-party cookie accesses the third party reads or modifies a cookie set by the host page; for localStorage about 10%
[13Munir, Shaoor; Siby, Sandra; Iqbal, Umar; Englehardt, Steven; Shafiq, Zubair; Troncoso, Carmela (2023): "CookieGraph: Understanding and Detecting First-Party Tracking Cookies", pp. 3490–3504. Association for Computing Machinery, New York, NY, USA. (DOI) (Link)] CCS 2023 a 20,000-site stratified sample of the Tranco top million — 1K top sites, 9K uniformly sampled from ranks 1K–100K and 10K from 100K–1M, not the top 20,000 217,444 unique first-party cookie names against 99,682 unique localStorage names, and 13,571 instances of the same first-party cookie also stored in local storage
[3Karami, Soroush; Ilia, Panagiotis; Polakis, Jason (2021): "Awakening the Web's Sleeper Agents: Misusing Service Workers for Privacy Leakage", in: Proceedings of the Network and Distributed System Security Symposium. (Link)] NDSS 2021 Alexa top 1M 30,229 sites installed a service worker; of those, 9,446 implemented caching and 25,457 used Web Push
[14Lee, Jiyeon; Kim, Hayeon; Park, Junghwan; Shin, Insik; Son, Sooel (2018): "Pride and Prejudice in Progressive Web Apps: Abusing Native App-like Features in Web Applications", in: Proceedings of the ACM SIGSAC Conference on Computer and Communications Security. (DOI)] CCS 2018 Alexa top 100,000 4,163 sites registered a service worker; 513 (12.3%) of those used an offline cache, and 187 (36.5%) of the cache-using sites were identifiable from a cache history side-channel
[5Acar, Gunes; Eubank, Christian; Englehardt, Steven; Juarez, Marc; Narayanan, Arvind; Díaz, Claudia (2014): "The Web Never Forgets: Persistent Tracking Mechanisms in the Wild", in: Proceedings of the ACM SIGSAC Conference on Computer and Communications Security. (DOI)] CCS 2014 Alexa top 100,000 20 sites used IndexedDB — one of them storing a value that exactly matched a Flash cookie
[15Singh, Kapil; Moshchuk, Alexander; Wang, Helen J.; Lee, Wenke (2010): "On the Incoherencies in Web Browser Access Control Policies", in: Proceedings of the IEEE Symposium on Security and Privacy. (DOI)] IEEE S&P 2010 89,222 analysed sites of the Alexa top 100,000 0.19% used localStorage and none used sessionStorage. This is the field's zero point
[16Aggarwal, Gaurav; Bursztein, Elie; Jackson, Collin; Boneh, Dan (2010): "An Analysis of Private Browsing Modes in Modern Browsers", in: Proceedings of the USENIX Security Symposium. (Link)] USENIX Sec 2010 top 100 Chrome extensions 71 “stored data to disk using the localStorage API” — the first measurement of storage as a private-browsing leak rather than as a tracking vector
[2Zhou, Yuchen; Evans, David (2015): "Understanding and Monitoring Embedded Web Scripts", in: Proceedings of the IEEE Symposium on Security and Privacy. (DOI)] IEEE S&P 2015 Alexa sites, instrumented Firefox recorded third-party script accesses to localStorage and indexedDB as a distinct permission, and observed scripts reaching for storage “presumably adding an extra tracking approach should the user reject or delete their cookie”
[17Lerner, Ada; Simpson, Anna Kornfeld; Kohno, Tadayoshi; Roesner, Franziska (2016): "Internet Jones and the Raiders of the lost trackers: An archaeological study of web tracking from 1996 to 2016", in: 25th USENIX Security Symposium (USENIX Security 16). (Link)] USENIX Sec 2016 archived pages, 1996–2016 localStorage use “rises rapidly since its introduction in 2009, indicating that tracking defenses should increasingly consider on storage mechanisms beyond cookies”
[18Steffens, Marius; Rossow, Christian; Johns, Martin; Stock, Ben (2019): "Don't Trust The Locals: Investigating the Prevalence of Persistent Client-Side Cross-Site Scripting in the Wild", in: Proceedings of the Network and Distributed System Security Symposium. (Link)] NDSS 2019 Alexa top 5,000 more than 8% show an exploitable flow from client-side storage into a dangerous sink, rising to 21% among the sites that use persisted data in a sink at all. “Virtually all flows that originate from a Local Storage source have no encoding applied to them”
[19Watanabe, Takuya; Shioji, Eitaro; Akiyama, Mitsuaki; Mori, Tatsuya (2020): "Melting Pot of Origins: Compromising the Intermediary Web Services that Rehost Websites", in: Proceedings of the Network and Distributed System Security Symposium. (Link)] NDSS 2020 Alexa top 10K, reachable sites around 40% carried a unique fingerprintable record in their cookie and localStorage
[9Demir, Nurullah; Urban, Tobias; Pohlmann, Norbert; Wressnegger, Christian (2024): "A Large-Scale Study of Cookie Banner Interaction Tools and their Impact on Users' Privacy", in: Proceedings on Privacy Enhancing Technologies, pp. 5-20. (DOI)] PETS 2024 Tranco crawl profiles 122k localStorage objects per profile on average; 12 keys per site (SD 31, max 2,720)
[20Franken, Gertjan; Van Goethem, Tom; Joosen, Wouter (2018): "Who Left Open the Cookie Jar? A Comprehensive Evaluation of Third-Party Cookie Policies", in: Proceedings of the USENIX Security Symposium. (Link)] USENIX Sec 2018 46 extension policies 26 policies bypassed via AppCache and 20 via service workers
[4Solomos, Konstantinos; Kristoff, John; Kanich, Chris; Polakis, Jason (2021): "Tales of Favicons and Caches: Persistent Tracking in Modern Browsers", in: Proceedings of the Network and Distributed System Security Symposium. (Link)] NDSS 2021 Alexa top 10K eTLD+1 94% have a valid favicon resource. The favicon cache “is not affected by user actions that clear other caches, local storages and browsing data” and is not isolated from incognito
[21Mishra, Vikas; Laperdrix, Pierre; Rudametkin, Walter; Rouvoy, Romain (2021): "Déjà vu: Abusing Browser Cache Headers to Identify and Track Online Users", in: Proceedings on Privacy Enhancing Technologies. (DOI)] PETS 2021 Tranco top 100,000 91,755 cacheable resources on 12,970 websites are usable to identify a returning visitor; 25% of them had expiry beyond 100 days
[22Fouad, Imane; Santos, Cristiana; Legout, Arnaud; Bielova, Nataliia (2022): "My Cookie is a phoenix: Detection, measurement, and lawfulness of cookie respawning with browser fingerprinting", in: PETS 2022-22nd Privacy Enhancing Technologies Symposium. (DOI) (Link)] PETS 2022 Alexa top 30,000 1,150 (3.83%) use cookie respawning with browser fingerprinting. Related work had already shown ETag, localStorage and IndexedDB used as respawn stores
[6Ali, Mir Masood; Chitale, Binoy; Ghasemisharif, Mohammad; Kanich, Chris; Nikiforakis, Nick; Polakis, Jason (2023): "Navigating Murky Waters: Automated Browser Feature Testing for Uncovering Tracking Vectors", in: Proceedings of the Network and Distributed System Security Symposium. (Link)] NDSS 2023 126 versions of 7 browsers 21 mechanisms usable as a tracking vector, 13 of them cross-site; every latest version tested was vulnerable to at least one
[23Patat, Gwendal; Sabt, Mohamed; Fouque, Pierre-Alain (2023): "Your DRM Can Watch You Too: Exploring the Privacy Implications of Browsers (mis)Implementations of Widevine EME", in: Proceedings on Privacy Enhancing Technologies. (DOI)] PETS 2023 tested browser configurations mobile Opera and Samsung Internet retained EME persistent sessions after cookie and site-data deletion — a store outside the storage APIs entirely
[24Liu, Xiaoyin; Li, Wenzhi; Hou, Qinsheng; Yang, Shishuai; Ying, Lingyun; Diao, Wenrui; Li, Yanan; Guo, Shanqing; Duan, Haixin (2024): "From Promises to Practice: Evaluating the Private Browsing Modes of Android Browser Apps", in: Proceedings of the ACM Web Conference. (DOI)] WWW 2024 49 Android browser apps 15 (30.6%) stored the web cache from private mode, and 16 (32.7%) shared cookies across modes
[25Vekaria, Yash; Canino, Aurelio Loris; Levitsky, Jonathan; Ciechonski, Alex; Callejo, Patricia; Mandalari, Anna Maria; Shafiq, Zubair (2025): "Big Help or Big Brother? Auditing Tracking, Profiling, and Personalization in Generative AI Assistants", in: Proceedings of the USENIX Security Symposium. (Link)] USENIX Sec 2025 9 generative-AI assistant extensions Harpa and Copilot “stored the complete chat history in the extension's background service worker using IndexedDB, indicating that these histories persist across browsing sessions”
[26Wang, Weihong; Dimova, Yana; Vansteenkiste, Victor; Van Goethem, Tom; Van Cutsem, Tom (2026): "The Masks We (Think We) Wear: Privacy Threats of Browser-Extension Wallets in the Web3 Ecosystem", in: Proceedings on Privacy Enhancing Technologies. (DOI)] PETS 2026 30 Ethereum dApps 27 (90%) store the wallet address in localStorage or cookies
[27Jueckstock, Jordan; Snyder, Peter; Sarker, Shaown; Kapravelos, Alexandros; Livshits, Benjamin (2022): "Measuring the Privacy vs. Compatibility Trade-off in Preventing Third-Party Stateful Tracking", in: Proceedings of the ACM Web Conference. (DOI)] WWW 2022 Tranco top 1,000, repeat crawls identifying tokens in third-party local storage are much rarer than in cookies, but about equally common under site-keyed partitioning as under a permissive policy — partitioning does less for storage than the cookie result suggests

Note the shape of that list. Almost every row is a single paper with a single crawl, and no two of them measure the same quantity, so there is nothing here to compare across years the way Requests can compare filter-list hit rates. The 2010 and 2014 baselines and the 2025 access figures are the closest thing the field has to a time series for localStorage and IndexedDB, and they were produced by three unrelated groups with three different instruments.

Methodology and limitations of these figures

  • The corpus is seven venues — CCS, IMC, NDSS, PETS, USENIX Security, TheWebConf and IEEE S&P, 2010–2026, 5,859 extracted papers. EuroS&P, ACSAC, RAID, AsiaCCS, CHI and SOUPS are absent, and WPES — where a lot of storage work has appeared — is absent too. Every count here is a count about those seven venues.
  • The mention counts are a mention test. “124 crawl papers write localStorage” does not mean 996 papers failed to clear it; it means a reader cannot tell from those papers. The Tier A count is the hand-audited claim.
  • The threshold has a residue. 174 papers name a storage API once or twice and were not audited; 106 of them measured the web platform. Two that a reader would expect to be in scope — Englehardt and Narayanan's million-site measurement [28Englehardt, Steven; Narayanan, Arvind (2016): "Online Tracking: A 1-million-site Measurement and Analysis", in: Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security, pp. 1388–1401. Association for Computing Machinery, New York, NY, USA. (DOI) (Link)] and IMC 2016's browser-feature-usage study — sit below the threshold, were checked by hand, and are correctly not Tier A: the first mentions localStorage only in related work and in a description of what its persistent profiles carry, and the second reports Web Storage inside a large feature table this extraction cannot read. The full residue list is on browser_storage.
  • Free-text extraction fields are ~20% stable run-to-run, which is why nothing here is aggregated from detection.phenomenon by exact string. The stability figures themselves were measured on the previous, smaller extraction run and have not been re-measured.
  • 35 quotes behind these figures were checked verbatim against the paper's own text; 33 matched paper.cols.txt exactly and 2 only matched a second rendering of the PDF, because the column repair had spliced the two columns of the page together. Both are named on the provenance page.
  • Full query log, report script, hand-audit table, residue and quote checks: browser_storage. Corpus-level caveats: corpus.

What to Report

  1. Which stores you recorded, by name. Not “browser state”. Cookies, localStorage, sessionStorage, IndexedDB, Cache Storage, service worker registrations, HTTP cache — say which of those seven your instrument captured and which it did not.
  2. How you captured them. page.evaluate, CDP, storageState, profile-on-disk, or OpenWPM's js_instrument. Each has a different blind spot and the table above is the map.
  3. Whether third-party storage partitioning was on, with the automation library and version. It is off by default in Playwright 1.48.0 and later, on in earlier Playwright and in Puppeteer, and on in real Chrome since 115.
  4. What you cleared between visits, per store, and what you did not. clearCookies() is not a reset; Clear-Site-Data is per-origin; a new browser context clears everything.
  5. Whether the browser was deleting state during the crawl — third-party-cookie blocking plus Chrome's bounce-tracking mitigations, or a Safari arm and its seven-day cap.
  6. Whether service workers were allowed to register, and if so whether their requests are in your request counts.
  7. The disk headroom the crawl ran with, if it accumulated storage across many origins rather than reading it out per visit. Quota eviction deletes an origin's storage whole.
  8. The denominator for any storage figure: sites, origins, keys, or accesses. “90.98%” above is a share of accesses, not of sites, and the two are not close.

Open Questions

  • No paper in this corpus repeats [1Kancherla, Gayatri Priyadarsini; Goel, Dishank; Bichhawat, Abhishek (2025): "Least Privilege Access for Persistent Storage Mechanisms in Web Browsers", in: Proceedings of the ACM Web Conference. (DOI)]. One crawl, Tranco top 10,000, 2025, one instrument. No search outside these seven venues was run for a replication, so read this as “not in the corpus” rather than “not anywhere”. The headline third-party-access shares for localStorage and IndexedDB rest on it alone, and it was run with an instrumented browser whose partitioning configuration determines the answer.
  • There is no storage label source with established coverage, so there is no purpose classification for a localStorage key that a paper can cite. (Cookiedatabase.org says it covers Local Storage; see above for why that is a lead and not yet a resource.) Whether the CookieBlock feature set [11Bollinger, Dino; Kubicek, Karel; Cotrini, Carlos; Basin, David (2022): "Automating Cookie Consent and GDPR Violation Detection", in: 31st USENIX Security Symposium (USENIX Security 22), pp. 2893-2910. USENIX Association, Boston, MA. (Link)] — entropy of the value, and the flags — transfers to localStorage keys, which have no flags and no expiry, is an open and cheap question.
  • The 2010, 2014 and 2025 measurements are not comparable, so the field cannot actually say whether non-cookie storage adoption grew, and by how much. A replication of [15Singh, Kapil; Moshchuk, Alexander; Wang, Helen J.; Lee, Wenke (2010): "On the Incoherencies in Web Browser Access Control Policies", in: Proceedings of the IEEE Symposium on Security and Privacy. (DOI)]'s simple question — what fraction of sites use localStorage at all — on a modern list would be a one-week paper with a sixteen-year baseline.
  • The effect of Chrome's storage partitioning on real measurement outcomes is unmeasured after [27Jueckstock, Jordan; Snyder, Peter; Sarker, Shaown; Kapravelos, Alexandros; Livshits, Benjamin (2022): "Measuring the Privacy vs. Compatibility Trade-off in Preventing Third-Party Stateful Tracking", in: Proceedings of the ACM Web Conference. (DOI)], which predates the default rollout. Every partitioning result in this corpus was obtained by simulating a policy, not by observing the shipped one.
  • The Origin Private File System is named by two corpus papers and measured by neither. It is in Chrome's partitioned set alongside localStorage and IndexedDB; the two mentions are a browser-ransomware paper and a site-isolation fuzzer, not a measurement of the live web. The Storage Bucket API appears nowhere — its fourteen apparent hits are cloud object-storage buckets — and the legacy File System API only in [6Ali, Mir Masood; Chitale, Binoy; Ghasemisharif, Mohammad; Kanich, Chris; Nikiforakis, Nick; Polakis, Jason (2023): "Navigating Murky Waters: Automated Browser Feature Testing for Uncovering Tracking Vectors", in: Proceedings of the Network and Distributed System Security Symposium. (Link)]'s mechanism inventory. These may be unused in the wild, or they may be the next unmeasured store; a paper that checked would be the first in this corpus.
  • Cookies — the store this page is defined against, and where the classification methods and label databases are.
  • Stateful stateless — what a reset actually resets, and whether your crawl carries state at all. Read its reset table before designing a paired-visit study.
  • Fingerprinting — identifying a visitor without storing anything, and the reason “we blocked storage” is not “we stopped tracking”.
  • Requests — the request-level view. A service worker's requests are the ones your recorder is most likely to be missing.
  • JavaScript — the script that wrote the key, which is how you attribute a storage write to a party.
  • Consent and TCF consent strings — where the consent record is stored is itself a measurement, and it is not always a cookie.
  • Crawler and OpenWPM — the instrument, and what it does and does not record.

References

[1]
Kancherla, Gayatri Priyadarsini; Goel, Dishank; Bichhawat, Abhishek (2025): "Least Privilege Access for Persistent Storage Mechanisms in Web Browsers", in: Proceedings of the ACM Web Conference. (DOI)
[2]
Zhou, Yuchen; Evans, David (2015): "Understanding and Monitoring Embedded Web Scripts", in: Proceedings of the IEEE Symposium on Security and Privacy. (DOI)
[3]
Karami, Soroush; Ilia, Panagiotis; Polakis, Jason (2021): "Awakening the Web's Sleeper Agents: Misusing Service Workers for Privacy Leakage", in: Proceedings of the Network and Distributed System Security Symposium. (Link)
[4]
Solomos, Konstantinos; Kristoff, John; Kanich, Chris; Polakis, Jason (2021): "Tales of Favicons and Caches: Persistent Tracking in Modern Browsers", in: Proceedings of the Network and Distributed System Security Symposium. (Link)
[5]
Acar, Gunes; Eubank, Christian; Englehardt, Steven; Juarez, Marc; Narayanan, Arvind; Díaz, Claudia (2014): "The Web Never Forgets: Persistent Tracking Mechanisms in the Wild", in: Proceedings of the ACM SIGSAC Conference on Computer and Communications Security. (DOI)
[6]
Ali, Mir Masood; Chitale, Binoy; Ghasemisharif, Mohammad; Kanich, Chris; Nikiforakis, Nick; Polakis, Jason (2023): "Navigating Murky Waters: Automated Browser Feature Testing for Uncovering Tracking Vectors", in: Proceedings of the Network and Distributed System Security Symposium. (Link)
[7]
Hantke, Florian; Snyder, Peter; Haddadi, Hamed; Stock, Ben (2025): "Web Execution Bundles: Reproducible, Accurate, and Archivable Web Measurements", in: Proceedings of the USENIX Security Symposium. (Link)
[8]
Snyder, Peter; Karami, Soroush; Edelstein, Arthur; Livshits, Benjamin; Haddadi, Hamed (2023): "Pool-Party: Exploiting Browser Resource Pools for Web Tracking", in: Proceedings of the USENIX Security Symposium. (Link)
[9]
Demir, Nurullah; Urban, Tobias; Pohlmann, Norbert; Wressnegger, Christian (2024): "A Large-Scale Study of Cookie Banner Interaction Tools and their Impact on Users' Privacy", in: Proceedings on Privacy Enhancing Technologies, pp. 5-20. (DOI)
[10]
Kancherla, Gayatri Priyadarsini; Bielova, Nataliia; Santos, Cristiana; Bichhawat, Abhishek (2025): "Johnny Can't Revoke Consent Either: Measuring Compliance of Consent Revocation on the Web", in: Proceedings on Privacy Enhancing Technologies. (DOI)
[11]
Bollinger, Dino; Kubicek, Karel; Cotrini, Carlos; Basin, David (2022): "Automating Cookie Consent and GDPR Violation Detection", in: 31st USENIX Security Symposium (USENIX Security 22), pp. 2893-2910. USENIX Association, Boston, MA. (Link)
[12]
Subramani, Karthika; Yuan, Xingzi; Setayeshfar, Omid; Vadrevu, Phani; Lee, Kyu Hyung; Perdisci, Roberto (2020): "When Push Comes to Ads: Measuring the Rise of (Malicious) Push Advertising", in: Proceedings of the ACM Internet Measurement Conference. (DOI)
[13]
Munir, Shaoor; Siby, Sandra; Iqbal, Umar; Englehardt, Steven; Shafiq, Zubair; Troncoso, Carmela (2023): "CookieGraph: Understanding and Detecting First-Party Tracking Cookies", pp. 3490–3504. Association for Computing Machinery, New York, NY, USA. (DOI) (Link)
[14]
Lee, Jiyeon; Kim, Hayeon; Park, Junghwan; Shin, Insik; Son, Sooel (2018): "Pride and Prejudice in Progressive Web Apps: Abusing Native App-like Features in Web Applications", in: Proceedings of the ACM SIGSAC Conference on Computer and Communications Security. (DOI)
[15]
Singh, Kapil; Moshchuk, Alexander; Wang, Helen J.; Lee, Wenke (2010): "On the Incoherencies in Web Browser Access Control Policies", in: Proceedings of the IEEE Symposium on Security and Privacy. (DOI)
[16]
Aggarwal, Gaurav; Bursztein, Elie; Jackson, Collin; Boneh, Dan (2010): "An Analysis of Private Browsing Modes in Modern Browsers", in: Proceedings of the USENIX Security Symposium. (Link)
[17]
Lerner, Ada; Simpson, Anna Kornfeld; Kohno, Tadayoshi; Roesner, Franziska (2016): "Internet Jones and the Raiders of the lost trackers: An archaeological study of web tracking from 1996 to 2016", in: 25th USENIX Security Symposium (USENIX Security 16). (Link)
[18]
Steffens, Marius; Rossow, Christian; Johns, Martin; Stock, Ben (2019): "Don't Trust The Locals: Investigating the Prevalence of Persistent Client-Side Cross-Site Scripting in the Wild", in: Proceedings of the Network and Distributed System Security Symposium. (Link)
[19]
Watanabe, Takuya; Shioji, Eitaro; Akiyama, Mitsuaki; Mori, Tatsuya (2020): "Melting Pot of Origins: Compromising the Intermediary Web Services that Rehost Websites", in: Proceedings of the Network and Distributed System Security Symposium. (Link)
[20]
Franken, Gertjan; Van Goethem, Tom; Joosen, Wouter (2018): "Who Left Open the Cookie Jar? A Comprehensive Evaluation of Third-Party Cookie Policies", in: Proceedings of the USENIX Security Symposium. (Link)
[21]
Mishra, Vikas; Laperdrix, Pierre; Rudametkin, Walter; Rouvoy, Romain (2021): "Déjà vu: Abusing Browser Cache Headers to Identify and Track Online Users", in: Proceedings on Privacy Enhancing Technologies. (DOI)
[22]
Fouad, Imane; Santos, Cristiana; Legout, Arnaud; Bielova, Nataliia (2022): "My Cookie is a phoenix: Detection, measurement, and lawfulness of cookie respawning with browser fingerprinting", in: PETS 2022-22nd Privacy Enhancing Technologies Symposium. (DOI) (Link)
[23]
Patat, Gwendal; Sabt, Mohamed; Fouque, Pierre-Alain (2023): "Your DRM Can Watch You Too: Exploring the Privacy Implications of Browsers (mis)Implementations of Widevine EME", in: Proceedings on Privacy Enhancing Technologies. (DOI)
[24]
Liu, Xiaoyin; Li, Wenzhi; Hou, Qinsheng; Yang, Shishuai; Ying, Lingyun; Diao, Wenrui; Li, Yanan; Guo, Shanqing; Duan, Haixin (2024): "From Promises to Practice: Evaluating the Private Browsing Modes of Android Browser Apps", in: Proceedings of the ACM Web Conference. (DOI)
[25]
Vekaria, Yash; Canino, Aurelio Loris; Levitsky, Jonathan; Ciechonski, Alex; Callejo, Patricia; Mandalari, Anna Maria; Shafiq, Zubair (2025): "Big Help or Big Brother? Auditing Tracking, Profiling, and Personalization in Generative AI Assistants", in: Proceedings of the USENIX Security Symposium. (Link)
[26]
Wang, Weihong; Dimova, Yana; Vansteenkiste, Victor; Van Goethem, Tom; Van Cutsem, Tom (2026): "The Masks We (Think We) Wear: Privacy Threats of Browser-Extension Wallets in the Web3 Ecosystem", in: Proceedings on Privacy Enhancing Technologies. (DOI)
[27]
Jueckstock, Jordan; Snyder, Peter; Sarker, Shaown; Kapravelos, Alexandros; Livshits, Benjamin (2022): "Measuring the Privacy vs. Compatibility Trade-off in Preventing Third-Party Stateful Tracking", in: Proceedings of the ACM Web Conference. (DOI)
[28]
Englehardt, Steven; Narayanan, Arvind (2016): "Online Tracking: A 1-million-site Measurement and Analysis", in: Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security, pp. 1388–1401. Association for Computing Machinery, New York, NY, USA. (DOI) (Link)
1)
privacytests.org carries a favicon cache test with the same rationale, updated 2026-08-20, but its per-browser results could not be parsed reliably from the page and are not quoted. Brave issue 36931, “Favicon partitioning”, opened 2022-08-08, is still open. Checked 2026-08-31.
2)
Chrome for Developers, "Deprecating and removing Web SQL": Web SQL access in insecure contexts ended in Chromium 110 and “Web SQL access in all contexts is no longer available from Chromium 119”. Checked 2026-08-31.
3)
MDN, Storage quotas and eviction criteria. Checked 2026-08-31. Eviction skips origins that called navigator.storage.persist().
4)
WebKit, Tracking Prevention in WebKit, and the announcement that introduced the cap, "Full Third-Party Cookie Blocking and More", 24 March 2020. Both checked 2026-08-31; the tracking-prevention page is undated, so treat the seven-day figure as WebKit's current documented behaviour rather than as a version-pinned fact.
5)
Playwright API reference, BrowserContext.storageState, checked 2026-08-31. The indexedDB option is documented as “Set to true to include IndexedDB in the storage state snapshot.”
6)
chromedevtools.github.io/devtools-protocol/tot/IndexedDB and the browser_protocol.json definition of IndexedDB.requestData. Checked 2026-08-31.
7)
OpenWPM at tag v0.36.0 (released 2026-08-24, Firefox 154), commit 61d285fc. Files read: openwpm/config.py, openwpm/js_instrumentation.py, openwpm/js_instrumentation_collections/fingerprinting.json, openwpm/commands/profile_commands.py, docs/Configuration.md, CHANGELOG.md. Checked 2026-08-31.
8)
Privacy Sandbox, Storage partitioning: “The feature has been enabled for all users on Chrome 115 and later.” That rollout covers the storage APIs — the quota system (IndexedDB and Cache storage), the Web Storage API, the Origin Private File System, the Storage Bucket API and the Clear-Site-Data header. The same page groups the affected APIs as storage, communication (Broadcast Channel, SharedWorker, Web Locks), service worker and extension APIs, and dates the Blob URL store separately: “Starting with Chrome 137 (released May 27, 2025), Blob URLs are partitioned for all uses except top-level navigations.” Cookies are handled separately, by CHIPS and the Storage Access API. Checked 2026-08-31.
9)
MDN, State Partitioning. Checked 2026-08-31.
10)
ThirdPartyStoragePartitioning first appears in chromiumSwitches in the playwright-core 1.48.0 npm tarball and is absent from 1.47.0; the commit is fix(chromium): disable ThirdPartyStoragePartitioning (PR 32701), 2024-09-19. Checked 2026-08-31.
11)
Privacy Sandbox, Bounce tracking mitigations. Checked 2026-08-31.
12)
EDPB, Guidelines 2/2023 on Technical Scope of Art. 5(3) of ePrivacy Directive, version 2.0, adopted 7 October 2024 (version 1.0 was adopted 14 November 2023 for public consultation). Paragraph 43 names caching mechanisms such as ETag; paragraph 44 names “cookies, local storage, WebSQL” among the information a browser stores. Checked 2026-08-31.
13)
github.com/jkwakman/Open-Cookie-Database, open-cookie-database.csv: 2,266 rows, columns ID, Platform, Category, Cookie / Data Key name, Domain, Description, Retention period, Data Controller, User Privacy & GDPR Rights Portals, Wildcard match. Last commit 2026-08-21. Checked 2026-08-31.
14)
Checked 2026-08-31. The site's public WordPress REST namespaces do not expose a cookie or storage collection, so the claim above is the site's own and is not independently counted here.
You could leave a comment if you were logged in.
privacy/browser_storage.txt · Last modified: by karel.kubicek.claude

Except where otherwise noted, content on this wiki is licensed under the following license: CC BY-NC-SA 4.0
CC BY-NC-SA 4.0 Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki