Consistency
A detector reads many values and compares them. A single odd value is common. Detectors flag a set of values that no real machine has. Values against each other. The WebGL renderer string names a GPU, and the WebGL limits, extensions and WebGPU adapter have to be that GPU’s. The platform implies fonts, voices, system colours, a taskbar and a keyboard layout. The core count can be checked by timing parallel work in workers. A claimed x86 CPU can be checked with arithmetic that behaves differently on ARM, such as the sign of a computed NaN, and with Web Audio output. Values against headers. TheUser-Agent, Sec-CH-UA-*, Accept-Language and Device-Memory headers have to match what JavaScript reads.
The main thread against workers and iframes. A detector reads navigator again in a dedicated, shared or service worker, and in iframes. A value overridden only in the page’s main world shows its real value there.
Platform behaviour. Windows Chrome turns new URL("c:/foo") into a file: URL and every other platform does not. Windows and macOS Chrome have navigator.share(); Linux Chrome does not.
The machine against the IP address. The timezone and languages have to fit the location of the IP address the visit comes from.
One visitor across visits. A returning visitor with the same cookies should have the same hardware.
What Apostate does:
- The browser composes one machine from real option tables, with dependent choices, so the GPU, cores, memory, screen and devices belong together. See How it works.
- The C++ that produces each value serves it. The main thread, workers, iframes and headers read the same value because there is only one.
- Platform behaviours follow the persona. A Windows persona parses Windows file paths as Windows Chrome does. On a bare-metal x86 Linux server and on an Apple silicon Mac, adding that change took FingerprintJS Pro’s
anomaly_scorefrom 1 to 0 on every Windows seed tested (5 of 5 seeds on each host, 2026-09-26, development builds of 0.4.3). - Windows and Linux personas report the host’s CPU architecture, so the claim matches what arithmetic reveals. A Windows persona that claimed x86 on an Apple silicon Mac was flagged
virtual_machineby FingerprintJS Pro, and claimingarmcleared it (2026-09-23, development build before 0.4.0). - The packages set the timezone and language from the proxy’s exit before launch.
- A persistent context keeps its machine across launches. See Seeds and identity.
- Install the persona’s fonts on the host. A Windows persona on Linux with no Windows fonts shows text metrics no Windows machine has. See Fonts.
- Pick a host that fits the persona. See Hosts.
- Give each account its own seed or its own persistent context, and keep it.
- Check a launch before you rely on it.
--fingerprint-explainprints the machine and itslimitations, and Verify covers checking from a page.
Automation traces
Drivers such as Playwright and Puppeteer control the browser through the Chrome DevTools Protocol (CDP), and headless mode differs from a desktop browser in small ways. Detectors look for:navigator.webdriverset to true.HeadlessChromein the User-Agent.- Side effects of an attached CDP client, such as console calls that serialize an object and trigger its getters, or stack traces captured for every exception.
- Globals and bindings a driver adds to the page, such as
$cdc_or__playwright, and driver-named frames in stack traces. - Headless geometry: an 800x600 screen,
availHeightequal toheight, a window of zero size, a viewport larger than its window. - A remote debugging port that answers a page’s requests on
localhost. - An incognito profile, which sites can tell from a normal one.
navigator.webdriverstays false under the switches drivers launch with. A client that asks for it through CDP’sEmulation.setAutomationOverridestill gets it.- The User-Agent keeps the
Chromeproduct token in headless mode. - An attached CDP client that sends
Runtime.enablebut notDebugger.enable, as Playwright and Puppeteer do, changes nothing a page can time or count in console calls and exceptions. - The
--remote-debugging-portendpoint closes requests that web pages send. In a measurement from a page on a local address, an open port and a closed port gave the same result. - The screen and work area come from the persona in headless mode too, and
outerWidthandouterHeightare never zero. The packages turn off the drivers’ viewport emulation, whose defaults report sizes no desktop has. From 0.4.4 they also turn off the drivers’ light colour-scheme emulation, so a dark-theme persona reads dark. launch()opens pages in a normal profile, not an off-the-record one.- The packages drive the browser with Patchright by default, which does not send
Runtime.enable. FingerprintJS Pro reporteddeveloper_toolson every run under Playwright and on none under Patchright, the same as with no driver attached (2026-09-25, development build on Chromium 152.0.7977.83, Windows persona, bare-metal x86 Linux server). The likely cause,Runtime.enable, has not been isolated. - The classic sentinels,
$cdc_,__webdriver_evaluate,__playwrightand eight others, were absent under every driver tested, and the page’swindowkeys were identical with and without a driver.
- Keep Patchright, the default driver. Under Puppeteer, stack traces from code your script evaluates carry the script’s absolute file path, and
exposeFunction("hello", ...)adds apuppeteer_helloglobal next tohello. Nothing in the browser can remove them. - Anything your script adds to the page’s main world is visible to the page: an init script, an exposed function, a global set with
evaluate. - Use
new_page(), notnew_context(), which is off-the-record. - Connect over a pipe, as Playwright and Patchright do. If you open
--remote-debugging-port, keep it on 127.0.0.1, its default. Any program that reaches the port can drive the browser.
Network
Detectors score the connection as well as the browser:- IP reputation. Reputation services list datacenter ranges, known VPN and proxy exits, and residential proxy pools. FingerprintJS Pro, for example, reports a residential proxy exit as such.
- Location mismatch. A timezone or language that does not fit the IP’s location.
- WebRTC. ICE candidates that carry the host’s real address, or a WebRTC address that differs from the one HTTP arrives from.
- DNS. Name lookups that leave from the host rather than from the proxy.
- TLS and HTTP/2. The handshake and connection settings, compared with the browser the User-Agent names.
- The browser takes proxy credentials itself, for HTTP, HTTPS and SOCKS5, and keeps them out of logs, error messages and
chrome://version. socks5://resolves names on the proxy.- The packages look up the proxy’s exit before launch and pass its timezone and language. A failed lookup gives a warning, and the packages invent no value in its place.
- Behind a single SOCKS5 proxy that supports UDP ASSOCIATE, WebRTC’s UDP goes through the proxy, so a peer sees the proxy’s exit. Behind any other proxy setup, WebRTC opens no UDP socket.
- The network stack is Chromium 152’s own, so the TLS and HTTP/2 handshakes are the ones Chrome 152 sends.
- Choose the proxy. Its reputation is the largest network signal and Apostate cannot change it.
- Keep one exit per account or session. A rotating exit can show WebRTC one address and HTTP another within one page.
- Use a SOCKS5 proxy that relays UDP if pages use WebRTC. See WebRTC.
- Read the GeoIP warnings, and pass
localeandtimezoneyourself when the lookup cannot reach your exit. See Proxies and Locale and timezone.
Behaviour
Some detectors score how the visitor acts: mouse paths, typing rhythm, scrolling, the time between actions, the order of page visits, and how many requests arrive per minute. Apostate does not move the mouse, type or pace your script. The Node package refuseshumanize: true with an UnsupportedFeatureError.
Pace your script like a person using the site, send input through the driver’s mouse and keyboard, and keep request rates to what one person would produce.