Headless anti-detect: what breaks and how to keep stealth
Headless anti-detect: what breaks and how to keep stealth
Running a browser without a display sounds like it should be invisible. the browser renders pages, executes javascript, processes cookies, the same as any real user. but that framing is wrong, and if you’ve lost accounts or had automation workflows suddenly start returning captchas at scale, you’ve already learned this the hard way. headless browsers leak identity at a dozen different layers, and the anti-bot vendors know every single one of them.
I’ve been running multi-account and automation workflows out of Singapore for about four years, primarily on platforms that have very active bot-detection arms. the pattern I keep seeing is that operators spend months perfecting their proxy stack and fingerprint profiles, then forget that the browser itself is broadcasting “i am a headless bot” before any of that matters. this article is about exactly that problem. we’ll go layer by layer through what headless browsers expose, what detection systems look for, and what actually works to close the gaps.
the stakes matter here because the detection side has gotten significantly more sophisticated. DataDome, Kasada, Akamai Bot Manager, and PerimeterX (now Human Security) all maintain real-time behavioral models trained on billions of real sessions. they’re not running a simple navigator.webdriver check anymore. if your headless stack was working fine a year ago and it isn’t now, it’s almost certainly not your proxies.
background and prior art
headless Chrome has been detectable since around 2017. the original Intoli blog post catalogued the obvious tells: the HeadlessChrome string in the user-agent, navigator.webdriver returning true, window.chrome being undefined, and a handful of others. the community patched these quickly, and for about two years, a well-configured puppeteer script with those properties spoofed would pass most checks.
the W3C WebDriver specification itself defines navigator.webdriver as a required flag when a browser is under automation. this is a design choice made in the name of user transparency, not a bug. the implication is that any standards-compliant driver will set this flag, meaning compliant usage is inherently detectable. vendors took note. by 2020, detection systems had moved well past the single-flag check into ensemble methods, correlating dozens of signals simultaneously. the Chrome DevTools Protocol gives automation frameworks low-level access to browser internals, but every CDP connection leaves its own traces in browser state, timing characteristics, and process memory layout.
today the primary open-source response to this is the puppeteer-extra ecosystem (maintained by berstend on GitHub) and its Playwright counterpart. these patch surface-level tells reasonably well. but they don’t touch the deeper rendering, timing, and TLS-layer signals that enterprise-grade anti-bot vendors now exploit.
the core mechanism
to understand what breaks, you need a mental model of the detection surface. it breaks into five distinct layers, each of which can be interrogated independently.
layer 1: javascript environment flags
this is the most documented layer. beyond navigator.webdriver, headless Chrome has historically differed in:
navigator.pluginsbeing empty (real Chrome has built-in plugins including Chrome PDF Viewer)navigator.languagesdefaulting to['en-US']with no secondary languageswindow.chromebeing undefined or having an emptyruntimeobjectNotification.permissionreturning'denied'without a user-facing permission dialogwindow.outerWidthandwindow.outerHeightbeing zero in some headless modes
modern headless Chrome (Chrome 112+, with the “headless=new” mode) fixed several of these. HeadlessChrome no longer appears in the user-agent by default. but the fixes are incomplete and the differences are well-catalogued in active detection libraries.
layer 2: rendering and graphics fingerprints
canvas and WebGL fingerprints are generated by the actual GPU and driver stack. headless browsers on cloud VMs render differently because they’re using software rasterization (SwiftShader or llvmpipe) instead of a real GPU. a canvas drawn on a MacBook M2 with a real GPU will produce a different pixel hash than the same canvas drawn by headless Chrome on an AWS t3.medium using software rendering.
the WebGL renderer string is especially telling. a real Chrome session on a modern consumer machine returns something like ANGLE (Apple, ANGLE Metal Renderer: Apple M2, Unspecified Version). headless on a cloud VM returns Google SwiftShader. detection systems maintain databases of expected (renderer, OS, browser version) combinations and flag mismatches.
layer 3: behavioral and timing signals
this is where modern detection is most sophisticated. human mouse movements have specific velocity curves, micro-tremors, and pause patterns. they don’t move in straight lines. clicks have small position variance relative to the center of the target element. scroll events cluster at specific frame-rate intervals tied to the display refresh rate (60Hz or 120Hz), not the arbitrary intervals that automated scrolling tends to produce.
beyond mouse behavior, there are timing signals in javascript itself. performance.now() resolution is deliberately coarsened in browsers post-2018 (to 0.1ms in Chrome, per the Spectre mitigations) but headless environments sometimes expose higher-resolution timing through worker threads or because the isolation flags aren’t set correctly.
layer 4: network and TLS fingerprinting
your browser’s TLS handshake is fingerprinted before a single byte of HTTP content is exchanged. the JA3 and JA4 fingerprints encode the TLS version, cipher suites, extensions, elliptic curves, and extension data format from the ClientHello message. Playwright’s default chromium binary produces a specific JA3 hash. Puppeteer’s binary produces a slightly different one. both differ from real Chrome installed by a user because they’re built with different compile flags and ship without some vendor-specific cipher suite orderings.
if your automation workflow routes through a proxy, the proxy’s TLS stack matters too. a Python requests library making the initial connection before handing off to the browser will produce a Python/urllib3 JA3 fingerprint on that first request, which is trivially detectable.
layer 5: CDP and automation artifact exposure
the Chrome DevTools Protocol itself leaves traces. when a CDP session is active, there are specific internal Chrome flags set in the browser process. some sites probe for these by examining the behavior of certain APIs that differ when CDP is attached. the --enable-automation Chrome flag (set by default by WebDriver and most automation frameworks) triggers a visible automation indicator bar in the browser and also modifies internal Chrome state that javascript can sometimes detect indirectly through timing of certain API calls.
worked examples
example 1: Playwright on a bare AWS EC2 instance
a team I know was running Playwright against a social platform using residential proxies from Brightdata (~$8.40/GB as of Q1 2026). their scripts passed for the first few weeks. then the platform updated their bot detection and their account creation rate dropped from about 85% success to under 20% overnight.
the diagnosis was a combination of three things. first, their EC2 instances (t3.large, Ubuntu 22.04) were rendering canvas with SwiftShader, producing WebGL renderer strings that matched zero real user profiles in any bot detection vendor’s dataset. second, they were using Playwright’s default --no-sandbox flag (required in most Linux containerized environments) which is another automation signal. third, the TLS fingerprint from their Playwright chromium build didn’t match any known Chrome version on any known OS.
the fix had three parts. they moved to Camoufox, a Firefox-based headless browser project built specifically for anti-detect work, which uses real GPU rendering paths where available and ships a patched Firefox binary with consistent fingerprints. for the remaining Playwright workflows where they needed Chromium specifically, they attached a real GPU to a subset of instances (AWS g4dn.xlarge at ~$0.526/hr) to get hardware-accelerated rendering. TLS fingerprint normalization required routing through a tool that injects correct TLS parameters at the socket level.
example 2: undetected-chromedriver for account management
for selenium-based workflows, undetected-chromedriver (the Python package by ultrafine-fish, installable via pip install undetected-chromedriver) patches the chromedriver binary at runtime to remove the cdc_ variable prefixes that webdriver injects into javascript and strips the --enable-automation flag. it also downloads the correct chromedriver version to match your installed Chrome binary.
import undetected_chromedriver as uc
options = uc.ChromeOptions()
options.add_argument('--disable-blink-features=AutomationControlled')
driver = uc.Chrome(options=options, headless=False)
note the headless=False. undetected-chromedriver works significantly better in “virtual display” mode using Xvfb on Linux than in true headless mode. the reason is exactly what was discussed above: true headless mode enables different internal Chrome code paths. using Xvfb (a virtual framebuffer) gives you a headed Chrome session that can run on a display-less server without triggering the headless rendering pipeline. on Ubuntu, this looks like:
Xvfb :99 -screen 0 1920x1080x24 &
export DISPLAY=:99
python your_script.py
with this setup and residential proxies, this team’s detection rate on their target platform dropped to under 5% over a 60-day run. that’s not zero, which matters. the remaining detections were behavioral, not fingerprint-based.
example 3: Camoufox for high-volume scraping
Camoufox deserves its own example because it represents a different architectural approach. rather than patching Chrome (which requires ongoing maintenance as Chrome’s internal APIs change), Camoufox ships a modified Firefox binary with fingerprint injection built directly into the browser’s C++ layer. this means the spoofed values appear at the lowest possible level: before javascript can even interrogate them.
for a scraping workflow hitting an e-commerce platform with Akamai Bot Manager, a setup using Camoufox with rotating mobile fingerprints (Android Firefox profiles) and datacenter proxies with correct ASN labeling achieved 91% success on initial page loads over a two-week run. the key metric was cost: datacenter proxies at $0.40-0.80/GB versus residential at $8+/GB is a 10-20x cost difference. getting datacenter proxies to work requires matching the proxy’s ASN profile to a plausible fingerprint, and Camoufox’s fingerprint injection made this significantly easier.
edge cases and failure modes
pitfall 1: patching navigator.webdriver but not CDP internals
this is the most common mistake. Object.defineProperty(navigator, 'webdriver', {get: () => undefined}) patches the javascript-visible property but does nothing to the CDP session state. some detection systems probe for CDP presence through timing attacks on specific internal Chrome APIs. the fix is to avoid CDP-attached sessions where possible, or to use rebrowser-patches (a real project that patches Playwright at the CDP level to reduce automation indicators).
pitfall 2: font fingerprint mismatches
browsers enumerate system fonts, and the font list is strongly correlated with OS and browser version. headless Chrome on a fresh Ubuntu VM will return a very short list of system fonts. real Chrome on a Mac returns hundreds. spoofing this at the javascript layer is possible but the timing of font enumeration calls differs between real and headless browsers in ways that canvas timing attacks can detect. the safest fix is to pre-install a realistic font set matching your spoofed OS profile. for a Windows 11 profile, install the fonts that ship with Windows 11. the exact list is available from Microsoft’s documentation.
pitfall 3: timezone and locale mismatches
your proxy IP might geolocate to Chicago (CDT, UTC-5). your browser’s Intl.DateTimeFormat().resolvedOptions().timeZone might return Asia/Singapore because that’s your server’s system timezone. this is a trivially detected mismatch. fix it at the OS level (timedatectl set-timezone America/Chicago) or via Playwright’s timezoneId option, not by patching javascript. patching javascript means the value changes after page load, which some detection scripts catch by comparing pre-load and post-load timezone readings.
pitfall 4: screen resolution and device pixel ratio inconsistencies
a real 4K display running Chrome at 200% DPI reports window.devicePixelRatio of 2.0 and screen.width of 1920 (logical pixels). headless Chrome on an EC2 instance with a fake display reports devicePixelRatio of 1.0 and whatever resolution you specified in your launch arguments. the issue is the relationship between these values. if your profile claims to be a MacBook Pro 14” (which has a 3456x2234 native resolution) but reports devicePixelRatio of 1.0, that’s inconsistent. maintain a lookup table of (device model, native resolution, logical resolution, devicePixelRatio) combinations and use them consistently.
pitfall 5: request header ordering and sec-ch-ua consistency
Chromium sends client hints headers (sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform) with specific values that must match the user-agent string. the ordering of HTTP/2 headers is also fingerprinted. a Playwright session using a custom user-agent of a recent Chrome version but sending old-format client hints (or no client hints at all) is immediately flagged. verify that your client hint injection is consistent with your user-agent string. Playwright exposes extraHTTPHeaders for this, but you need to set all three client hint headers in the correct format for your claimed Chrome version.
what we learned in production
the single biggest lesson from running these workflows at scale is that detection systems are now primarily behavioral, not signature-based. fixing fingerprints gets you past the initial challenge, but sustained operation requires human-like behavioral patterns. this means realistic dwell times (not uniform random delays, but gamma-distributed delays that cluster around typical human response times), mouse movement that follows real trajectories, and session lengths that match the target platform’s user base.
the second lesson is that tool choice matters more than configuration. undetected-chromedriver is good for selenium workflows where you need Chrome specifically. for everything else, Camoufox’s approach of patching at the binary level rather than the javascript level is more durable. the reason is maintenance. every Chrome update potentially breaks puppeteer-extra-plugin-stealth patches because they’re hooking into javascript APIs that Chrome can modify. binary-level patches survive more Chrome version cycles. if you’re building a production stack that needs to run for six-plus months without constant maintenance, that durability matters. you can read more about profile management strategy in the multi-account profile setup guide and review the current tool landscape in the 2026 anti-detect browser comparison.
for the proxy side of this equation, getting datacenter IPs to work is primarily about matching the full fingerprint to a plausible use case for that IP range. a residential-looking fingerprint on a datacenter IP is suspicious precisely because it’s inconsistent. the proxyscraping.org blog has good current coverage of proxy ASN classification and how detection vendors use it. when the proxy profile and browser fingerprint are internally consistent, datacenter IPs become viable for a much wider range of targets.
a few operational notes: always run your full stack through a detection test suite before deploying, not just browserleaks.com. creepJS (run your own instance from the GitHub source rather than using the public instance, which logs fingerprints) gives you the most comprehensive view of what you’re leaking. second, build in fingerprint rotation. even a perfect fingerprint becomes a tracking vector if it’s constant across sessions. rotate profiles at session boundaries or after significant account age, not on a fixed timer. the timing of rotation is itself a behavioral signal.
if you’re doing airdrop farming or multi-account social workflows, the airdropfarming.org blog covers platform-specific detection behavior that’s worth cross-referencing with your own testing. different platforms weight different signals differently, and the community documentation there tends to be current.
references and further reading
-
W3C WebDriver 2 Specification , the official spec that defines
navigator.webdriverbehavior and why standards-compliant automation is inherently detectable. -
Chrome DevTools Protocol Documentation , complete reference for the CDP layer, useful for understanding what signals a CDP session exposes and which domains are most fingerprint-relevant.
-
puppeteer-extra on GitHub (berstend) , the primary open-source stealth patching ecosystem for both Puppeteer and Playwright. read the plugin source, not just the README, to understand what’s actually being patched and where the gaps are.
-
Playwright Official Documentation , covers the modern headless implementation details, including the distinction between the old headless mode and the current “headless=new” implementation and what changed in each.
-
Intoli: “Not Possible to Block Chrome Headless” , the 2017 article that kicked off the modern cat-and-mouse cycle. worth reading for historical context; most of the individual tells listed have been patched, but the methodology for identifying new ones remains sound.
for a deeper look at the profile and proxy side of production anti-detect stacks, see the GoLogin vs Multilogin comparison and the residential proxy buyer’s guide.
Written by Xavier Fok
disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-05-19.