Open the cookie list your consent banner publishes and count the third-party scripts on it. A script that stores nothing there can still hold a stable identifier for every visitor. It sits in web storage instead, the shelf most scripts reach for blocks the main thread, and they reach for it at the worst moments a page has.
A cookie audit sees one shelf of several
A browser gives a script several places to keep an identifier across a page load. The four a script is likely to use are cookies, localStorage, sessionStorage and IndexedDB, and behind them sit the Cache API, the origin private file system, a service worker registration and window.name, each of which has been used for the same purpose at one time or another. A consent banner enumerates the first of those, and so does most of the vendor paperwork built from it. An inventory assembled that way covers a small fraction of the surface, and the shelves it skips are the ones a reviewer cannot see without opening the storage panel and knowing which database to look in.
Our own tag sits inside that blind spot. It sets no cookie: there is no document.cookie anywhere in the widget’s source, and every fetch it makes goes out with credentials omitted. The visitor identifier is a single IndexedDB entry, one per project. A reviewer auditing us by the cookie list would record that we store nothing on the visitor’s machine, and that conclusion would be wrong.
The shelf is what matters, not the presence of a cookie. IndexedDB is same-origin, so the entry lives in the customer’s own origin and cannot be read from anybody else’s site, while a third-party cookie is the opposite arrangement by design. What a script keeps on the device and what it sends off it are separate questions, and our answer to the second is set out under security.
Synchronous means the page cannot yield while it waits
localStorage and sessionStorage are synchronous by specification. The call returns the value, so the main thread stops until the browser has finished, and there is no point at which the page can hand the CPU back to itself. IndexedDB returns a request object instead, and the page keeps running while the answer is prepared.
That single difference decides what a slow disk does to a page. A slow synchronous read makes the page late. A slow asynchronous read makes the answer late, and an answer that is late can be abandoned on a deadline, which is a choice the synchronous version never offers.
There is a second trap a performance audit usually walks past. Reaching for localStorage is itself code that can throw: Safari with cookies blocked raises on the property access, before any method is called on it. An audit wrapper that patches only Storage.prototype.getItem never sees that access, and a script that reads the property outside a try block takes the host page down with it on browsers where the feature is switched off entirely.
The four moments a script reaches for storage
The cost of an access is the cost of the operation multiplied by how badly the moment was chosen, and there are four moments a third-party script is tempted. The last column is ours, and it is last on purpose: the first three are the ones a reviewer can apply to any vendor.
| The moment | Why it costs | What to require of any vendor | What our contract does |
|---|---|---|---|
| Before the page has painted | Competes with the host’s own first render, when the main thread is the scarcest thing on the page | No storage access on the load path at all | The file served as widget.js performs no storage access of any kind |
| Inside the handler for an interaction somebody is waiting on | This is the window responsiveness metrics measure, so a blocking read lands directly in the score | Storage access deferred out of input handlers | The session record is read once per document, and written on this path only to clear it, when an interaction arrives 30 minutes or more after the last one |
| On page hide or visibility change | The browser is already trying to freeze the page, so the write extends the freeze and may be discarded anyway | A beacon on the exit path, never a write | The exit path hands the browser a beacon, and the same visibility check that gates every refresh closes the write path |
| Once per captured event | Turns an in-memory buffer into steady disk traffic that scales with how busy the page is | A declared write cadence, not a write per event | The refresh writes at most once per visible minute |
Not one of those answers mentions us, which is the test each row had to pass. The tag those rows describe is the feedback widget, and the rows are the questions we had to answer before it could go on anybody else’s checkout page. The last row carries the rule the other three lean on: the busy path calls a method whose only job is to decide whether this minute has already been written, so a page that produces a thousand events cannot turn them into a thousand writes. Ours calls that method on every piece of evidence it records, and almost every call returns without touching the disk.
What to ask a vendor, and what we answer
Five questions are enough to tell a contract from a default. Which APIs does the script touch, what ceiling does it hold itself to per record, what time budget does a single operation get, how often does it write, and what does it do when storage throws.
| The question | What our contract answers for the automatic session |
|---|---|
| Which API | IndexedDB for the visitor identifier, sessionStorage for the one per-tab session record, no cookie |
| Size ceiling per record | 128 characters, checked against the serialized string before the write is attempted |
| Time budget per operation | 2 ms, measured around the call itself |
| Write cadence | The refresh writes at most once per visible minute, and never while the tab is hidden |
| On failure or overrun | That record is abandoned for the rest of the runtime epoch |
Scoping the table to one path is the point of asking the question precisely, because the honest answer for most scripts is a list rather than a single API. Ours keeps two reporter preferences in localStorage: where a launcher was dragged to, behind a project setting that is off by default, and a reviewer’s name in review mode. A preference that has to survive the tab is the legitimate case for synchronous storage. The test to apply to any such exception is whether its read can land near an interaction somebody is waiting on. What a page carrying our tag gets is one write per deliberate gesture rather than one per event, on a path an admin has to switch on before anybody can reach it: the drag write happens in a pointer handler, which the table above names as a bad moment, and the setting that allows a launcher to be dragged ships off. A reviewer is entitled to weigh that trade themselves.
The 2 ms row is the one worth stealing. A budget nobody measures is a comment, so the widget times its own storage call and compares the elapsed value against the budget; an operation that runs long sets a flag that stops every later access to that record for the rest of the epoch. A test that hands the object a clock which advances 3 ms on read asserts that zero writes follow, which is what makes the number a contract rather than an intention.
When storage fails, decide once and stop asking
Storage failure is a normal condition rather than an error: private mode, a full quota, a hardened browser and a blocked property access are ordinary states of ordinary machines. A script that treats them as exceptions retries, and every retry lands in the same conditions that caused the first failure, on a page that has since become busier. Deciding once per page load and never asking again is the cheaper design and the more honest one.
Our automatic path resolves identity once per document, behind a 100 ms wall deadline. A timeout, a blocked database, private mode and a full quota are deliberately not distinguished: all four select an ephemeral identifier for the whole runtime epoch, with no retry and no switch of identity part way through a session. That epoch is the same unit the runtime uses to withdraw its other capabilities under load, which the host isolation contract sets out in full.
That choice has a price, and the visitor pays it in continuity. A visitor whose storage never answered is a new visitor every time, so some visits by the same person never join up and returning-visitor continuity is best-effort by design. The session record refreshes at most once per visible minute, which makes the 30 minute idle boundary precise to roughly a minute rather than to the second. Both are the same trade read from two directions: a script that writes less knows less, and the reviewer deciding what to allow onto a checkout page is entitled to know which way each vendor made that call.