detectIncognito logodetectIncognito.js

How Incognito Mode Detection Works

Browsers try hard to make private windows indistinguishable from normal ones — and they keep getting better at it. But private browsing's core promise, that nothing you do persists to disk, forces real differences in how storage APIs behave. Those differences leak. This page documents the actual techniques detectIncognito.js uses, per browser, as of v1.8.

Step zero: identify the engine without the user agent

User-agent strings lie — Brave claims to be Chrome, and everything on iOS is WebKit. So the library first fingerprints the JavaScript engine itself with a one-liner:

javascript
try {
  (-1).toFixed(-1);
} catch (e) {
  return e.message.length; // 44/43 = JavaScriptCore, 51 = V8, 25 = SpiderMonkey
}

Calling toFixed with an invalid argument throws a RangeError whose message text differs between JavaScriptCore (Safari), V8 (Chrome and friends), and SpiderMonkey (Firefox). The message length alone identifies the engine — no user-agent parsing involved. Chromium flavors (Brave, Edge, Opera) are then told apart afterward.

Safari: three generations of storage errors

Modern Safari supports the Origin Private File System. In a private window, navigator.storage.getDirectory() rejects — and the error message contains the distinctive phrase "unknown transient reason". That exact phrasing only appears in private browsing, so it's a clean signal.

On Safari versions without OPFS (it arrived in Safari 15.2), the library falls back to an IndexedDB probe: in private mode, putting a Blob into an object store throws an error containing "are not yet supported". Older Safari still is detected the classic way — openDatabase() and localStorage.setItem() both threw in private windows.

Chrome: a timing side-channel on IndexedDB durability

Chrome is the hard one. Storage quotas used to give incognito away, but Google deliberately randomized and enlarged incognito quotas to kill quota-based detection. The library's current approach doesn't look at what the browser reports — it measures what the storage engine physically does.

In a normal window, Chrome's IndexedDB is backed by on-disk storage: committing a transaction with durability: "strict" forces an fsync, which is measurably slower than durability: "relaxed". In incognito, IndexedDB lives in an in-memory backend — there is no disk, so the "strict" fsync is a no-op and both modes take the same time.

pseudocode
ratio = time(strict commits) / time(relaxed commits)

ratio ≈ 1.0   → in-memory backend → incognito
ratio > 1.3   → real fsync to disk → normal window

The measured ratio is self-normalizing — it holds across fast desktops and slow phones alike, because both sides of the division run on the same hardware. The test takes the median of repeated rounds under a ~1-second budget, and abstains toward "not private" if the durability hint isn't honored at all.

Firefox: the OPFS security error

Firefox's private windows reject navigator.storage.getDirectory() with a "Security error". On older versions, opening an IndexedDB database fails with InvalidStateError — and only that specific error counts, so a quota failure or a disabled-IndexedDB preference doesn't produce a false positive.

Legacy Edge and Internet Explorer

The easiest of all: InPrivate windows in Internet Explorer 11 simply don't expose IndexedDB. window.indexedDB === undefined is the whole test.

Why this can't be fully fixed

Every technique above exploits the same tension: private mode must guarantee nothing touches disk, but web storage APIs are specified as if storage is durable. A browser can rename an error message or shim a quota — and they do, which is why each Safari generation needs a different test — but as long as private-mode storage behaves differently from durable storage in any observable way, including how long it takes, detection remains possible.

Live demo — this window

Running the tests…

Use it yourself

The library is MIT-licensed, dependency-free, and ~2 KB:

npm
npm i detectincognitojs
javascript
import { detectIncognito } from "detectincognitojs";

const { isPrivate, browserName } = await detectIncognito();

FAQ

Can a browser make incognito mode undetectable?
In principle a browser could close each individual leak, and vendors regularly do. But private mode's core promise — nothing persists to disk — forces real behavioral differences in storage APIs, and each new storage feature is a new place for those differences to show. It's a cat-and-mouse game, which is why detection techniques are versioned per browser generation.
Why do websites detect incognito mode at all?
The most common reason is metered paywalls: incognito windows start with a clean cookie jar, so a fresh private window would otherwise reset a free-article counter. Analytics and fraud-prevention systems also treat private sessions differently.
Does a VPN or ad blocker affect detection?
A VPN doesn't — it changes your network route, not your browser's storage behavior. Ad blockers can block the script itself from loading if it's served from a blocked CDN, which is why bundling or self-hosting detectIncognito.js is recommended.
How accurate is this?
High, but not perfect. The library is designed to abstain toward "not private" when a signal is ambiguous, so false positives are rarer than false negatives. Known edge cases, like certain Chrome Guest-mode setups, are tracked in the GitHub issues.

More