Back to Knowledge Hub
Fingerprint

Undetected Browser: What a Detector Actually Reads

Trace what a challenge page samples, why one browser can pass one vendor and fail another, and how to record per-detector results instead of chasing an undetected claim.

BotBrowser Team

Documentation

Want the structured docs for Fingerprint?

This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.

An "undetected browser" is a claim about a verdict, and a verdict belongs to whoever ran the check. A challenge page does not ask whether a browser is honest; it samples a set of surfaces, compares them with a model it owns, and returns a score whose pass lines and weights are the vendor's. Two detectors can sample different surfaces from the same session and reach opposite conclusions without either being broken. BotBrowser provides a configurable Chromium runtime and documents which signals it can keep consistent, but it does not guarantee any third-party verdict and publishes no pass rate, because a pass rate is a property of a detector version plus a target plus a session, not a property of a browser. The sections below cover what a detector can read, why the readings disagree, and what to record instead of a claim.

TL;DR

  • navigator.webdriver is one boolean. MDN documents that Chrome sets it when --enable-automation, --headless, or --remote-debugging-port=0 is used, so the flag reports how the browser was started, not whether a page will trust it.
  • performance.now() is deliberately coarsened to protect against timing attacks and fingerprinting: MDN documents 5 microseconds of resolution in a cross-origin isolated document and 100 microseconds otherwise. A timing probe run at one resolution cannot be compared with the same probe run at the other.
  • A verdict is a function of the detector's own model. Two vendors sampling different surfaces can disagree about one session, and neither is necessarily wrong about what it measured.
  • Replace the claim with a record: detector name and version, the surfaces it reported, the pass line it applied, the session configuration, and the timestamp. A record can be compared; a claim cannot.
  • The honest deliverable is a per-detector result table with the failure modes named, not an adjective in the product name.

A flow diagram shows one browser session sampled by three separate detectors, each reading a different surface set and applying its own pass line, with the resulting three verdicts written into a per-detector record instead of a single undetected claim.

Contents

What a challenge page can sample

A detector reads what any script on a page can read. That is the whole surface, and it is worth listing because the list is finite and unglamorous.

navigator.webdriver is the member of this group that a page sees first, and on its own it carries a single bit. MDN defines it as a read-only property indicating whether the user agent is controlled by automation, and documents the specific Chrome conditions that make it true: the --enable-automation flag, the --headless flag, or --remote-debugging-port with port 0. Each of those describes a launch configuration. A page that reads the boolean learns how the process was started, and a page that reads only that boolean has decided to trust a single bit.

Adjacent signals describe the same launch from other angles. The presence of an automation-specific binding on the window object, a debugging port that answers on loopback, a user agent string that names a headless build, and a window.chrome object whose shape differs from a stock installation all point at the same question: was this browser started the way a person's browser starts? None of them answers whether the session will be trusted, because a detector can weight them at zero.

The user agent string and Client Hints give different slices of one claim. navigator.userAgent returns a single string, while navigator.userAgentData returns a brands array of { brand, version } pairs, a mobile boolean and a platform string. The getHighEntropyValues() method accepts a list of fields, including architecture, bitness, model, platformVersion and uaFullVersion, and returns a promise with the requested values. A detector that reads only brands sees less than one that asks for uaFullVersion and bitness, which is another example of two different surface sets.

SignalWhat it describesWhy it is weak alone
navigator.webdriverWhether automation flags were usedOne boolean, and vendors may ignore it
Automation binding on windowWhich library injected scriptsAbsent in a direct launch, present in some frameworks
Debugging port reachableWhether a protocol endpoint is openA closed port is normal for a person's browser
User agent stringWhat the browser claims to beConfigurable, and a claim rather than a measurement
window.chrome shapeWhich Chromium build familyVaries by build, not by trustworthiness

Storage and permission surfaces

Storage is a fourth group, and it is the one that changes with a user's history rather than with the browser build. Cookie availability, whether third-party storage is partitioned, whether a quota probe returns the same number twice, and whether an origin has been visited before all describe a session's past rather than its configuration. A detector that reads them is asking whether this profile has been here before, which is a different question from whether the browser is automated.

Quota and usage are available through navigator.storage.estimate(), which returns a promise with quota and usage in bytes. Enumerating databases through indexedDB.databases() returns names and versions, and caches.keys() lists cache names. Those three calls give a detector information about accumulated state without reading any content: the mere presence of a database with a name or a cache with a version already separates a fresh profile from a used one.

Permissions behave the same way. The set of permissions a document can query, the state each returns, and whether a request triggers a prompt are all observable without any user interaction. The value of Notification.permission and the result of navigator.permissions.query({ name: 'geolocation' }) are two examples of that reading. A page can therefore build a picture of a profile's prior consent decisions, and a fresh profile and a long-lived one answer differently even when the browser build is identical.

