What a page can actually detect about headless Chrome
Ask ten people whether headless Chrome is detectable and you get ten answers, most of them out of date. The honest answer changed when Chrome rebuilt headless on the same code as the full browser, and it changes again depending on whether the machine running the browser has a GPU.
What old headless gave away
The original --headless mode ran a genuinely different code path, and it showed:
- A UA token. The user agent string carried
HeadlessChrome/xxdirectly, and plenty of anti-bot rules checked for that substring alone. navigator.pluginsandnavigator.mimeTypes. Headful Chrome reports a small set of built-in plugins, including the PDF viewer. Old headless reported an empty array, which is not something a real desktop browser does.- Window and screen oddities.
outerHeightandouterWidthcame back as0in old headless, because there was no real browser chrome to measure.window.screenvalues could also look implausible, a viewport with no corresponding physical display. - Permission behaviour. Calls like
navigator.permissions.query({name: 'notifications'})sometimes threw or resolved inconsistently with the Notification API's own state, a contradiction a real browser does not produce.
What new headless fixed
Chrome 112 introduced --headless=new, rebuilt on the same code as the full browser rather than a stripped-down pipeline, and Chrome 132 removed the old implementation so that plain --headless now means the new mode. The old one survives separately as chrome-headless-shell. The UA string no longer carries a HeadlessChrome token by default, navigator.plugins reports the same built-in set as a normal window, and outerHeight/outerWidth behave properly. Most of the checklist above, the part people still repeat from pre-112 blog posts, no longer applies.
The tell that is actually left: no GPU
What new headless did not fix, because it is not a headless problem, is what happens on a machine with no display hardware.
WebGL renderer. Query WEBGL_debug_renderer_info and its UNMASKED_RENDERER_WEBGL value. On a real desktop it reads something like an ANGLE string naming an actual NVIDIA, AMD or Intel GPU. On a headless server with no GPU, Chrome falls back to a software rasterizer, SwiftShader on most platforms or llvmpipe under Mesa on Linux, and that string says so directly. Real consumer machines essentially never run a software WebGL renderer, which makes this one of the more reliable server tells available.
Missing codecs. Minimal server images often lack the H.264 or AAC decode libraries that ship by default on a desktop OS, so canPlayType checks and actual playback can differ from what a consumer browser reports.
Font availability. A bare Linux server typically ships a handful of fonts, DejaVu and Liberation are common defaults, against 300 or more on a real Windows or Mac install. Font enumeration through canvas text metrics is a long-standing side channel, and it correlates strongly with headless server deployments simply because that is where minimal font sets live.
None of these are caused by the --headless flag itself. They are caused by running on hardware and an OS image that was never meant to render a display, which headless automation frequently is.
The practical guidance
If you are running on a machine with a real display, a desktop or a laptop, run headful. It costs you nothing that matters for most tasks, and it sidesteps the GPU question entirely because you already have a real graphics driver and a real window manager producing a real WebGL string.
If you have to run on a server, the GPU story is the actual problem to solve, not the headless flag. A virtual display and, where available, a real or virtualized GPU path matter more than any UA or plugin patch, because the renderer string is what a script cannot easily fake without also faking the rendering behind it.
Where a free approach is the better answer
If your task has no detection concerns at all, internal QA, scraping your own staging environment, generating PDFs, plain Playwright or Puppeteer in default headless mode is free, well maintained, and already carries the new headless fixes described above. You do not need an antidetect browser to close tells that Chromium itself already closed.
Where AntiBrow is honest about its limits
AntiBrow's headless option is currently a no-op on macOS: setting it still opens a full window, because there is no macOS equivalent implemented yet. On Linux, headless rendering needs Xvfb, a virtual display server, since the browser still needs somewhere to draw even when nothing is watching. Neither of these is disguised, because a reader would find both out within one test run.
Check it yourself
Open a page and read navigator.plugins.length, navigator.userAgent, and the UNMASKED_RENDERER_WEBGL value from a WEBGL_debug_renderer_info context. On new headless with no GPU, the plugins and UA will look normal and the renderer string will still say SwiftShader or llvmpipe. That single field is doing most of the work now.
Our fingerprint consistency checklist covers the coherence checks around this, including how the renderer string should agree with the claimed OS and GPU.
Try it on the free tier.
Unlimited local profiles, no credit card. Check it against the detectors yourself.