How private mode leaks
Private modes isolate and shrink storage, and that is what gives them away. The historical Chrome tell was the window.webkitRequestFileSystem API, which returned an error in incognito and worked normally otherwise - a one-call check. Google closed that, and detection moved to storage quota: navigator.storage.estimate() reports a much smaller quota in private mode (capped to a fraction of disk, or to a fixed ceiling) than in a normal session on the same machine. Comparing the reported quota against the device's apparent capacity reveals the mode.
Other differences have been used over time: IndexedDB throwing or behaving differently, the Quota API's temporary-vs-persistent storage limits, and service-worker/cache persistence not surviving. Each browser release tends to close the current tell, so the specific probe in use drifts, but the underlying fact - private mode constrains storage - keeps regenerating new ones.
Why sites detect it at all
Two motivations. Risk scoring: abusive and automated traffic is disproportionately private, because scrapers and fraud tooling default to fresh, stateless sessions to avoid carrying identifiers. Incognito alone is not proof of a bot - plenty of real users browse privately - so it is a soft signal folded into a broader score, not a standalone block.
The other motivation is business logic: metered paywalls (news sites) and free-trial limits historically used incognito detection to stop readers from resetting their article count by opening a private window. This is the same plumbing the anti-bot use case relies on, which is why incognito detection shows up both in paywall scripts and in fraud/bot stacks.
Implications for scrapers
For scraping, the practical lesson is that running headless does not put you in incognito by default - a headless browser with a normal user-data directory reports normal storage. But several stealth setups do use private contexts or ephemeral profiles, and if the site scores incognito as risk, that choice is working against you. If a target gates on private mode, the fix is to run with a persistent profile and real storage quota so navigator.storage.estimate() looks like an ordinary install.
As always, coherence matters more than any single value: storage quota should match the claimed device class, and persistence behaviour should be consistent with a returning user if you are presenting cookies and history. Treat incognito detection as one more consistency probe in the stack rather than a special case.