That is why a monitoring or comparison workload should decide deliberately whether it wants a fresh profile or a persistent one, and record the choice. The two produce different answers on this surface, and neither is more trustworthy than the other; they are simply different sessions.

Timing and performance surfaces

Timing is easy to overlook, because it looks like a measurement of the machine rather than of the browser. MDN documents that performance.now() is coarsened on purpose: 5 microseconds of resolution in a cross-origin isolated document, 100 microseconds otherwise. That is a twentyfold difference in the smallest observable interval, and it is a property of the document's isolation state rather than of the hardware.

The consequence for a comparison is direct. A probe that measures the smallest non-zero delta between two performance.now() calls reads the resolution, not the machine. Run the same probe in an isolated and a non-isolated document and the numbers differ by a factor of twenty. A detector that samples timer behaviour therefore observes the isolation state of the document it is running in, and a team that changes that state between runs has changed the measurement.

The crossOriginIsolated property is available to the page as a boolean, which lets a detector classify a document into one of the two resolution groups without measuring any interval at all. A session record without that field leaves open which of the two values a timer could have observed.

A second clock behaves differently. Date.now() returns whole milliseconds, so a probe built on it cannot resolve an interval smaller than one millisecond and does not show the coarsening of performance.now(). A requestAnimationFrame callback arrives at the display refresh rate, roughly sixty times per second at 60 Hz and roughly one hundred twenty times per second on a 120 Hz panel. A detector that mixes the three time sources gets three different precision floors, and one more reason to record the document configuration.

Network and media surfaces

RTCPeerConnection exposes local candidate information to a page that creates one, which is why candidate gathering is one way to inspect a WebRTC leak. The same call also reveals whether a connection path supports the protocol at all, and a page can distinguish a blocked path from a working one without ever completing a call. Media devices, canvas rendering, WebGL parameters, font metrics and audio processing are the same kind of surface: each is a legitimate API whose output depends on the environment, and each can be sampled without the page asking permission for anything unusual.

navigator.mediaDevices.enumerateDevices() returns a device list whose content depends on permission: MDN documents that devices other than the defaults are listed only once permission has been granted, so a document that never asked sees the defaults and no labels. The kind field is still populated, which is why a detector can count device classes without prompting; deviceId, label and groupId come back as empty strings until capture is granted. Canvas output depends on the driver, the antialiasing setting and the installed fonts, WebGLRenderingContext.getParameter() names the renderer vendor and model, and an AudioContext reports its sampleRate, while a DynamicsCompressorNode exposes ratio, knee, attack and release. The canvas, WebGL and audio readings do not require calling getUserMedia; populated device identifiers and labels do require media permission or capture.

Network metrics add another independent slice. navigator.connection.effectiveType takes the values slow-2g, 2g, 3g or 4g, the rtt and downlink fields estimate latency and throughput, and saveData reports a data-saving mode. Entries from performance.getEntriesByType('resource') carry nextHopProtocol, transferSize, encodedBodySize and decodedBodySize, so a page can distinguish the connection protocol and the transferred body size without access to the response content.

The point of listing them together is that they are independent. A detector that samples only timing, only canvas, or only the navigation flags is looking at a different slice of the same session, and a browser change that satisfies one slice can move another. The BotD detector registry shows which checks one open-source detector actually runs, and what public detector suites actually test compares the surface sets several public suites sample.

Why two detectors disagree about one session

The disagreement is structural, not a bug in either product.

Each vendor owns its model

A detector's output is a score produced by weights and pass lines the vendor chooses and revises. Two vendors can sample the same session and reach different verdicts for three ordinary reasons: they sample different surface sets, they weight a shared surface differently, and they place their pass line at different points. None of those is visible from the browser side.

That is why a "pass rate" cannot be stated without its scope. Quoting one number without naming the model version, the target and the session is quoting a measurement with its conditions removed.

Reason for disagreementWhat differsWhat the browser can do about it
Different surface setsOne vendor reads timing, another reads canvasNothing directly; the surface is the vendor's choice
Different weightsThe same signal counts more or lessNothing; weights are internal
Different pass linesThe pass line sits elsewhereNothing; the pass line is not published
Different detector versionThe model changed between releasesRe-test after each release
Different targetA different site runs a different configurationTest the actual target

The same browser, two verdicts

A session that passes one vendor and fails another is the normal case rather than the contradiction it looks like. The passing vendor may not sample the surface that changed; the failing vendor may sample only that surface. A team that reads the pair as "the browser is half undetected" has invented a metric that no vendor reports.

The useful reading is narrower. Each verdict is a statement about one detector's model applied to one session. Two verdicts together say that the session sits near a boundary in one model and comfortably inside another, which is information about the boundary rather than about the browser.

What "undetected" cannot mean

The word promises a result that no local change can deliver, because the result is produced elsewhere by a model the operator does not control. A configuration can remove a specific flag, align a specific set of signals, and keep a profile consistent across a run. It cannot decide what a vendor's model will do with the result, and it cannot promise the model stays the same next month.

Treating the word as a product claim rather than a description of a past observation is a mistake this category invites. A vendor that publishes a pass rate has published one measurement under conditions that may not match yours.

Replace the claim with a measurement record

The alternative to a claim is a record that can be re-checked. It has five parts, and each is cheap.

The five fields

The first is the detector: its name and the version or date of the model that produced the verdict. Without a version, a result cannot be reproduced after the vendor updates. The second is the surface set the detector reported, in the detector's own words, because a vendor's category names are the only shared vocabulary available. The third is the pass line or category the verdict landed in, not a converted percentage. The fourth is the session configuration: browser build, profile identifier, launch flags, route, locale, timezone and the isolation state of the document. The fifth is the timestamp and the target.

FieldExample shapeWhy it must be recorded
Detector identityvendor name plus model dateA model change invalidates a comparison
Reported surfacesthe vendor's own category labelsThose labels are the only shared vocabulary
Verdict categorypass, challenge, block, or the vendor's own termA converted percentage loses the pass line
Session configurationbuild, profile, flags, route, locale, isolationTwo configurations are not one measurement
Target and timeURL class plus timestampThe same vendor scores different targets differently

Recording the configuration that produced a verdict

The configuration field is easy to omit, and it is the one that makes the record useful. A browser build identifier, a profile identifier, the launch flags in effect, the route's egress region, the negotiated locale and the document's cross-origin isolation state together describe the session well enough that a second run can be set up to match. Recording the isolation state matters for the reason the timing section gave: it changes the resolution a timing probe can observe by a factor of twenty.

Record the effective values rather than the requested ones. A requested locale that the site overrode, or a requested route that failed over, produces a session that does not match the record unless the record captures what actually applied.

A result table instead of an adjective

The deliverable that survives scrutiny is a table with one row per detector and one column per field above, plus a column for the observed verdict and a column for the failure mode when a detector blocked. A reader can then see that a browser passed two detectors and was challenged by a third, and can decide whether the third matters for their target.

That table is also the honest answer to the question the product name implies. It says what was measured, by whom, under which configuration, and when. It does not say "undetected", because that word would erase every column in it.

What to re-test after a change

Three changes invalidate a stored result. A detector release changes the model, so the row must be re-measured rather than re-read. A browser or profile update changes the session configuration, so the row's configuration field no longer matches. A target change, including a new challenge version on the same site, changes the thing being tested.

Everything else can be reused. That is the practical benefit of the record: it turns an open-ended question into a small set of rows that need refreshing when something specific changes.

FAQ

Does setting navigator.webdriver to false make a browser undetected?

No. MDN documents the property as a signal that reports whether automation flags were used, and a detector is free to weight it at zero or to cross-check it against other observations. A page that reads the boolean has read one bit; a vendor's model reads many, and the model's weights are not published.

Why does the same browser pass one detector and fail another?

Because the two vendors sample different surfaces, weight shared surfaces differently and place their pass lines at different points. A session near a boundary in one model can sit comfortably inside another. The pair of verdicts describes two models, not two halves of one browser.

Can a pass rate be published as a product property?

A rate can be published only with its conditions: detector and model version, target, session configuration and measurement window. Without those, the number describes a measurement that cannot be reproduced, and a later vendor update can invalidate it without any change on the browser side.

Practical conclusion

Start from the surface list rather than from the adjective. Read what a challenge page can sample, note that navigator.webdriver reports a launch flag and that performance.now() resolution depends on the document's isolation state, and accept that a verdict is produced by a model the operator does not control.

The next action is one table. Pick the detector that matters for your target, record its name and model date, the surfaces it reported, the category it returned, and the full session configuration including the isolation state, then run it twice under the same configuration to see whether the row is stable. Two matching rows prove the measurement repeats; they do not prove a verdict, and that distinction is the whole point.

Sources

#Undetected Browser#Bot Detection#Fingerprinting#Navigator.webdriver#Timing

This article is part of the guide: What Is a Browser Fingerprint?

Run the same check in BotBrowser

Load a profile, keep the signals you just read about consistent inside the browser, and confirm the result on your own machine before a rollout.